@uverifyng/node 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/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # UVerify for Node.js
2
+
3
+ The official Node.js library for [UVerify](https://uverify.com.ng): BVN, NIN, driver’s licence and voter’s card checks, liveness and face match, ID documents, AML screening and CAC, for Nigerian businesses.
4
+
5
+ ```bash
6
+ npm install @uverifyng/node
7
+ ```
8
+
9
+ Node 18 or later. No dependencies. ESM and CommonJS, with TypeScript types.
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { UVerify } from '@uverifyng/node';
15
+
16
+ const uverify = new UVerify({ apiKey: process.env.UVERIFY_API_KEY }); // uvk_test_… is the free sandbox
17
+
18
+ const v = await uverify.identity.nin({ id_number: '12345678901', first_name: 'Adaeze', last_name: 'Okafor' });
19
+ if (v.status === 'verified') {
20
+ console.log(v.data, v.field_matches); // the record, and which details matched
21
+ }
22
+ ```
23
+
24
+ In the sandbox the last two digits of the ID number choose the result: `00` not found, `99` registry error, `98` face mismatch, anything else verified.
25
+
26
+ ## A live customer, end to end
27
+
28
+ ```ts
29
+ // 1. A liveness session: send the person to session.url (any phone browser)
30
+ const session = await uverify.liveness.createSession({ redirect_url: 'https://yourapp.com/kyc/done' });
31
+
32
+ // 2. When they return, match the face that passed to their BVN photo
33
+ const v = await uverify.identity.bvnFaceMatch({
34
+ id_number: '22212345678', first_name: 'Adaeze', last_name: 'Okafor',
35
+ liveness_session_id: session.id,
36
+ aml_screening: true, // also screen the verified person against sanctions lists
37
+ });
38
+
39
+ if (v.status === 'verified' && v.face_match?.status === 'matched' && v.aml_screening?.status === 'clear') {
40
+ // approve
41
+ }
42
+ ```
43
+
44
+ ## Everything else
45
+
46
+ ```ts
47
+ await uverify.identity.bvn({ id_number, first_name, last_name }); // also driversLicense, votersCard, tin
48
+ await uverify.identity.ninFaceMatch({ id_number, selfie_image: buffer }); // a selfie instead of liveness (Buffer or base64)
49
+ await uverify.business.cac({ id_number: 'RC123456', aml_screening: true }); // company + each director screened
50
+
51
+ await uverify.documents.verify({ front_image: fs.readFileSync('nin.jpg'), document_type: 'nin_card', liveness_session_id });
52
+ await uverify.aml.screen({ name: 'Adaeze Okafor', date_of_birth: '1990-01-15' });
53
+ await uverify.aml.monitors.create({ name: 'Adaeze Okafor' }); // re-screened after every list update
54
+
55
+ const link = await uverify.kyc.createLink({ customer_name: 'Ada', require_document: true }); // no-code: send link.url
56
+
57
+ await uverify.verifications.list({ status: 'verified' });
58
+ await uverify.account.balance();
59
+ ```
60
+
61
+ ## Errors
62
+
63
+ Every failure is a `UVerifyError` with a stable `code`:
64
+
65
+ ```ts
66
+ import { UVerifyError } from '@uverifyng/node';
67
+
68
+ try {
69
+ await uverify.identity.nin({ id_number });
70
+ } catch (e) {
71
+ if (e instanceof UVerifyError && e.code === 'insufficient_balance') { /* top up */ }
72
+ console.log(e.code, e.message, e.requestId, e.details);
73
+ }
74
+ ```
75
+
76
+ A check that finds nothing is **not** an error: it returns `status: 'not_found'` (and you aren’t charged).
77
+
78
+ ## Retries
79
+
80
+ Timeouts, connection errors, 5xx responses and rate limits are retried twice with backoff (`maxRetries`, `timeoutMs`). Every check carries a `reference` (one is generated if you don’t pass your own), so a retry can never charge twice; if the first attempt did go through, the library returns that result.
81
+
82
+ ## Webhooks
83
+
84
+ ```ts
85
+ import express from 'express';
86
+ import { UVerify } from '@uverifyng/node';
87
+
88
+ app.post('/webhooks/uverify', express.raw({ type: 'application/json' }), (req, res) => {
89
+ let event;
90
+ try {
91
+ event = UVerify.webhooks.constructEvent(req.body, req.header('UVerify-Signature'), process.env.UVERIFY_WEBHOOK_SECRET!);
92
+ } catch {
93
+ return res.sendStatus(400);
94
+ }
95
+ if (event.type === 'verification.completed') { /* event.data is the verification */ }
96
+ res.sendStatus(200);
97
+ });
98
+ ```
99
+
100
+ Pass the **raw** body. Deliveries can repeat, so dedupe on `event.id`.
101
+
102
+ ## Tests
103
+
104
+ ```bash
105
+ npm test # offline
106
+ UVERIFY_TEST_KEY=uvk_test_… npx vitest run test/sandbox.test.ts # against the real sandbox
107
+ ```
108
+
109
+ MIT licence. API reference: https://uverify.com.ng/docs
package/dist/index.cjs ADDED
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ UVerify: () => UVerify,
24
+ UVerifyConnectionError: () => UVerifyConnectionError,
25
+ UVerifyError: () => UVerifyError,
26
+ UVerifySignatureError: () => UVerifySignatureError,
27
+ VERSION: () => VERSION,
28
+ constructEvent: () => constructEvent,
29
+ default: () => index_default,
30
+ signPayload: () => signPayload
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/client.ts
35
+ var import_node_crypto2 = require("crypto");
36
+
37
+ // src/errors.ts
38
+ var UVerifyError = class extends Error {
39
+ /** e.g. validation_error, insufficient_balance, duplicate_reference, rate_limited. */
40
+ code;
41
+ /** HTTP status (0 when the request never got a response). */
42
+ status;
43
+ /** Quote this to UVerify support to trace the request. */
44
+ requestId;
45
+ /** Field errors for validation_error; retry_after_seconds for rate_limited; … */
46
+ details;
47
+ constructor(message, opts) {
48
+ super(message);
49
+ this.name = "UVerifyError";
50
+ this.code = opts.code;
51
+ this.status = opts.status;
52
+ this.requestId = opts.requestId ?? null;
53
+ this.details = opts.details;
54
+ }
55
+ };
56
+ var UVerifyConnectionError = class extends UVerifyError {
57
+ constructor(message, cause) {
58
+ super(message, { code: "connection_error", status: 0 });
59
+ this.name = "UVerifyConnectionError";
60
+ if (cause) this.cause = cause;
61
+ }
62
+ };
63
+ var UVerifySignatureError = class extends Error {
64
+ constructor(message) {
65
+ super(message);
66
+ this.name = "UVerifySignatureError";
67
+ }
68
+ };
69
+
70
+ // src/webhooks.ts
71
+ var import_node_crypto = require("crypto");
72
+ function constructEvent(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
73
+ const header = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
74
+ if (!header) throw new UVerifySignatureError("Missing UVerify-Signature header.");
75
+ if (!secret) throw new UVerifySignatureError("Missing webhook secret.");
76
+ const parts = Object.fromEntries(
77
+ header.split(",").map((p) => {
78
+ const i = p.indexOf("=");
79
+ return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
80
+ })
81
+ );
82
+ const t = parts.t;
83
+ const v1 = parts.v1;
84
+ if (!t || !v1 || !/^\d+$/.test(t)) throw new UVerifySignatureError("Malformed UVerify-Signature header.");
85
+ const body = typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8");
86
+ const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(`${t}.${body}`).digest("hex");
87
+ const a = Buffer.from(expected, "hex");
88
+ const b = Buffer.from(v1.length === expected.length ? v1 : "", "hex");
89
+ if (a.length !== b.length || !(0, import_node_crypto.timingSafeEqual)(a, b)) throw new UVerifySignatureError("Signature does not match. Check the secret and that you passed the raw body.");
90
+ if (toleranceSeconds > 0 && Math.abs(Date.now() / 1e3 - Number(t)) > toleranceSeconds) throw new UVerifySignatureError("Signature is too old (possible replay).");
91
+ try {
92
+ return JSON.parse(body);
93
+ } catch {
94
+ throw new UVerifySignatureError("Body is not JSON.");
95
+ }
96
+ }
97
+ function signPayload(body, secret, timestamp = Math.floor(Date.now() / 1e3)) {
98
+ return `t=${timestamp},v1=${(0, import_node_crypto.createHmac)("sha256", secret).update(`${timestamp}.${body}`).digest("hex")}`;
99
+ }
100
+
101
+ // src/client.ts
102
+ var VERSION = "0.1.0";
103
+ var DEFAULT_BASE_URL = "https://api.uverify.com.ng/v1";
104
+ var IMAGE_FIELDS = ["selfie_image", "front_image", "back_image"];
105
+ var newReference = (prefix) => `${prefix}_${(0, import_node_crypto2.randomBytes)(9).toString("base64url")}`;
106
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
107
+ function toBase64(v) {
108
+ return typeof v === "string" ? v : Buffer.from(v).toString("base64");
109
+ }
110
+ var UVerify = class _UVerify {
111
+ identity;
112
+ business;
113
+ verifications;
114
+ liveness;
115
+ documents;
116
+ aml;
117
+ kyc;
118
+ account;
119
+ static webhooks = { constructEvent, signPayload };
120
+ /** Verify webhook signatures. Also usable without a client: `UVerify.webhooks.constructEvent(…)`. */
121
+ webhooks = _UVerify.webhooks;
122
+ apiKey;
123
+ baseUrl;
124
+ timeoutMs;
125
+ maxRetries;
126
+ fetchImpl;
127
+ constructor(options = {}) {
128
+ const o = typeof options === "string" ? { apiKey: options } : options;
129
+ const key = o.apiKey ?? process.env.UVERIFY_API_KEY ?? "";
130
+ if (!key) throw new Error("UVerify: pass an apiKey (uvk_test_\u2026 or uvk_live_\u2026) or set UVERIFY_API_KEY.");
131
+ this.apiKey = key;
132
+ this.baseUrl = (o.baseUrl ?? process.env.UVERIFY_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
133
+ this.timeoutMs = o.timeoutMs ?? 6e4;
134
+ this.maxRetries = o.maxRetries ?? 2;
135
+ this.fetchImpl = o.fetch ?? fetch;
136
+ this.identity = new IdentityResource(this);
137
+ this.business = new BusinessResource(this);
138
+ this.verifications = new VerificationsResource(this);
139
+ this.liveness = new LivenessResource(this);
140
+ this.documents = new DocumentsResource(this);
141
+ this.aml = new AmlResource(this);
142
+ this.kyc = new KycResource(this);
143
+ this.account = new AccountResource(this);
144
+ }
145
+ /** 'test' for a sandbox key, 'live' for a live one. */
146
+ get environment() {
147
+ return /_live_/.test(this.apiKey) ? "live" : "test";
148
+ }
149
+ /** @internal */
150
+ async request(method, path, opts = {}) {
151
+ const url = new URL(this.baseUrl + path);
152
+ for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
153
+ const body = opts.body ? JSON.stringify(Object.fromEntries(Object.entries(opts.body).filter(([, v]) => v !== void 0))) : void 0;
154
+ const retryable = opts.retryable ?? method === "GET";
155
+ for (let attempt = 0; ; attempt++) {
156
+ let res;
157
+ try {
158
+ res = await this.fetchImpl(url, {
159
+ method,
160
+ headers: {
161
+ authorization: `Bearer ${this.apiKey}`,
162
+ accept: "application/json",
163
+ "user-agent": `uverify-node/${VERSION} node/${process.version}`,
164
+ ...body ? { "content-type": "application/json" } : {}
165
+ },
166
+ body,
167
+ signal: AbortSignal.timeout(this.timeoutMs)
168
+ });
169
+ } catch (e) {
170
+ if (retryable && attempt < this.maxRetries) {
171
+ await sleep(backoff(attempt));
172
+ continue;
173
+ }
174
+ const timedOut = e?.name === "TimeoutError";
175
+ throw new UVerifyConnectionError(timedOut ? `UVerify didn't answer within ${this.timeoutMs}ms.` : `Couldn't reach UVerify: ${e.message}`, e);
176
+ }
177
+ const json = await res.json().catch(() => null);
178
+ if (res.ok && json?.success !== false) return json?.data ?? json;
179
+ const err = new UVerifyError(json?.error?.message ?? `UVerify returned HTTP ${res.status}.`, {
180
+ code: json?.error?.code ?? (res.status >= 500 ? "internal_error" : "http_error"),
181
+ status: res.status,
182
+ requestId: json?.request_id ?? res.headers.get("x-request-id"),
183
+ details: json?.error?.details
184
+ });
185
+ const rateLimited = res.status === 429 && err.code === "rate_limited";
186
+ if (retryable && attempt < this.maxRetries && (rateLimited || res.status >= 500 && res.status !== 501)) {
187
+ const after = Number(err.details?.retry_after_seconds);
188
+ await sleep(rateLimited && after > 0 ? Math.min(after, 60) * 1e3 : backoff(attempt));
189
+ continue;
190
+ }
191
+ if (attempt > 0 && err.code === "duplicate_reference") throw Object.assign(err, { __duplicateOnRetry: true });
192
+ throw err;
193
+ }
194
+ }
195
+ /** @internal An identity/business check: always carries a reference, so it can be retried safely. */
196
+ async check(path, params) {
197
+ const body = { ...params, reference: params.reference ?? newReference("sdk") };
198
+ for (const f of IMAGE_FIELDS) if (body[f] !== void 0) body[f] = toBase64(body[f]);
199
+ try {
200
+ return await this.request("POST", path, { body, retryable: true });
201
+ } catch (e) {
202
+ if (e.__duplicateOnRetry) {
203
+ const found = await this.verifications.list({ reference: body.reference, per_page: 1 });
204
+ if (found.items[0]) return this.verifications.get(found.items[0].id);
205
+ }
206
+ throw e;
207
+ }
208
+ }
209
+ /** @internal A create that carries a reference (safe to retry). */
210
+ async create(path, params, prefix) {
211
+ const body = { ...params, reference: params.reference ?? newReference(prefix) };
212
+ for (const f of IMAGE_FIELDS) if (body[f] !== void 0) body[f] = toBase64(body[f]);
213
+ return this.request("POST", path, { body, retryable: true });
214
+ }
215
+ };
216
+ var backoff = (attempt) => Math.min(8e3, 500 * 2 ** attempt) * (0.75 + Math.random() * 0.5);
217
+ var Resource = class {
218
+ constructor(client) {
219
+ this.client = client;
220
+ }
221
+ client;
222
+ };
223
+ var IdentityResource = class extends Resource {
224
+ /** BVN lookup. Names are required by the registry. */
225
+ bvn(params) {
226
+ return this.client.check("/identity/bvn", params);
227
+ }
228
+ /** BVN lookup + face match against the BVN photo. */
229
+ bvnFaceMatch(params) {
230
+ return this.client.check("/identity/bvn/face-match", params);
231
+ }
232
+ nin(params) {
233
+ return this.client.check("/identity/nin", params);
234
+ }
235
+ ninFaceMatch(params) {
236
+ return this.client.check("/identity/nin/face-match", params);
237
+ }
238
+ driversLicense(params) {
239
+ return this.client.check("/identity/drivers-license", params);
240
+ }
241
+ driversLicenseFaceMatch(params) {
242
+ return this.client.check("/identity/drivers-license/face-match", params);
243
+ }
244
+ votersCard(params) {
245
+ return this.client.check("/identity/voters-card", params);
246
+ }
247
+ votersCardFaceMatch(params) {
248
+ return this.client.check("/identity/voters-card/face-match", params);
249
+ }
250
+ /** Tax ID lookup. */
251
+ tin(params) {
252
+ return this.client.check("/identity/tin", params);
253
+ }
254
+ };
255
+ var BusinessResource = class extends Resource {
256
+ /** CAC lookup: status, address, directors, owners. */
257
+ cac(params) {
258
+ return this.client.check("/business/cac", params);
259
+ }
260
+ };
261
+ var VerificationsResource = class extends Resource {
262
+ list(params = {}) {
263
+ return this.client.request("GET", "/verifications", { query: { ...params } });
264
+ }
265
+ /** One verification, with its record. */
266
+ get(id) {
267
+ return this.client.request("GET", `/verifications/${encodeURIComponent(id)}`);
268
+ }
269
+ };
270
+ var LivenessResource = class extends Resource {
271
+ /** A hosted camera check. Send the person to `session.url`, then pass `session.id` to a face match. */
272
+ createSession(params = {}) {
273
+ return this.client.create("/liveness/sessions", params, "lv");
274
+ }
275
+ getSession(id) {
276
+ return this.client.request("GET", `/liveness/sessions/${encodeURIComponent(id)}`);
277
+ }
278
+ /** Sandbox only: finish a session without a camera. */
279
+ simulate(id, params) {
280
+ return this.client.request("POST", `/liveness/sessions/${encodeURIComponent(id)}/simulate`, { body: { ...params } });
281
+ }
282
+ };
283
+ var DocumentsResource = class extends Resource {
284
+ /** Read and check a photo of an ID (NIN slip or card, licence, voter's card, passport). Images: Buffer or base64. */
285
+ verify(params) {
286
+ return this.client.create("/documents/verify", params, "doc");
287
+ }
288
+ get(id) {
289
+ return this.client.request("GET", `/documents/${encodeURIComponent(id)}`);
290
+ }
291
+ };
292
+ var AmlMonitorsResource = class extends Resource {
293
+ /** Screen a name now and keep watching it (billed monthly). */
294
+ create(params) {
295
+ return this.client.create("/aml/monitors", params, "amon");
296
+ }
297
+ list(params = {}) {
298
+ return this.client.request("GET", "/aml/monitors", { query: { ...params } });
299
+ }
300
+ get(id) {
301
+ return this.client.request("GET", `/aml/monitors/${encodeURIComponent(id)}`);
302
+ }
303
+ /** Stop watching a name. */
304
+ stop(id) {
305
+ return this.client.request("DELETE", `/aml/monitors/${encodeURIComponent(id)}`, { retryable: true });
306
+ }
307
+ };
308
+ var AmlResource = class extends Resource {
309
+ monitors = new AmlMonitorsResource(this.client);
310
+ /** Screen a person or organisation against the UN, OFAC, UK, EU and Nigeria sanctions lists. */
311
+ screen(params) {
312
+ return this.client.create("/aml/screen", params, "aml");
313
+ }
314
+ getScreening(id) {
315
+ return this.client.request("GET", `/aml/screenings/${encodeURIComponent(id)}`);
316
+ }
317
+ /** The lists screened, and how fresh each is. */
318
+ lists() {
319
+ return this.client.request("GET", "/aml/lists");
320
+ }
321
+ };
322
+ var KycResource = class extends Resource {
323
+ /** A hosted verification link: send `link.url` to your customer. */
324
+ createLink(params = {}) {
325
+ return this.client.create("/kyc/requests", params, "kyc");
326
+ }
327
+ getLink(id) {
328
+ return this.client.request("GET", `/kyc/requests/${encodeURIComponent(id)}`);
329
+ }
330
+ listLinks(params = {}) {
331
+ return this.client.request("GET", "/kyc/requests", { query: { ...params } });
332
+ }
333
+ };
334
+ var AccountResource = class extends Resource {
335
+ balance() {
336
+ return this.client.request("GET", "/balance");
337
+ }
338
+ /** Your prices per check (custom prices included). */
339
+ pricing() {
340
+ return this.client.request("GET", "/pricing");
341
+ }
342
+ };
343
+
344
+ // src/index.ts
345
+ var index_default = UVerify;
346
+ // Annotate the CommonJS export names for ESM import in node:
347
+ 0 && (module.exports = {
348
+ UVerify,
349
+ UVerifyConnectionError,
350
+ UVerifyError,
351
+ UVerifySignatureError,
352
+ VERSION,
353
+ constructEvent,
354
+ signPayload
355
+ });