@nombaoss/nomba-node 0.0.0-stage → 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 +21 -0
- package/README.md +467 -2
- package/dist/index.cjs +1557 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1365 -0
- package/dist/index.d.ts +1365 -0
- package/dist/index.js +1532 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1532 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var NombaError = class extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* Set on errors from money-moving calls (`transfers.toBank`, `transfers.toWallet`,
|
|
7
|
+
* `bills.*` purchases, `checkout.chargeSavedCard`). After a timeout or connection
|
|
8
|
+
* error, requery with this reference before retrying, and retry with the same one.
|
|
9
|
+
*/
|
|
10
|
+
reference;
|
|
11
|
+
constructor(message, options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.name = new.target.name;
|
|
14
|
+
this.reference = void 0;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function attachErrorReference(err, reference) {
|
|
18
|
+
if (err instanceof NombaError) {
|
|
19
|
+
Object.defineProperty(err, "reference", {
|
|
20
|
+
value: Object.freeze({ ...reference }),
|
|
21
|
+
enumerable: true,
|
|
22
|
+
writable: false,
|
|
23
|
+
configurable: true
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var ConfigurationError = class extends NombaError {
|
|
28
|
+
};
|
|
29
|
+
var WebhookVerificationError = class extends NombaError {
|
|
30
|
+
};
|
|
31
|
+
var ConnectionError = class extends NombaError {
|
|
32
|
+
/** HTTP method of the failed request. */
|
|
33
|
+
method;
|
|
34
|
+
/** Request path, without query string. */
|
|
35
|
+
path;
|
|
36
|
+
constructor(message, options = {}) {
|
|
37
|
+
super(message, { cause: options.cause });
|
|
38
|
+
this.method = options.method;
|
|
39
|
+
this.path = options.path;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var TimeoutError = class extends ConnectionError {
|
|
43
|
+
};
|
|
44
|
+
var ApiError = class _ApiError extends NombaError {
|
|
45
|
+
/** HTTP status of the response, or `null` when the error was raised before sending. */
|
|
46
|
+
httpStatus;
|
|
47
|
+
/** Nomba's `code` field from the response envelope, if present. */
|
|
48
|
+
code;
|
|
49
|
+
/** Nomba's `description` (human-readable message), if present. */
|
|
50
|
+
description;
|
|
51
|
+
/** Parsed response body. */
|
|
52
|
+
body;
|
|
53
|
+
/** HTTP method of the request. */
|
|
54
|
+
method;
|
|
55
|
+
/** Request path, without query string. */
|
|
56
|
+
path;
|
|
57
|
+
constructor(init, message) {
|
|
58
|
+
super(message ?? _ApiError.formatMessage(init));
|
|
59
|
+
this.httpStatus = init.httpStatus;
|
|
60
|
+
this.code = init.code;
|
|
61
|
+
this.description = init.description;
|
|
62
|
+
this.body = init.body;
|
|
63
|
+
this.method = init.method;
|
|
64
|
+
this.path = init.path;
|
|
65
|
+
}
|
|
66
|
+
/** `Nomba API error 400 (code "400"): <description> [POST /v2/transfers/bank]` */
|
|
67
|
+
static formatMessage(init) {
|
|
68
|
+
let msg = "Nomba API error";
|
|
69
|
+
if (init.httpStatus !== null) msg += ` ${init.httpStatus}`;
|
|
70
|
+
if (init.code !== void 0) msg += ` (code "${init.code}")`;
|
|
71
|
+
msg += `: ${init.description ?? "no description"}`;
|
|
72
|
+
if (init.method && init.path) msg += ` [${init.method} ${init.path}]`;
|
|
73
|
+
return msg;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var AuthenticationError = class extends ApiError {
|
|
77
|
+
};
|
|
78
|
+
var PermissionDeniedError = class extends ApiError {
|
|
79
|
+
};
|
|
80
|
+
var NotFoundError = class extends ApiError {
|
|
81
|
+
};
|
|
82
|
+
var ValidationError = class _ValidationError extends ApiError {
|
|
83
|
+
/** Client-side validation error: nothing was sent. */
|
|
84
|
+
static client(description) {
|
|
85
|
+
return new _ValidationError(
|
|
86
|
+
{ httpStatus: null, description },
|
|
87
|
+
`Nomba validation error: ${description}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var RateLimitError = class extends ApiError {
|
|
92
|
+
/** Seconds to wait, from the `Retry-After` header, if present. */
|
|
93
|
+
retryAfter;
|
|
94
|
+
/** `X-Rate-Limit-*` header values. */
|
|
95
|
+
rateLimit;
|
|
96
|
+
constructor(init) {
|
|
97
|
+
super(init);
|
|
98
|
+
this.retryAfter = init.retryAfter;
|
|
99
|
+
this.rateLimit = init.rateLimit ?? {};
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var ServerError = class extends ApiError {
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/auth.ts
|
|
106
|
+
var InMemoryTokenStore = class {
|
|
107
|
+
#token = null;
|
|
108
|
+
get() {
|
|
109
|
+
return this.#token;
|
|
110
|
+
}
|
|
111
|
+
set(token) {
|
|
112
|
+
this.#token = token;
|
|
113
|
+
}
|
|
114
|
+
toJSON() {
|
|
115
|
+
return { tokenStore: "InMemoryTokenStore" };
|
|
116
|
+
}
|
|
117
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
118
|
+
return "InMemoryTokenStore {}";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
122
|
+
var FALLBACK_LIFETIME_MS = 30 * 60 * 1e3;
|
|
123
|
+
var TokenManager = class {
|
|
124
|
+
#deps;
|
|
125
|
+
#inflight = null;
|
|
126
|
+
constructor(deps) {
|
|
127
|
+
this.#deps = deps;
|
|
128
|
+
}
|
|
129
|
+
#now() {
|
|
130
|
+
return this.#deps.now ? this.#deps.now() : Date.now();
|
|
131
|
+
}
|
|
132
|
+
/** Current access token, obtaining or refreshing one if needed. */
|
|
133
|
+
async getAccessToken() {
|
|
134
|
+
if (this.#inflight) return this.#inflight;
|
|
135
|
+
const cached = await this.#deps.store.get();
|
|
136
|
+
if (cached && this.#expiryMs(cached) - this.#now() > REFRESH_MARGIN_MS) {
|
|
137
|
+
return cached.accessToken;
|
|
138
|
+
}
|
|
139
|
+
if (this.#inflight) return this.#inflight;
|
|
140
|
+
this.#inflight = this.#obtain(cached).finally(() => {
|
|
141
|
+
this.#inflight = null;
|
|
142
|
+
});
|
|
143
|
+
return this.#inflight;
|
|
144
|
+
}
|
|
145
|
+
/** Drop the cached token, but only if it is still the one that failed. */
|
|
146
|
+
async invalidate(failedAccessToken) {
|
|
147
|
+
const cached = await this.#deps.store.get();
|
|
148
|
+
if (cached && cached.accessToken === failedAccessToken) {
|
|
149
|
+
await this.#deps.store.set(null);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async current() {
|
|
153
|
+
return this.#deps.store.get();
|
|
154
|
+
}
|
|
155
|
+
async clear() {
|
|
156
|
+
await this.#deps.store.set(null);
|
|
157
|
+
}
|
|
158
|
+
async #obtain(cached) {
|
|
159
|
+
if (cached?.refreshToken) {
|
|
160
|
+
try {
|
|
161
|
+
return await this.#save(await this.#deps.refresh(cached.refreshToken));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const status = err instanceof ApiError ? err.httpStatus : null;
|
|
164
|
+
const clientError = status !== null && status >= 400 && status < 500 && status !== 429;
|
|
165
|
+
const nonSuccessCode = err instanceof ApiError && status !== null && status < 300;
|
|
166
|
+
if (!clientError && !nonSuccessCode) {
|
|
167
|
+
if (this.#expiryMs(cached) > this.#now()) return cached.accessToken;
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return this.#save(await this.#deps.issue());
|
|
173
|
+
}
|
|
174
|
+
async #save(res) {
|
|
175
|
+
const accessToken = typeof res.access_token === "string" ? res.access_token : "";
|
|
176
|
+
if (!accessToken) {
|
|
177
|
+
throw new ApiError(
|
|
178
|
+
{
|
|
179
|
+
httpStatus: 200,
|
|
180
|
+
description: "token response did not include an access_token"
|
|
181
|
+
},
|
|
182
|
+
"Nomba API error: token response did not include an access_token"
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
let expiresAt = typeof res.expiresAt === "string" ? res.expiresAt : "";
|
|
186
|
+
if (Number.isNaN(Date.parse(expiresAt))) {
|
|
187
|
+
expiresAt = new Date(this.#now() + FALLBACK_LIFETIME_MS).toISOString();
|
|
188
|
+
}
|
|
189
|
+
await this.#deps.store.set({
|
|
190
|
+
accessToken,
|
|
191
|
+
refreshToken: typeof res.refresh_token === "string" ? res.refresh_token : "",
|
|
192
|
+
expiresAt
|
|
193
|
+
});
|
|
194
|
+
return accessToken;
|
|
195
|
+
}
|
|
196
|
+
#expiryMs(token) {
|
|
197
|
+
const t = Date.parse(token.expiresAt);
|
|
198
|
+
return Number.isNaN(t) ? 0 : t;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/constants.ts
|
|
203
|
+
var TransactionStatus = {
|
|
204
|
+
/** Service delivered to the destination. Do not refund. */
|
|
205
|
+
SUCCESS: "SUCCESS",
|
|
206
|
+
/** Still processing. Wait for the webhook or requery. */
|
|
207
|
+
PENDING_BILLING: "PENDING_BILLING",
|
|
208
|
+
/** Still processing (v1 endpoints). Wait for the webhook or requery. */
|
|
209
|
+
PROCESSING: "PROCESSING",
|
|
210
|
+
/** Funds returned to your Nomba wallet. Refund your customer. */
|
|
211
|
+
REFUND: "REFUND",
|
|
212
|
+
/** The transaction will not proceed. Contact Nomba support. */
|
|
213
|
+
CANCELLED: "CANCELLED",
|
|
214
|
+
/** Debit failed; Nomba may retry. Wait for the webhook. */
|
|
215
|
+
PAYMENT_FAILED: "PAYMENT_FAILED",
|
|
216
|
+
/** Nomba may retry via another vendor. Wait for the webhook. */
|
|
217
|
+
REVERSED_BY_VENDOR: "REVERSED_BY_VENDOR"
|
|
218
|
+
};
|
|
219
|
+
var WebhookEventType = {
|
|
220
|
+
PAYMENT_SUCCESS: "payment_success",
|
|
221
|
+
PAYMENT_FAILED: "payment_failed",
|
|
222
|
+
PAYMENT_REVERSAL: "payment_reversal",
|
|
223
|
+
PAYOUT_SUCCESS: "payout_success",
|
|
224
|
+
PAYOUT_FAILED: "payout_failed",
|
|
225
|
+
PAYOUT_REFUND: "payout_refund"
|
|
226
|
+
};
|
|
227
|
+
var SUCCESS_CODES = ["00", "200", "201"];
|
|
228
|
+
var BASE_URLS = {
|
|
229
|
+
sandbox: "https://sandbox.nomba.com",
|
|
230
|
+
production: "https://api.nomba.com"
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/internal/utils.ts
|
|
234
|
+
function encodePathSegment(value, name = "path parameter") {
|
|
235
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
236
|
+
throw ValidationError.client(`${name} must be a non-empty string`);
|
|
237
|
+
}
|
|
238
|
+
const s = String(value);
|
|
239
|
+
if (s.trim() === "") throw ValidationError.client(`${name} must not be empty`);
|
|
240
|
+
if (s === "." || s === "..") throw ValidationError.client(`${name} must not be "${s}"`);
|
|
241
|
+
if (s.includes("/") || s.includes("\\")) {
|
|
242
|
+
throw ValidationError.client(`${name} must not contain "/" or "\\"`);
|
|
243
|
+
}
|
|
244
|
+
return encodeURIComponent(s);
|
|
245
|
+
}
|
|
246
|
+
function buildQuery(query) {
|
|
247
|
+
if (!query) return "";
|
|
248
|
+
const params = new URLSearchParams();
|
|
249
|
+
for (const [key, value] of Object.entries(query)) {
|
|
250
|
+
if (value === null || value === void 0) continue;
|
|
251
|
+
params.append(
|
|
252
|
+
key,
|
|
253
|
+
typeof value === "boolean" ? value ? "true" : "false" : String(value)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const s = params.toString();
|
|
257
|
+
return s ? `?${s}` : "";
|
|
258
|
+
}
|
|
259
|
+
function requireFields(obj, keys, context) {
|
|
260
|
+
for (const key of keys) {
|
|
261
|
+
const v = obj?.[key];
|
|
262
|
+
if (v === void 0 || v === null || v === "") {
|
|
263
|
+
throw ValidationError.client(`${context}: "${key}" is required`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function requireExactlyOne(obj, keys, context) {
|
|
268
|
+
const set = keys.filter((k) => {
|
|
269
|
+
const v = obj?.[k];
|
|
270
|
+
return v !== void 0 && v !== null && v !== "";
|
|
271
|
+
});
|
|
272
|
+
if (set.length !== 1) {
|
|
273
|
+
throw ValidationError.client(
|
|
274
|
+
`${context}: pass exactly one of ${keys.map((k) => `"${k}"`).join(", ")}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
var MAX_PAGE_LIMIT = 50;
|
|
279
|
+
function validateLimit(limit) {
|
|
280
|
+
if (limit === void 0 || limit === null) return;
|
|
281
|
+
if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) {
|
|
282
|
+
throw ValidationError.client(
|
|
283
|
+
`"limit" must be an integer between 1 and ${MAX_PAGE_LIMIT}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function randomUUID() {
|
|
288
|
+
const c = globalThis.crypto;
|
|
289
|
+
if (c && typeof c.randomUUID === "function") return c.randomUUID();
|
|
290
|
+
const nodeCrypto = await import('node:crypto');
|
|
291
|
+
return nodeCrypto.randomUUID();
|
|
292
|
+
}
|
|
293
|
+
var timing = {
|
|
294
|
+
sleep(ms) {
|
|
295
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function isRecord(v) {
|
|
299
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
300
|
+
}
|
|
301
|
+
function readEnv(name) {
|
|
302
|
+
const proc = globalThis.process;
|
|
303
|
+
const v = proc?.env?.[name];
|
|
304
|
+
return v === void 0 || v === "" ? void 0 : v;
|
|
305
|
+
}
|
|
306
|
+
function runtimeVersion() {
|
|
307
|
+
const proc = globalThis.process;
|
|
308
|
+
return proc?.version ? `node/${proc.version}` : "runtime/unknown";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/internal/http.ts
|
|
312
|
+
var SUCCESS_CODES2 = new Set(SUCCESS_CODES);
|
|
313
|
+
var MAX_BACKOFF_MS = 8e3;
|
|
314
|
+
var INITIAL_BACKOFF_MS = 500;
|
|
315
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
316
|
+
function backoffMs(attempt, random = Math.random) {
|
|
317
|
+
const base = Math.min(MAX_BACKOFF_MS, INITIAL_BACKOFF_MS * 2 ** attempt);
|
|
318
|
+
return Math.round(base * (1 - 0.25 * random()));
|
|
319
|
+
}
|
|
320
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
321
|
+
if (value === null || value.trim() === "") return void 0;
|
|
322
|
+
const secs = Number(value);
|
|
323
|
+
if (Number.isFinite(secs)) return Math.max(0, secs);
|
|
324
|
+
const date = Date.parse(value);
|
|
325
|
+
if (Number.isNaN(date)) return void 0;
|
|
326
|
+
return Math.max(0, Math.ceil((date - now) / 1e3));
|
|
327
|
+
}
|
|
328
|
+
var HttpClient = class {
|
|
329
|
+
#config;
|
|
330
|
+
#tokens = null;
|
|
331
|
+
constructor(config) {
|
|
332
|
+
this.#config = config;
|
|
333
|
+
}
|
|
334
|
+
setTokenManager(tokens) {
|
|
335
|
+
this.#tokens = tokens;
|
|
336
|
+
}
|
|
337
|
+
/** Make a request and return the unwrapped `data` of a successful envelope. */
|
|
338
|
+
async request(opts) {
|
|
339
|
+
const { reference } = opts;
|
|
340
|
+
if (!reference) return await this.#authorized(opts);
|
|
341
|
+
try {
|
|
342
|
+
return withReference(await this.#authorized(opts), reference);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
attachErrorReference(err, reference);
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async #authorized(opts) {
|
|
349
|
+
if (opts.token === "obtain" || !this.#tokens) {
|
|
350
|
+
return this.#send(opts, void 0);
|
|
351
|
+
}
|
|
352
|
+
const token = await this.#tokens.getAccessToken();
|
|
353
|
+
try {
|
|
354
|
+
return await this.#send(opts, token);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (!(err instanceof AuthenticationError) || err.httpStatus !== 401) throw err;
|
|
357
|
+
await this.#tokens.invalidate(token);
|
|
358
|
+
const fresh = await this.#tokens.getAccessToken();
|
|
359
|
+
return this.#send(opts, fresh);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async #send(opts, token) {
|
|
363
|
+
const retryable = opts.method === "GET" || opts.token === "obtain";
|
|
364
|
+
const maxAttempts = retryable ? this.#config.maxRetries + 1 : 1;
|
|
365
|
+
const url = `${this.#config.baseUrl}${opts.path}${buildQuery(opts.query)}`;
|
|
366
|
+
const headers = {
|
|
367
|
+
Accept: "application/json",
|
|
368
|
+
"Content-Type": "application/json",
|
|
369
|
+
accountId: this.#config.accountId,
|
|
370
|
+
"User-Agent": this.#config.userAgent
|
|
371
|
+
};
|
|
372
|
+
if (token !== void 0) headers.Authorization = `Bearer ${token}`;
|
|
373
|
+
const body = opts.body === void 0 ? void 0 : JSON.stringify(opts.body);
|
|
374
|
+
for (let attempt = 0; ; attempt++) {
|
|
375
|
+
const isLast = attempt + 1 >= maxAttempts;
|
|
376
|
+
let res;
|
|
377
|
+
try {
|
|
378
|
+
res = await this.#fetchOnce(url, opts.method, headers, body);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
const wrapped = err instanceof TimeoutError ? new TimeoutError(
|
|
381
|
+
`Nomba request timed out after ${this.#config.timeout} ms [${opts.method} ${opts.path}]`,
|
|
382
|
+
{ method: opts.method, path: opts.path }
|
|
383
|
+
) : new ConnectionError(
|
|
384
|
+
`Could not connect to Nomba [${opts.method} ${opts.path}]`,
|
|
385
|
+
{ method: opts.method, path: opts.path, cause: err }
|
|
386
|
+
);
|
|
387
|
+
if (isLast) throw wrapped;
|
|
388
|
+
await timing.sleep(backoffMs(attempt));
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!isLast && (res.status === 429 || res.status >= 500)) {
|
|
392
|
+
const retryAfter = res.status === 429 ? parseRetryAfter(res.headers.get("retry-after")) : void 0;
|
|
393
|
+
await timing.sleep(
|
|
394
|
+
retryAfter !== void 0 ? Math.min(retryAfter * 1e3, MAX_RETRY_AFTER_MS) : backoffMs(attempt)
|
|
395
|
+
);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
return this.#unwrap(res, opts);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async #fetchOnce(url, method, headers, body) {
|
|
402
|
+
const controller = new AbortController();
|
|
403
|
+
let timedOut = false;
|
|
404
|
+
const timer = setTimeout(() => {
|
|
405
|
+
timedOut = true;
|
|
406
|
+
controller.abort();
|
|
407
|
+
}, this.#config.timeout);
|
|
408
|
+
try {
|
|
409
|
+
const response = await this.#config.fetch(url, {
|
|
410
|
+
method,
|
|
411
|
+
headers,
|
|
412
|
+
body,
|
|
413
|
+
signal: controller.signal,
|
|
414
|
+
// Never follow redirects: the Authorization header must only go to Nomba.
|
|
415
|
+
redirect: "error"
|
|
416
|
+
});
|
|
417
|
+
const text = await response.text();
|
|
418
|
+
return {
|
|
419
|
+
status: response.status,
|
|
420
|
+
headers: response.headers,
|
|
421
|
+
body: parseJson(text)
|
|
422
|
+
};
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (timedOut) throw new TimeoutError("timeout");
|
|
425
|
+
throw err;
|
|
426
|
+
} finally {
|
|
427
|
+
clearTimeout(timer);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
#unwrap(res, opts) {
|
|
431
|
+
const envelope = isRecord(res.body) ? res.body : void 0;
|
|
432
|
+
const rawCode = envelope?.code;
|
|
433
|
+
const code = rawCode === void 0 || rawCode === null ? void 0 : String(rawCode);
|
|
434
|
+
const ok = res.status >= 200 && res.status < 300;
|
|
435
|
+
const notJson = typeof res.body === "string";
|
|
436
|
+
if (ok && !notJson && (code === void 0 || SUCCESS_CODES2.has(code))) {
|
|
437
|
+
if (envelope && "data" in envelope) return envelope.data;
|
|
438
|
+
return envelope ? null : res.body ?? null;
|
|
439
|
+
}
|
|
440
|
+
const descriptionField = notJson ? "response was not valid JSON" : envelope?.description ?? envelope?.message;
|
|
441
|
+
const init = {
|
|
442
|
+
httpStatus: res.status,
|
|
443
|
+
code,
|
|
444
|
+
description: typeof descriptionField === "string" && descriptionField !== "" ? descriptionField : void 0,
|
|
445
|
+
// Token endpoints: never keep a response body that might contain tokens.
|
|
446
|
+
body: opts.token ? redactTokenBody(res.body) : res.body,
|
|
447
|
+
method: opts.method,
|
|
448
|
+
path: opts.path
|
|
449
|
+
};
|
|
450
|
+
throw errorFor(init, res.headers, opts);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
function errorFor(init, headers, opts) {
|
|
454
|
+
const status = init.httpStatus ?? 0;
|
|
455
|
+
if (status === 429) {
|
|
456
|
+
return new RateLimitError({
|
|
457
|
+
...init,
|
|
458
|
+
retryAfter: parseRetryAfter(headers.get("retry-after")),
|
|
459
|
+
rateLimit: {
|
|
460
|
+
limit: headers.get("x-rate-limit-limit") ?? void 0,
|
|
461
|
+
remaining: headers.get("x-rate-limit-remaining") ?? void 0,
|
|
462
|
+
window: headers.get("x-rate-limit-window") ?? void 0
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (status >= 500) return new ServerError(init);
|
|
467
|
+
if (opts.token === "obtain") return new AuthenticationError(init);
|
|
468
|
+
if (status === 401) return new AuthenticationError(init);
|
|
469
|
+
if (status === 403) return new PermissionDeniedError(init);
|
|
470
|
+
if (status === 404) return new NotFoundError(init);
|
|
471
|
+
if (status === 400 || status === 422) return new ValidationError(init);
|
|
472
|
+
return new ApiError(init);
|
|
473
|
+
}
|
|
474
|
+
function parseJson(text) {
|
|
475
|
+
if (text === "") return void 0;
|
|
476
|
+
try {
|
|
477
|
+
return JSON.parse(text);
|
|
478
|
+
} catch {
|
|
479
|
+
return text;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function withReference(data, { field: field2, value }) {
|
|
483
|
+
if (data === null || data === void 0) return { [field2]: value };
|
|
484
|
+
if (isRecord(data) && (data[field2] === void 0 || data[field2] === null || data[field2] === "")) {
|
|
485
|
+
return { ...data, [field2]: value };
|
|
486
|
+
}
|
|
487
|
+
return data;
|
|
488
|
+
}
|
|
489
|
+
function redactTokenBody(body) {
|
|
490
|
+
if (!isRecord(body)) return body;
|
|
491
|
+
const { data: _data, ...rest } = body;
|
|
492
|
+
return rest;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/internal/pagination.ts
|
|
496
|
+
async function* autoPage(fetchPage, startCursor) {
|
|
497
|
+
let cursor = startCursor;
|
|
498
|
+
const seen = /* @__PURE__ */ new Set();
|
|
499
|
+
for (; ; ) {
|
|
500
|
+
const page = await fetchPage(cursor);
|
|
501
|
+
const results = page?.results ?? [];
|
|
502
|
+
for (const item of results) yield item;
|
|
503
|
+
const next = page?.cursor;
|
|
504
|
+
if (!next || results.length === 0 || seen.has(next)) return;
|
|
505
|
+
seen.add(next);
|
|
506
|
+
cursor = next;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/resources/base.ts
|
|
511
|
+
var Resource = class {
|
|
512
|
+
ctx;
|
|
513
|
+
constructor(ctx) {
|
|
514
|
+
this.ctx = ctx;
|
|
515
|
+
}
|
|
516
|
+
request(opts) {
|
|
517
|
+
return this.ctx.http.request(opts);
|
|
518
|
+
}
|
|
519
|
+
/** `path` + `/{subAccountId}` (when given) + `suffix`. */
|
|
520
|
+
withSubAccount(path, subAccountId, suffix = "") {
|
|
521
|
+
const sub = subAccountId === void 0 ? "" : `/${encodePathSegment(subAccountId, "subAccountId")}`;
|
|
522
|
+
return `${path}${sub}${suffix}`;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Money-moving POST with a `merchantTxRef`: generate one if missing. The request
|
|
526
|
+
* layer returns it on the result and attaches it to any error.
|
|
527
|
+
*/
|
|
528
|
+
async withMerchantTxRef(path, params, options = {}) {
|
|
529
|
+
const body = { ...params };
|
|
530
|
+
if (typeof body.merchantTxRef !== "string" || body.merchantTxRef === "") {
|
|
531
|
+
body.merchantTxRef = await randomUUID();
|
|
532
|
+
}
|
|
533
|
+
return this.request({
|
|
534
|
+
method: "POST",
|
|
535
|
+
path: this.withSubAccount(path, options.subAccountId),
|
|
536
|
+
body,
|
|
537
|
+
reference: { field: "merchantTxRef", value: body.merchantTxRef }
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
var seg = encodePathSegment;
|
|
542
|
+
|
|
543
|
+
// src/resources/accounts.ts
|
|
544
|
+
var Accounts = class extends Resource {
|
|
545
|
+
/** Parent account details. `GET /v1/accounts/parent` */
|
|
546
|
+
async getParent() {
|
|
547
|
+
return this.request({ method: "GET", path: "/v1/accounts/parent" });
|
|
548
|
+
}
|
|
549
|
+
/** Balance of the parent account, or of a sub account. */
|
|
550
|
+
async getBalance(params = {}) {
|
|
551
|
+
return this.request({
|
|
552
|
+
method: "GET",
|
|
553
|
+
path: this.withSubAccount("/v1/accounts", params.subAccountId, "/balance")
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/** Sub account details by `accountId` or `accountRef` (exactly one). */
|
|
557
|
+
async getSubAccount(params) {
|
|
558
|
+
requireExactlyOne(
|
|
559
|
+
params,
|
|
560
|
+
["accountId", "accountRef"],
|
|
561
|
+
"accounts.getSubAccount"
|
|
562
|
+
);
|
|
563
|
+
return this.request({
|
|
564
|
+
method: "GET",
|
|
565
|
+
path: "/v1/accounts/sub-account-details",
|
|
566
|
+
query: { accountId: params.accountId, accountRef: params.accountRef }
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/** One page of terminals on the parent account or a sub account. */
|
|
570
|
+
async listTerminals(params = {}) {
|
|
571
|
+
const { subAccountId, ...query } = params;
|
|
572
|
+
validateLimit(query.limit);
|
|
573
|
+
return this.request({
|
|
574
|
+
method: "GET",
|
|
575
|
+
path: this.withSubAccount("/v1/accounts", subAccountId, "/terminals"),
|
|
576
|
+
query: { ...query }
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/** Every terminal, across pages: `for await (const t of nomba.accounts.listAllTerminals())`. */
|
|
580
|
+
async *listAllTerminals(params = {}) {
|
|
581
|
+
validateLimit(params.limit);
|
|
582
|
+
yield* autoPage((cursor) => this.listTerminals({ ...params, cursor }));
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// src/resources/auth.ts
|
|
587
|
+
var AuthResource = class extends Resource {
|
|
588
|
+
#tokens;
|
|
589
|
+
#clientId;
|
|
590
|
+
constructor(ctx, tokens, clientId) {
|
|
591
|
+
super(ctx);
|
|
592
|
+
this.#tokens = tokens;
|
|
593
|
+
this.#clientId = clientId;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Revoke the cached access token (`POST /v1/auth/token/revoke`) and clear the
|
|
597
|
+
* token store. Does nothing and returns `null` when no token is cached.
|
|
598
|
+
*/
|
|
599
|
+
async revoke() {
|
|
600
|
+
const token = await this.#tokens.current();
|
|
601
|
+
if (!token) return null;
|
|
602
|
+
try {
|
|
603
|
+
return await this.request({
|
|
604
|
+
method: "POST",
|
|
605
|
+
path: "/v1/auth/token/revoke",
|
|
606
|
+
body: { clientId: this.#clientId, access_token: token.accessToken },
|
|
607
|
+
token: "revoke"
|
|
608
|
+
});
|
|
609
|
+
} finally {
|
|
610
|
+
await this.#tokens.clear();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/resources/bills.ts
|
|
616
|
+
var Bills = class extends Resource {
|
|
617
|
+
async buyAirtime(params, options = {}) {
|
|
618
|
+
requireFields(
|
|
619
|
+
params,
|
|
620
|
+
["amount", "phoneNumber", "network"],
|
|
621
|
+
"bills.buyAirtime"
|
|
622
|
+
);
|
|
623
|
+
return this.withMerchantTxRef("/v2/bill/topup", params, options);
|
|
624
|
+
}
|
|
625
|
+
async buyData(params, options = {}) {
|
|
626
|
+
requireFields(
|
|
627
|
+
params,
|
|
628
|
+
["productId", "phoneNumber", "network"],
|
|
629
|
+
"bills.buyData"
|
|
630
|
+
);
|
|
631
|
+
return this.withMerchantTxRef("/v2/bill/data", params, options);
|
|
632
|
+
}
|
|
633
|
+
/** Data plans for a telco (e.g. `mtn`). The telco is lowercased. */
|
|
634
|
+
async listDataPlans(telco) {
|
|
635
|
+
const t = typeof telco === "string" ? telco.toLowerCase() : telco;
|
|
636
|
+
return this.request({ method: "GET", path: `/v1/bill/data-plan/${seg(t, "telco")}` });
|
|
637
|
+
}
|
|
638
|
+
async buyElectricity(params, options = {}) {
|
|
639
|
+
return this.withMerchantTxRef("/v2/bill/electricity", params, options);
|
|
640
|
+
}
|
|
641
|
+
/** Electricity distribution companies (discos). */
|
|
642
|
+
async listElectricityProviders() {
|
|
643
|
+
return this.request({ method: "GET", path: "/v1/bill/electricity/discos" });
|
|
644
|
+
}
|
|
645
|
+
async lookupElectricityCustomer(params) {
|
|
646
|
+
requireFields(
|
|
647
|
+
params,
|
|
648
|
+
["disco", "customerId"],
|
|
649
|
+
"bills.lookupElectricityCustomer"
|
|
650
|
+
);
|
|
651
|
+
return this.request({
|
|
652
|
+
method: "GET",
|
|
653
|
+
path: "/v2/bill/electricity/lookup",
|
|
654
|
+
query: { disco: params.disco, customerId: params.customerId }
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async payCableTv(params, options = {}) {
|
|
658
|
+
return this.withMerchantTxRef("/v2/bill/cabletv", params, options);
|
|
659
|
+
}
|
|
660
|
+
async listCableTvPlans(cableTvType) {
|
|
661
|
+
requireFields({ cableTvType }, ["cableTvType"], "bills.listCableTvPlans");
|
|
662
|
+
return this.request({
|
|
663
|
+
method: "GET",
|
|
664
|
+
path: "/v1/bill/cableTvProduct",
|
|
665
|
+
query: { cableTvType }
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
/** Returns the customer's name. */
|
|
669
|
+
async lookupCableTvCustomer(params) {
|
|
670
|
+
requireFields(
|
|
671
|
+
params,
|
|
672
|
+
["customerId", "cableTvType"],
|
|
673
|
+
"bills.lookupCableTvCustomer"
|
|
674
|
+
);
|
|
675
|
+
return this.request({
|
|
676
|
+
method: "GET",
|
|
677
|
+
path: "/v1/bill/cabletv/lookup",
|
|
678
|
+
query: { customerId: params.customerId, cableTvType: params.cableTvType }
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async fundBetting(params, options = {}) {
|
|
682
|
+
return this.withMerchantTxRef("/v2/bill/betting", params, options);
|
|
683
|
+
}
|
|
684
|
+
async listBettingProviders() {
|
|
685
|
+
return this.request({ method: "GET", path: "/v1/bill/betting/providers" });
|
|
686
|
+
}
|
|
687
|
+
/** Returns the customer's name. */
|
|
688
|
+
async lookupBettingCustomer(params) {
|
|
689
|
+
requireFields(
|
|
690
|
+
params,
|
|
691
|
+
["providerId", "customerId"],
|
|
692
|
+
"bills.lookupBettingCustomer"
|
|
693
|
+
);
|
|
694
|
+
return this.request({
|
|
695
|
+
method: "GET",
|
|
696
|
+
path: "/v1/bill/betting/lookup",
|
|
697
|
+
query: { providerId: params.providerId, customerId: params.customerId }
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// src/resources/checkout.ts
|
|
703
|
+
var Checkout = class extends Resource {
|
|
704
|
+
/** Create a hosted checkout order and get its `checkoutLink`. */
|
|
705
|
+
async createOrder(params) {
|
|
706
|
+
if (!isRecord(params?.order)) {
|
|
707
|
+
throw ValidationError.client('checkout.createOrder: "order" is required');
|
|
708
|
+
}
|
|
709
|
+
return this.request({ method: "POST", path: "/v1/checkout/order", body: params });
|
|
710
|
+
}
|
|
711
|
+
async getOrder(orderReference) {
|
|
712
|
+
return this.request({
|
|
713
|
+
method: "GET",
|
|
714
|
+
path: `/v1/checkout/order/${seg(orderReference, "orderReference")}`
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
async cancelOrder(params) {
|
|
718
|
+
requireFields(
|
|
719
|
+
params,
|
|
720
|
+
["orderReference"],
|
|
721
|
+
"checkout.cancelOrder"
|
|
722
|
+
);
|
|
723
|
+
return this.request({
|
|
724
|
+
method: "POST",
|
|
725
|
+
path: "/v1/checkout/order/cancel",
|
|
726
|
+
body: params
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
/** A checkout transaction by order ID or order reference. */
|
|
730
|
+
async getTransaction(params) {
|
|
731
|
+
requireFields(
|
|
732
|
+
params,
|
|
733
|
+
["idType", "id"],
|
|
734
|
+
"checkout.getTransaction"
|
|
735
|
+
);
|
|
736
|
+
return this.request({
|
|
737
|
+
method: "GET",
|
|
738
|
+
path: "/v1/checkout/transaction",
|
|
739
|
+
query: { idType: params.idType, id: params.id }
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
/** Refund a checkout transaction. Never retried automatically. */
|
|
743
|
+
async refund(params) {
|
|
744
|
+
requireFields(params, ["transactionId"], "checkout.refund");
|
|
745
|
+
return this.request({ method: "POST", path: "/v1/checkout/refund", body: params });
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Charge a saved (tokenized) card. This endpoint has no `merchantTxRef`; its
|
|
749
|
+
* reference is `order.orderReference`. If you omit it the SDK generates one
|
|
750
|
+
* (UUID v4) and returns it as `orderReference`. Never retried automatically.
|
|
751
|
+
*/
|
|
752
|
+
async chargeSavedCard(params) {
|
|
753
|
+
requireFields(params, ["tokenKey"], "checkout.chargeSavedCard");
|
|
754
|
+
const body = { ...params };
|
|
755
|
+
let reference;
|
|
756
|
+
if (isRecord(params.order)) {
|
|
757
|
+
const order = { ...params.order };
|
|
758
|
+
if (typeof order.orderReference !== "string" || order.orderReference === "") {
|
|
759
|
+
order.orderReference = await randomUUID();
|
|
760
|
+
}
|
|
761
|
+
reference = { field: "orderReference", value: order.orderReference };
|
|
762
|
+
body.order = order;
|
|
763
|
+
}
|
|
764
|
+
return this.request({
|
|
765
|
+
method: "POST",
|
|
766
|
+
path: "/v1/checkout/tokenized-card-payment",
|
|
767
|
+
body,
|
|
768
|
+
reference
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
/** Saved cards, filtered by customer email and/or date range (page-numbered). */
|
|
772
|
+
async listSavedCards(params = {}) {
|
|
773
|
+
return this.request({
|
|
774
|
+
method: "GET",
|
|
775
|
+
path: "/v1/checkout/tokenized-card-data",
|
|
776
|
+
query: { ...params }
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
async updateSavedCardEmail(params) {
|
|
780
|
+
requireFields(
|
|
781
|
+
params,
|
|
782
|
+
["tokenKey", "currentEmailAddress", "newEmailAddress"],
|
|
783
|
+
"checkout.updateSavedCardEmail"
|
|
784
|
+
);
|
|
785
|
+
return this.request({
|
|
786
|
+
method: "POST",
|
|
787
|
+
path: "/v1/checkout/tokenized-card-data",
|
|
788
|
+
body: params
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
/** Delete a saved card. Sends a JSON body with DELETE. */
|
|
792
|
+
async deleteSavedCard(params) {
|
|
793
|
+
requireFields(params, ["tokenKey"], "checkout.deleteSavedCard");
|
|
794
|
+
return this.request({
|
|
795
|
+
method: "DELETE",
|
|
796
|
+
path: "/v1/checkout/tokenized-card-data",
|
|
797
|
+
body: params
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/** The bank account a customer can transfer to for this order. */
|
|
801
|
+
async getTransferAccount(orderReference) {
|
|
802
|
+
return this.request({
|
|
803
|
+
method: "GET",
|
|
804
|
+
path: `/v1/checkout/get-checkout-kta/${seg(orderReference, "orderReference")}`
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
async submitCardDetails(params) {
|
|
808
|
+
return this.request({
|
|
809
|
+
method: "POST",
|
|
810
|
+
path: "/v1/checkout/checkout-card-detail",
|
|
811
|
+
body: params
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async submitCardOtp(params) {
|
|
815
|
+
requireFields(
|
|
816
|
+
params,
|
|
817
|
+
["otp", "orderReference", "transactionId"],
|
|
818
|
+
"checkout.submitCardOtp"
|
|
819
|
+
);
|
|
820
|
+
return this.request({
|
|
821
|
+
method: "POST",
|
|
822
|
+
path: "/v1/checkout/checkout-card-otp",
|
|
823
|
+
body: params
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
async resendOtp(params) {
|
|
827
|
+
requireFields(params, ["orderReference"], "checkout.resendOtp");
|
|
828
|
+
return this.request({
|
|
829
|
+
method: "POST",
|
|
830
|
+
path: "/v1/checkout/resend-otp",
|
|
831
|
+
body: params
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async confirmTransactionReceipt(params) {
|
|
835
|
+
requireFields(
|
|
836
|
+
params,
|
|
837
|
+
["orderReference"],
|
|
838
|
+
"checkout.confirmTransactionReceipt"
|
|
839
|
+
);
|
|
840
|
+
return this.request({
|
|
841
|
+
method: "POST",
|
|
842
|
+
path: "/v1/checkout/confirm-transaction-receipt",
|
|
843
|
+
body: params
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
async cancelTransaction(params) {
|
|
847
|
+
requireFields(
|
|
848
|
+
params,
|
|
849
|
+
["transactionId", "forceCancel"],
|
|
850
|
+
"checkout.cancelTransaction"
|
|
851
|
+
);
|
|
852
|
+
return this.request({
|
|
853
|
+
method: "POST",
|
|
854
|
+
path: "/v1/checkout/transaction/cancel",
|
|
855
|
+
body: params
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/** Send an OTP to a customer who wants to save their card. */
|
|
859
|
+
async requestSaveCardOtp(params) {
|
|
860
|
+
requireFields(
|
|
861
|
+
params,
|
|
862
|
+
["orderReference", "phoneNumber"],
|
|
863
|
+
"checkout.requestSaveCardOtp"
|
|
864
|
+
);
|
|
865
|
+
return this.request({
|
|
866
|
+
method: "POST",
|
|
867
|
+
path: "/v1/checkout/user-card/auth",
|
|
868
|
+
body: params
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
/** Send an OTP to a customer who already has saved cards. */
|
|
872
|
+
async requestSavedCardsOtp(params) {
|
|
873
|
+
requireFields(
|
|
874
|
+
params,
|
|
875
|
+
["orderReference"],
|
|
876
|
+
"checkout.requestSavedCardsOtp"
|
|
877
|
+
);
|
|
878
|
+
return this.request({
|
|
879
|
+
method: "POST",
|
|
880
|
+
path: "/v1/checkout/user-card/saved-card/auth",
|
|
881
|
+
body: params
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
async submitUserOtp(params) {
|
|
885
|
+
requireFields(
|
|
886
|
+
params,
|
|
887
|
+
["orderReference", "phoneNumber", "otp"],
|
|
888
|
+
"checkout.submitUserOtp"
|
|
889
|
+
);
|
|
890
|
+
return this.request({ method: "POST", path: "/v1/checkout/user-card", body: params });
|
|
891
|
+
}
|
|
892
|
+
/** The customer's saved cards for this order, after OTP verification. */
|
|
893
|
+
async getSavedCards(orderReference, params) {
|
|
894
|
+
requireFields(params, ["otp"], "checkout.getSavedCards");
|
|
895
|
+
return this.request({
|
|
896
|
+
method: "GET",
|
|
897
|
+
path: `/v1/checkout/user-card/${seg(orderReference, "orderReference")}`,
|
|
898
|
+
query: { otp: params.otp }
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// src/resources/directDebits.ts
|
|
904
|
+
var DirectDebits = class extends Resource {
|
|
905
|
+
async createMandate(params) {
|
|
906
|
+
return this.request({ method: "POST", path: "/v1/direct-debits", body: params });
|
|
907
|
+
}
|
|
908
|
+
/** Debit a customer against an active mandate. Never retried automatically. */
|
|
909
|
+
async debitMandate(params) {
|
|
910
|
+
requireFields(
|
|
911
|
+
params,
|
|
912
|
+
["mandateId", "amount"],
|
|
913
|
+
"directDebits.debitMandate"
|
|
914
|
+
);
|
|
915
|
+
return this.request({
|
|
916
|
+
method: "POST",
|
|
917
|
+
path: "/v1/direct-debits/debit-mandate",
|
|
918
|
+
body: params
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
async getMandateStatus(mandateId) {
|
|
922
|
+
requireFields({ mandateId }, ["mandateId"], "directDebits.getMandateStatus");
|
|
923
|
+
return this.request({
|
|
924
|
+
method: "GET",
|
|
925
|
+
path: "/v1/direct-debits/status",
|
|
926
|
+
query: { mandateId }
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
async listMandates(params = {}) {
|
|
930
|
+
return this.request({
|
|
931
|
+
method: "GET",
|
|
932
|
+
path: "/v1/direct-debits/mandates",
|
|
933
|
+
query: { ...params }
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
async updateMandateStatus(params) {
|
|
937
|
+
requireFields(
|
|
938
|
+
params,
|
|
939
|
+
["mandateId", "status"],
|
|
940
|
+
"directDebits.updateMandateStatus"
|
|
941
|
+
);
|
|
942
|
+
return this.request({
|
|
943
|
+
method: "PUT",
|
|
944
|
+
path: "/v1/direct-debits/update-status",
|
|
945
|
+
body: params
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// src/resources/globalCollection.ts
|
|
951
|
+
var GlobalCollection = class extends Resource {
|
|
952
|
+
/** Mobile-money providers for DRC inflows. */
|
|
953
|
+
async listDrcProviders() {
|
|
954
|
+
return this.request({
|
|
955
|
+
method: "GET",
|
|
956
|
+
path: "/v1/global-collection/drc/inflow/providers"
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
/** Start a mobile-money inflow. Never retried automatically. */
|
|
960
|
+
async initiateInflow(params) {
|
|
961
|
+
return this.request({
|
|
962
|
+
method: "POST",
|
|
963
|
+
path: "/v1/global-collection/inflow/initiate",
|
|
964
|
+
body: params
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
async getTransaction(transactionId) {
|
|
968
|
+
return this.request({
|
|
969
|
+
method: "GET",
|
|
970
|
+
path: `/v1/global-collection/transactions/${seg(transactionId, "transactionId")}`
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// src/resources/globalPayout.ts
|
|
976
|
+
var GlobalPayout = class extends Resource {
|
|
977
|
+
#accountsPath() {
|
|
978
|
+
return this.ctx.environment === "sandbox" ? "/v1/sandbox/global-payout/accounts" : "/v1/global-payout/accounts";
|
|
979
|
+
}
|
|
980
|
+
async listAccounts() {
|
|
981
|
+
return this.request({ method: "GET", path: this.#accountsPath() });
|
|
982
|
+
}
|
|
983
|
+
async getAccount(accountId) {
|
|
984
|
+
return this.request({
|
|
985
|
+
method: "GET",
|
|
986
|
+
path: `${this.#accountsPath()}/${seg(accountId, "accountId")}`
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
async listBankProviders(params = {}) {
|
|
990
|
+
return this.request({
|
|
991
|
+
method: "GET",
|
|
992
|
+
path: "/v1/global-payout/bank/providers",
|
|
993
|
+
query: { ...params }
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
async getExchangeRates(params) {
|
|
997
|
+
requireFields(
|
|
998
|
+
params,
|
|
999
|
+
["from", "to"],
|
|
1000
|
+
"globalPayout.getExchangeRates"
|
|
1001
|
+
);
|
|
1002
|
+
return this.request({
|
|
1003
|
+
method: "GET",
|
|
1004
|
+
path: "/v1/global-payout/exchange-rates",
|
|
1005
|
+
query: { ...params }
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
async listPaymentMethods(params = {}) {
|
|
1009
|
+
return this.request({
|
|
1010
|
+
method: "GET",
|
|
1011
|
+
path: "/v1/global-payout/payment-methods",
|
|
1012
|
+
query: { ...params }
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
async getTransaction(transactionId) {
|
|
1016
|
+
return this.request({
|
|
1017
|
+
method: "GET",
|
|
1018
|
+
path: `/v1/global-payout/transactions/${seg(transactionId, "transactionId")}`
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
/** Never retried automatically; pass an `idempotencyKey` and reuse it when retrying. */
|
|
1022
|
+
async authorizeExchange(params) {
|
|
1023
|
+
return this.request({
|
|
1024
|
+
method: "POST",
|
|
1025
|
+
path: "/v1/global-payout/exchange/authorize",
|
|
1026
|
+
body: params
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
async convertMoney(params) {
|
|
1030
|
+
return this.request({
|
|
1031
|
+
method: "POST",
|
|
1032
|
+
path: "/v1/global-payout/money/convert",
|
|
1033
|
+
body: params
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
/** Never retried automatically; pass an `idempotencyKey` and reuse it when retrying. */
|
|
1037
|
+
async authorizeTransfer(params) {
|
|
1038
|
+
return this.request({
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
path: "/v1/global-payout/transfer/authorize",
|
|
1041
|
+
body: params
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// src/resources/terminals.ts
|
|
1047
|
+
var Terminals = class extends Resource {
|
|
1048
|
+
/** Assign a terminal to the parent account or a sub account. */
|
|
1049
|
+
async assign(params, options = {}) {
|
|
1050
|
+
requireFields(
|
|
1051
|
+
params,
|
|
1052
|
+
["serialNumber"],
|
|
1053
|
+
"terminals.assign"
|
|
1054
|
+
);
|
|
1055
|
+
return this.request({
|
|
1056
|
+
method: "POST",
|
|
1057
|
+
path: this.withSubAccount("/v1/terminals/assign", options.subAccountId),
|
|
1058
|
+
body: params
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
/** Un-assign a terminal from the parent account or a sub account. */
|
|
1062
|
+
async unassign(params, options = {}) {
|
|
1063
|
+
requireFields(
|
|
1064
|
+
params,
|
|
1065
|
+
["serialNumber"],
|
|
1066
|
+
"terminals.unassign"
|
|
1067
|
+
);
|
|
1068
|
+
return this.request({
|
|
1069
|
+
method: "POST",
|
|
1070
|
+
path: this.withSubAccount("/v1/terminals/unassign", options.subAccountId),
|
|
1071
|
+
body: params
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
/** Push a payment request to a terminal. */
|
|
1075
|
+
async sendPaymentRequest(terminalId, params) {
|
|
1076
|
+
requireFields(
|
|
1077
|
+
params,
|
|
1078
|
+
["merchantTxRef", "amount", "currency"],
|
|
1079
|
+
"terminals.sendPaymentRequest"
|
|
1080
|
+
);
|
|
1081
|
+
return this.request({
|
|
1082
|
+
method: "POST",
|
|
1083
|
+
path: `/v1/terminals/payment-request/${seg(terminalId, "terminalId")}`,
|
|
1084
|
+
body: params
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
// src/resources/transactions.ts
|
|
1090
|
+
var REFERENCE_KEYS = [
|
|
1091
|
+
"transactionRef",
|
|
1092
|
+
"merchantTxRef",
|
|
1093
|
+
"orderReference",
|
|
1094
|
+
"orderId"
|
|
1095
|
+
];
|
|
1096
|
+
var Transactions = class extends Resource {
|
|
1097
|
+
#accountsPath(subAccountId, suffix = "") {
|
|
1098
|
+
return this.withSubAccount("/v1/transactions/accounts", subAccountId, suffix);
|
|
1099
|
+
}
|
|
1100
|
+
/** One page of transactions on the parent account or a sub account. */
|
|
1101
|
+
async list(params = {}) {
|
|
1102
|
+
const { subAccountId, ...query } = params;
|
|
1103
|
+
validateLimit(query.limit);
|
|
1104
|
+
return this.request({
|
|
1105
|
+
method: "GET",
|
|
1106
|
+
path: this.#accountsPath(subAccountId),
|
|
1107
|
+
query: { ...query }
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
/** Every transaction, across pages: `for await (const tx of nomba.transactions.listAll())`. */
|
|
1111
|
+
async *listAll(params = {}) {
|
|
1112
|
+
validateLimit(params.limit);
|
|
1113
|
+
yield* autoPage((cursor) => this.list({ ...params, cursor }));
|
|
1114
|
+
}
|
|
1115
|
+
/** One page of transactions matching `filters`. */
|
|
1116
|
+
async filter(filters = {}, params = {}) {
|
|
1117
|
+
const { subAccountId, ...query } = params;
|
|
1118
|
+
validateLimit(query.limit);
|
|
1119
|
+
return this.request({
|
|
1120
|
+
method: "POST",
|
|
1121
|
+
path: this.#accountsPath(subAccountId),
|
|
1122
|
+
query: { ...query },
|
|
1123
|
+
body: filters
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
/** Every transaction matching `filters`, across pages. */
|
|
1127
|
+
async *filterAll(filters = {}, params = {}) {
|
|
1128
|
+
validateLimit(params.limit);
|
|
1129
|
+
yield* autoPage((cursor) => this.filter(filters, { ...params, cursor }));
|
|
1130
|
+
}
|
|
1131
|
+
/** One transaction, by exactly one of `transactionRef`, `merchantTxRef`, `orderReference`, `orderId`. */
|
|
1132
|
+
async get(params) {
|
|
1133
|
+
requireExactlyOne(
|
|
1134
|
+
params,
|
|
1135
|
+
REFERENCE_KEYS,
|
|
1136
|
+
"transactions.get"
|
|
1137
|
+
);
|
|
1138
|
+
const { subAccountId, ...query } = params;
|
|
1139
|
+
return this.request({
|
|
1140
|
+
method: "GET",
|
|
1141
|
+
path: this.#accountsPath(subAccountId, "/single"),
|
|
1142
|
+
query: { ...query }
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
/** One page of credits/debits on the parent account. */
|
|
1146
|
+
async listBankTransactions(params = {}) {
|
|
1147
|
+
validateLimit(params.limit);
|
|
1148
|
+
return this.request({
|
|
1149
|
+
method: "GET",
|
|
1150
|
+
path: "/v1/transactions/bank",
|
|
1151
|
+
query: { ...params }
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
/** Every credit/debit on the parent account, across pages. */
|
|
1155
|
+
async *listAllBankTransactions(params = {}) {
|
|
1156
|
+
validateLimit(params.limit);
|
|
1157
|
+
yield* autoPage((cursor) => this.listBankTransactions({ ...params, cursor }));
|
|
1158
|
+
}
|
|
1159
|
+
/** Confirm a transaction's status by its NIP `sessionId`. */
|
|
1160
|
+
async requery(sessionId) {
|
|
1161
|
+
return this.request({
|
|
1162
|
+
method: "GET",
|
|
1163
|
+
path: `/v1/transactions/requery/${seg(sessionId, "sessionId")}`
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// src/resources/transfers.ts
|
|
1169
|
+
var Transfers = class extends Resource {
|
|
1170
|
+
/** Bank codes and names. */
|
|
1171
|
+
async listBanks() {
|
|
1172
|
+
return this.request({ method: "GET", path: "/v1/transfers/banks" });
|
|
1173
|
+
}
|
|
1174
|
+
/** Resolve the account name for an account number and bank code. */
|
|
1175
|
+
async lookupAccount(params) {
|
|
1176
|
+
requireFields(
|
|
1177
|
+
params,
|
|
1178
|
+
["accountNumber", "bankCode"],
|
|
1179
|
+
"transfers.lookupAccount"
|
|
1180
|
+
);
|
|
1181
|
+
return this.request({
|
|
1182
|
+
method: "POST",
|
|
1183
|
+
path: "/v1/transfers/bank/lookup",
|
|
1184
|
+
body: { accountNumber: params.accountNumber, bankCode: params.bankCode }
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Transfer to a bank account. Never retried automatically. Pass your own
|
|
1189
|
+
* `merchantTxRef` (or keep the generated one from the result) to requery.
|
|
1190
|
+
*/
|
|
1191
|
+
async toBank(params, options = {}) {
|
|
1192
|
+
requireFields(
|
|
1193
|
+
params,
|
|
1194
|
+
["amount", "accountNumber", "accountName", "bankCode"],
|
|
1195
|
+
"transfers.toBank"
|
|
1196
|
+
);
|
|
1197
|
+
return this.withMerchantTxRef("/v2/transfers/bank", params, options);
|
|
1198
|
+
}
|
|
1199
|
+
/** Transfer to another Nomba wallet. Never retried automatically. */
|
|
1200
|
+
async toWallet(params, options = {}) {
|
|
1201
|
+
requireFields(
|
|
1202
|
+
params,
|
|
1203
|
+
["amount", "receiverAccountId"],
|
|
1204
|
+
"transfers.toWallet"
|
|
1205
|
+
);
|
|
1206
|
+
return this.withMerchantTxRef("/v2/transfers/wallet", params, options);
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
// src/resources/virtualAccounts.ts
|
|
1211
|
+
var VirtualAccounts = class extends Resource {
|
|
1212
|
+
/** Create a virtual account on the parent account or a sub account. */
|
|
1213
|
+
async create(params, options = {}) {
|
|
1214
|
+
requireFields(
|
|
1215
|
+
params,
|
|
1216
|
+
["accountRef", "accountName"],
|
|
1217
|
+
"virtualAccounts.create"
|
|
1218
|
+
);
|
|
1219
|
+
return this.request({
|
|
1220
|
+
method: "POST",
|
|
1221
|
+
path: this.withSubAccount("/v1/accounts/virtual", options.subAccountId),
|
|
1222
|
+
body: params
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
/** Fetch a virtual account by its account reference or number. */
|
|
1226
|
+
async get(identifier) {
|
|
1227
|
+
return this.request({
|
|
1228
|
+
method: "GET",
|
|
1229
|
+
path: `/v1/accounts/virtual/${seg(identifier, "identifier")}`
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
async update(identifier, params) {
|
|
1233
|
+
return this.request({
|
|
1234
|
+
method: "PUT",
|
|
1235
|
+
path: `/v1/accounts/virtual/${seg(identifier, "identifier")}`,
|
|
1236
|
+
body: params
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
/** Expire (close) a virtual account. */
|
|
1240
|
+
async expire(identifier) {
|
|
1241
|
+
return this.request({
|
|
1242
|
+
method: "DELETE",
|
|
1243
|
+
path: `/v1/accounts/virtual/${seg(identifier, "identifier")}`
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
/** One page of virtual accounts matching `filters`. */
|
|
1247
|
+
async list(filters = {}, options = {}) {
|
|
1248
|
+
validateLimit(options.limit);
|
|
1249
|
+
return this.request({
|
|
1250
|
+
method: "POST",
|
|
1251
|
+
path: "/v1/accounts/virtual/list",
|
|
1252
|
+
query: { limit: options.limit, cursor: options.cursor },
|
|
1253
|
+
body: filters
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
/** Every virtual account matching `filters`, across pages. */
|
|
1257
|
+
async *listAll(filters = {}, options = {}) {
|
|
1258
|
+
validateLimit(options.limit);
|
|
1259
|
+
yield* autoPage((cursor) => this.list(filters, { limit: options.limit, cursor }));
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
// src/version.ts
|
|
1264
|
+
var VERSION = "0.1.0";
|
|
1265
|
+
function header(headers, name) {
|
|
1266
|
+
if (typeof headers.get === "function") {
|
|
1267
|
+
const v = headers.get(name);
|
|
1268
|
+
return v ?? void 0;
|
|
1269
|
+
}
|
|
1270
|
+
const record = headers;
|
|
1271
|
+
for (const key of Object.keys(record)) {
|
|
1272
|
+
if (key.toLowerCase() === name) {
|
|
1273
|
+
const v = record[key];
|
|
1274
|
+
return Array.isArray(v) ? v[0] : v;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return void 0;
|
|
1278
|
+
}
|
|
1279
|
+
function field(v) {
|
|
1280
|
+
if (v === void 0 || v === null) return "";
|
|
1281
|
+
return typeof v === "string" ? v : String(v);
|
|
1282
|
+
}
|
|
1283
|
+
var RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
1284
|
+
function signedResponseCode(event) {
|
|
1285
|
+
const responseCode = field(event.data.transaction.responseCode);
|
|
1286
|
+
return responseCode === "null" ? "" : responseCode;
|
|
1287
|
+
}
|
|
1288
|
+
function webhookSignedMessage(event, timestamp) {
|
|
1289
|
+
const { merchant, transaction } = event.data;
|
|
1290
|
+
return [
|
|
1291
|
+
event.event_type,
|
|
1292
|
+
event.requestId,
|
|
1293
|
+
field(merchant.userId),
|
|
1294
|
+
field(merchant.walletId),
|
|
1295
|
+
field(transaction.transactionId),
|
|
1296
|
+
field(transaction.type),
|
|
1297
|
+
field(transaction.time),
|
|
1298
|
+
signedResponseCode(event),
|
|
1299
|
+
timestamp
|
|
1300
|
+
].join(":");
|
|
1301
|
+
}
|
|
1302
|
+
function computeWebhookSignature(event, timestamp, signingKey) {
|
|
1303
|
+
return createHmac("sha256", signingKey).update(webhookSignedMessage(event, timestamp), "utf8").digest("base64");
|
|
1304
|
+
}
|
|
1305
|
+
function parseEvent(rawBody) {
|
|
1306
|
+
if (typeof rawBody !== "string" && !(rawBody instanceof Uint8Array) && !(rawBody instanceof ArrayBuffer)) {
|
|
1307
|
+
throw new WebhookVerificationError(
|
|
1308
|
+
"Webhook body must be the raw request body as a string or bytes, not a parsed object"
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
const text = typeof rawBody === "string" ? rawBody : new TextDecoder("utf-8").decode(
|
|
1312
|
+
rawBody instanceof ArrayBuffer ? new Uint8Array(rawBody) : rawBody
|
|
1313
|
+
);
|
|
1314
|
+
let parsed;
|
|
1315
|
+
try {
|
|
1316
|
+
parsed = JSON.parse(text);
|
|
1317
|
+
} catch {
|
|
1318
|
+
throw new WebhookVerificationError("Webhook body is not valid JSON");
|
|
1319
|
+
}
|
|
1320
|
+
if (!isRecord(parsed) || typeof parsed.event_type !== "string" || typeof parsed.requestId !== "string" || !isRecord(parsed.data) || !isRecord(parsed.data.merchant) || !isRecord(parsed.data.transaction)) {
|
|
1321
|
+
throw new WebhookVerificationError(
|
|
1322
|
+
"Webhook body is missing event_type, requestId, data.merchant or data.transaction"
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
return parsed;
|
|
1326
|
+
}
|
|
1327
|
+
function verifyWebhook(rawBody, headers, signingKey, options = {}) {
|
|
1328
|
+
if (typeof signingKey !== "string" || signingKey === "") {
|
|
1329
|
+
throw new WebhookVerificationError("A webhook signing key is required");
|
|
1330
|
+
}
|
|
1331
|
+
if (!headers || typeof headers !== "object") {
|
|
1332
|
+
throw new WebhookVerificationError("Webhook headers are required");
|
|
1333
|
+
}
|
|
1334
|
+
const signature = header(headers, "nomba-signature") ?? header(headers, "nomba-sig-value");
|
|
1335
|
+
if (!signature) throw new WebhookVerificationError("Missing nomba-signature header");
|
|
1336
|
+
const timestamp = header(headers, "nomba-timestamp");
|
|
1337
|
+
if (!timestamp) throw new WebhookVerificationError("Missing nomba-timestamp header");
|
|
1338
|
+
const algorithm = header(headers, "nomba-signature-algorithm");
|
|
1339
|
+
if (algorithm !== void 0 && algorithm !== "HmacSHA256") {
|
|
1340
|
+
throw new WebhookVerificationError(`Unsupported signature algorithm "${algorithm}"`);
|
|
1341
|
+
}
|
|
1342
|
+
if (!RFC3339.test(timestamp)) {
|
|
1343
|
+
throw new WebhookVerificationError(
|
|
1344
|
+
"Invalid nomba-timestamp header: expected RFC 3339"
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const event = parseEvent(rawBody);
|
|
1348
|
+
if (signedResponseCode(event).includes(":")) {
|
|
1349
|
+
throw new WebhookVerificationError("Invalid data.transaction.responseCode");
|
|
1350
|
+
}
|
|
1351
|
+
if (options.toleranceSeconds !== void 0) {
|
|
1352
|
+
const ts = Date.parse(timestamp);
|
|
1353
|
+
if (Number.isNaN(ts))
|
|
1354
|
+
throw new WebhookVerificationError("Invalid nomba-timestamp header");
|
|
1355
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
1356
|
+
if (Math.abs(now - ts) > options.toleranceSeconds * 1e3) {
|
|
1357
|
+
throw new WebhookVerificationError(
|
|
1358
|
+
"Webhook timestamp is outside the tolerance window"
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const expected = computeWebhookSignature(event, timestamp, signingKey);
|
|
1363
|
+
const a = Buffer.from(expected, "utf8");
|
|
1364
|
+
const b = Buffer.from(signature.trim(), "utf8");
|
|
1365
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
1366
|
+
throw new WebhookVerificationError("Webhook signature does not match");
|
|
1367
|
+
}
|
|
1368
|
+
return event;
|
|
1369
|
+
}
|
|
1370
|
+
var webhooks = {
|
|
1371
|
+
verify: verifyWebhook,
|
|
1372
|
+
computeSignature: computeWebhookSignature
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
// src/client.ts
|
|
1376
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1377
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
1378
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1379
|
+
function required(value, option, env) {
|
|
1380
|
+
if (value === void 0 || value === "") {
|
|
1381
|
+
throw new ConfigurationError(
|
|
1382
|
+
`Missing ${option}: pass the "${option}" option or set the ${env} environment variable`
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
return value;
|
|
1386
|
+
}
|
|
1387
|
+
function resolveEnvironment(value) {
|
|
1388
|
+
const env = value ?? "sandbox";
|
|
1389
|
+
if (env !== "sandbox" && env !== "production") {
|
|
1390
|
+
throw new ConfigurationError(
|
|
1391
|
+
`Invalid environment "${env}": use "sandbox" or "production"`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
return env;
|
|
1395
|
+
}
|
|
1396
|
+
function resolveBaseUrl(value, environment) {
|
|
1397
|
+
if (value === void 0) return BASE_URLS[environment];
|
|
1398
|
+
let url;
|
|
1399
|
+
try {
|
|
1400
|
+
url = new URL(value);
|
|
1401
|
+
} catch {
|
|
1402
|
+
throw new ConfigurationError(`Invalid baseUrl: not a URL`);
|
|
1403
|
+
}
|
|
1404
|
+
const local = LOCAL_HOSTS.has(url.hostname);
|
|
1405
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
1406
|
+
throw new ConfigurationError(
|
|
1407
|
+
"baseUrl must use https:// (http:// is allowed only for localhost)"
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
if (url.search || url.hash || url.username || url.password) {
|
|
1411
|
+
throw new ConfigurationError(
|
|
1412
|
+
"baseUrl must not contain credentials, a query or a fragment"
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
return url.toString().replace(/\/+$/, "");
|
|
1416
|
+
}
|
|
1417
|
+
var Nomba = class {
|
|
1418
|
+
/** Webhook verification without a client: `Nomba.webhooks.verify(rawBody, headers, key)`. */
|
|
1419
|
+
static webhooks = webhooks;
|
|
1420
|
+
static VERSION = VERSION;
|
|
1421
|
+
environment;
|
|
1422
|
+
baseUrl;
|
|
1423
|
+
accountId;
|
|
1424
|
+
auth;
|
|
1425
|
+
accounts;
|
|
1426
|
+
virtualAccounts;
|
|
1427
|
+
checkout;
|
|
1428
|
+
transfers;
|
|
1429
|
+
transactions;
|
|
1430
|
+
terminals;
|
|
1431
|
+
directDebits;
|
|
1432
|
+
bills;
|
|
1433
|
+
globalPayout;
|
|
1434
|
+
globalCollection;
|
|
1435
|
+
webhooks = webhooks;
|
|
1436
|
+
#clientId;
|
|
1437
|
+
constructor(options = {}) {
|
|
1438
|
+
const clientId = required(
|
|
1439
|
+
options.clientId ?? readEnv("NOMBA_CLIENT_ID"),
|
|
1440
|
+
"clientId",
|
|
1441
|
+
"NOMBA_CLIENT_ID"
|
|
1442
|
+
);
|
|
1443
|
+
const clientSecret = required(
|
|
1444
|
+
options.clientSecret ?? readEnv("NOMBA_CLIENT_SECRET"),
|
|
1445
|
+
"clientSecret",
|
|
1446
|
+
"NOMBA_CLIENT_SECRET"
|
|
1447
|
+
);
|
|
1448
|
+
this.accountId = required(
|
|
1449
|
+
options.accountId ?? readEnv("NOMBA_ACCOUNT_ID"),
|
|
1450
|
+
"accountId",
|
|
1451
|
+
"NOMBA_ACCOUNT_ID"
|
|
1452
|
+
);
|
|
1453
|
+
this.environment = resolveEnvironment(
|
|
1454
|
+
options.environment ?? readEnv("NOMBA_ENVIRONMENT")
|
|
1455
|
+
);
|
|
1456
|
+
this.baseUrl = resolveBaseUrl(options.baseUrl, this.environment);
|
|
1457
|
+
this.#clientId = clientId;
|
|
1458
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
1459
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) {
|
|
1460
|
+
throw new ConfigurationError("timeout must be a positive number of milliseconds");
|
|
1461
|
+
}
|
|
1462
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1463
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
1464
|
+
throw new ConfigurationError("maxRetries must be a non-negative integer");
|
|
1465
|
+
}
|
|
1466
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1467
|
+
if (typeof fetchImpl !== "function") {
|
|
1468
|
+
throw new ConfigurationError(
|
|
1469
|
+
"No fetch implementation: use Node 18+ or pass the fetch option"
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
const http = new HttpClient({
|
|
1473
|
+
baseUrl: this.baseUrl,
|
|
1474
|
+
accountId: this.accountId,
|
|
1475
|
+
timeout,
|
|
1476
|
+
maxRetries,
|
|
1477
|
+
fetch: fetchImpl,
|
|
1478
|
+
userAgent: `nomba-node/${VERSION} ${runtimeVersion()}`
|
|
1479
|
+
});
|
|
1480
|
+
const tokens = new TokenManager({
|
|
1481
|
+
store: options.tokenStore ?? new InMemoryTokenStore(),
|
|
1482
|
+
issue: () => http.request({
|
|
1483
|
+
method: "POST",
|
|
1484
|
+
path: "/v1/auth/token/issue",
|
|
1485
|
+
body: {
|
|
1486
|
+
grant_type: "client_credentials",
|
|
1487
|
+
client_id: clientId,
|
|
1488
|
+
client_secret: clientSecret
|
|
1489
|
+
},
|
|
1490
|
+
token: "obtain"
|
|
1491
|
+
}),
|
|
1492
|
+
refresh: (refreshToken) => http.request({
|
|
1493
|
+
method: "POST",
|
|
1494
|
+
path: "/v1/auth/token/refresh",
|
|
1495
|
+
body: { grant_type: "refresh_token", refresh_token: refreshToken },
|
|
1496
|
+
token: "obtain"
|
|
1497
|
+
})
|
|
1498
|
+
});
|
|
1499
|
+
http.setTokenManager(tokens);
|
|
1500
|
+
const ctx = { http, environment: this.environment };
|
|
1501
|
+
this.auth = new AuthResource(ctx, tokens, clientId);
|
|
1502
|
+
this.accounts = new Accounts(ctx);
|
|
1503
|
+
this.virtualAccounts = new VirtualAccounts(ctx);
|
|
1504
|
+
this.checkout = new Checkout(ctx);
|
|
1505
|
+
this.transfers = new Transfers(ctx);
|
|
1506
|
+
this.transactions = new Transactions(ctx);
|
|
1507
|
+
this.terminals = new Terminals(ctx);
|
|
1508
|
+
this.directDebits = new DirectDebits(ctx);
|
|
1509
|
+
this.bills = new Bills(ctx);
|
|
1510
|
+
this.globalPayout = new GlobalPayout(ctx);
|
|
1511
|
+
this.globalCollection = new GlobalCollection(ctx);
|
|
1512
|
+
}
|
|
1513
|
+
/** Safe summary: never includes the client secret or tokens. */
|
|
1514
|
+
toJSON() {
|
|
1515
|
+
return {
|
|
1516
|
+
clientId: this.#clientId,
|
|
1517
|
+
accountId: this.accountId,
|
|
1518
|
+
environment: this.environment,
|
|
1519
|
+
baseUrl: this.baseUrl
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
1523
|
+
return `Nomba ${JSON.stringify(this.toJSON())}`;
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
// src/index.ts
|
|
1528
|
+
var index_default = Nomba;
|
|
1529
|
+
|
|
1530
|
+
export { ApiError, AuthenticationError, BASE_URLS, ConfigurationError, ConnectionError, InMemoryTokenStore, Nomba, NombaError, NotFoundError, PermissionDeniedError, RateLimitError, SUCCESS_CODES, ServerError, TimeoutError, TransactionStatus, VERSION, ValidationError, WebhookEventType, WebhookVerificationError, computeWebhookSignature, index_default as default, verifyWebhook };
|
|
1531
|
+
//# sourceMappingURL=index.js.map
|
|
1532
|
+
//# sourceMappingURL=index.js.map
|