@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/dist/index.js ADDED
@@ -0,0 +1,322 @@
1
+ // src/client.ts
2
+ import { randomBytes } from "crypto";
3
+
4
+ // src/errors.ts
5
+ var UVerifyError = class extends Error {
6
+ /** e.g. validation_error, insufficient_balance, duplicate_reference, rate_limited. */
7
+ code;
8
+ /** HTTP status (0 when the request never got a response). */
9
+ status;
10
+ /** Quote this to UVerify support to trace the request. */
11
+ requestId;
12
+ /** Field errors for validation_error; retry_after_seconds for rate_limited; … */
13
+ details;
14
+ constructor(message, opts) {
15
+ super(message);
16
+ this.name = "UVerifyError";
17
+ this.code = opts.code;
18
+ this.status = opts.status;
19
+ this.requestId = opts.requestId ?? null;
20
+ this.details = opts.details;
21
+ }
22
+ };
23
+ var UVerifyConnectionError = class extends UVerifyError {
24
+ constructor(message, cause) {
25
+ super(message, { code: "connection_error", status: 0 });
26
+ this.name = "UVerifyConnectionError";
27
+ if (cause) this.cause = cause;
28
+ }
29
+ };
30
+ var UVerifySignatureError = class extends Error {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "UVerifySignatureError";
34
+ }
35
+ };
36
+
37
+ // src/webhooks.ts
38
+ import { createHmac, timingSafeEqual } from "crypto";
39
+ function constructEvent(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
40
+ const header = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
41
+ if (!header) throw new UVerifySignatureError("Missing UVerify-Signature header.");
42
+ if (!secret) throw new UVerifySignatureError("Missing webhook secret.");
43
+ const parts = Object.fromEntries(
44
+ header.split(",").map((p) => {
45
+ const i = p.indexOf("=");
46
+ return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
47
+ })
48
+ );
49
+ const t = parts.t;
50
+ const v1 = parts.v1;
51
+ if (!t || !v1 || !/^\d+$/.test(t)) throw new UVerifySignatureError("Malformed UVerify-Signature header.");
52
+ const body = typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8");
53
+ const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
54
+ const a = Buffer.from(expected, "hex");
55
+ const b = Buffer.from(v1.length === expected.length ? v1 : "", "hex");
56
+ if (a.length !== b.length || !timingSafeEqual(a, b)) throw new UVerifySignatureError("Signature does not match. Check the secret and that you passed the raw body.");
57
+ if (toleranceSeconds > 0 && Math.abs(Date.now() / 1e3 - Number(t)) > toleranceSeconds) throw new UVerifySignatureError("Signature is too old (possible replay).");
58
+ try {
59
+ return JSON.parse(body);
60
+ } catch {
61
+ throw new UVerifySignatureError("Body is not JSON.");
62
+ }
63
+ }
64
+ function signPayload(body, secret, timestamp = Math.floor(Date.now() / 1e3)) {
65
+ return `t=${timestamp},v1=${createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex")}`;
66
+ }
67
+
68
+ // src/client.ts
69
+ var VERSION = "0.1.0";
70
+ var DEFAULT_BASE_URL = "https://api.uverify.com.ng/v1";
71
+ var IMAGE_FIELDS = ["selfie_image", "front_image", "back_image"];
72
+ var newReference = (prefix) => `${prefix}_${randomBytes(9).toString("base64url")}`;
73
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
74
+ function toBase64(v) {
75
+ return typeof v === "string" ? v : Buffer.from(v).toString("base64");
76
+ }
77
+ var UVerify = class _UVerify {
78
+ identity;
79
+ business;
80
+ verifications;
81
+ liveness;
82
+ documents;
83
+ aml;
84
+ kyc;
85
+ account;
86
+ static webhooks = { constructEvent, signPayload };
87
+ /** Verify webhook signatures. Also usable without a client: `UVerify.webhooks.constructEvent(…)`. */
88
+ webhooks = _UVerify.webhooks;
89
+ apiKey;
90
+ baseUrl;
91
+ timeoutMs;
92
+ maxRetries;
93
+ fetchImpl;
94
+ constructor(options = {}) {
95
+ const o = typeof options === "string" ? { apiKey: options } : options;
96
+ const key = o.apiKey ?? process.env.UVERIFY_API_KEY ?? "";
97
+ if (!key) throw new Error("UVerify: pass an apiKey (uvk_test_\u2026 or uvk_live_\u2026) or set UVERIFY_API_KEY.");
98
+ this.apiKey = key;
99
+ this.baseUrl = (o.baseUrl ?? process.env.UVERIFY_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
100
+ this.timeoutMs = o.timeoutMs ?? 6e4;
101
+ this.maxRetries = o.maxRetries ?? 2;
102
+ this.fetchImpl = o.fetch ?? fetch;
103
+ this.identity = new IdentityResource(this);
104
+ this.business = new BusinessResource(this);
105
+ this.verifications = new VerificationsResource(this);
106
+ this.liveness = new LivenessResource(this);
107
+ this.documents = new DocumentsResource(this);
108
+ this.aml = new AmlResource(this);
109
+ this.kyc = new KycResource(this);
110
+ this.account = new AccountResource(this);
111
+ }
112
+ /** 'test' for a sandbox key, 'live' for a live one. */
113
+ get environment() {
114
+ return /_live_/.test(this.apiKey) ? "live" : "test";
115
+ }
116
+ /** @internal */
117
+ async request(method, path, opts = {}) {
118
+ const url = new URL(this.baseUrl + path);
119
+ for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
120
+ const body = opts.body ? JSON.stringify(Object.fromEntries(Object.entries(opts.body).filter(([, v]) => v !== void 0))) : void 0;
121
+ const retryable = opts.retryable ?? method === "GET";
122
+ for (let attempt = 0; ; attempt++) {
123
+ let res;
124
+ try {
125
+ res = await this.fetchImpl(url, {
126
+ method,
127
+ headers: {
128
+ authorization: `Bearer ${this.apiKey}`,
129
+ accept: "application/json",
130
+ "user-agent": `uverify-node/${VERSION} node/${process.version}`,
131
+ ...body ? { "content-type": "application/json" } : {}
132
+ },
133
+ body,
134
+ signal: AbortSignal.timeout(this.timeoutMs)
135
+ });
136
+ } catch (e) {
137
+ if (retryable && attempt < this.maxRetries) {
138
+ await sleep(backoff(attempt));
139
+ continue;
140
+ }
141
+ const timedOut = e?.name === "TimeoutError";
142
+ throw new UVerifyConnectionError(timedOut ? `UVerify didn't answer within ${this.timeoutMs}ms.` : `Couldn't reach UVerify: ${e.message}`, e);
143
+ }
144
+ const json = await res.json().catch(() => null);
145
+ if (res.ok && json?.success !== false) return json?.data ?? json;
146
+ const err = new UVerifyError(json?.error?.message ?? `UVerify returned HTTP ${res.status}.`, {
147
+ code: json?.error?.code ?? (res.status >= 500 ? "internal_error" : "http_error"),
148
+ status: res.status,
149
+ requestId: json?.request_id ?? res.headers.get("x-request-id"),
150
+ details: json?.error?.details
151
+ });
152
+ const rateLimited = res.status === 429 && err.code === "rate_limited";
153
+ if (retryable && attempt < this.maxRetries && (rateLimited || res.status >= 500 && res.status !== 501)) {
154
+ const after = Number(err.details?.retry_after_seconds);
155
+ await sleep(rateLimited && after > 0 ? Math.min(after, 60) * 1e3 : backoff(attempt));
156
+ continue;
157
+ }
158
+ if (attempt > 0 && err.code === "duplicate_reference") throw Object.assign(err, { __duplicateOnRetry: true });
159
+ throw err;
160
+ }
161
+ }
162
+ /** @internal An identity/business check: always carries a reference, so it can be retried safely. */
163
+ async check(path, params) {
164
+ const body = { ...params, reference: params.reference ?? newReference("sdk") };
165
+ for (const f of IMAGE_FIELDS) if (body[f] !== void 0) body[f] = toBase64(body[f]);
166
+ try {
167
+ return await this.request("POST", path, { body, retryable: true });
168
+ } catch (e) {
169
+ if (e.__duplicateOnRetry) {
170
+ const found = await this.verifications.list({ reference: body.reference, per_page: 1 });
171
+ if (found.items[0]) return this.verifications.get(found.items[0].id);
172
+ }
173
+ throw e;
174
+ }
175
+ }
176
+ /** @internal A create that carries a reference (safe to retry). */
177
+ async create(path, params, prefix) {
178
+ const body = { ...params, reference: params.reference ?? newReference(prefix) };
179
+ for (const f of IMAGE_FIELDS) if (body[f] !== void 0) body[f] = toBase64(body[f]);
180
+ return this.request("POST", path, { body, retryable: true });
181
+ }
182
+ };
183
+ var backoff = (attempt) => Math.min(8e3, 500 * 2 ** attempt) * (0.75 + Math.random() * 0.5);
184
+ var Resource = class {
185
+ constructor(client) {
186
+ this.client = client;
187
+ }
188
+ client;
189
+ };
190
+ var IdentityResource = class extends Resource {
191
+ /** BVN lookup. Names are required by the registry. */
192
+ bvn(params) {
193
+ return this.client.check("/identity/bvn", params);
194
+ }
195
+ /** BVN lookup + face match against the BVN photo. */
196
+ bvnFaceMatch(params) {
197
+ return this.client.check("/identity/bvn/face-match", params);
198
+ }
199
+ nin(params) {
200
+ return this.client.check("/identity/nin", params);
201
+ }
202
+ ninFaceMatch(params) {
203
+ return this.client.check("/identity/nin/face-match", params);
204
+ }
205
+ driversLicense(params) {
206
+ return this.client.check("/identity/drivers-license", params);
207
+ }
208
+ driversLicenseFaceMatch(params) {
209
+ return this.client.check("/identity/drivers-license/face-match", params);
210
+ }
211
+ votersCard(params) {
212
+ return this.client.check("/identity/voters-card", params);
213
+ }
214
+ votersCardFaceMatch(params) {
215
+ return this.client.check("/identity/voters-card/face-match", params);
216
+ }
217
+ /** Tax ID lookup. */
218
+ tin(params) {
219
+ return this.client.check("/identity/tin", params);
220
+ }
221
+ };
222
+ var BusinessResource = class extends Resource {
223
+ /** CAC lookup: status, address, directors, owners. */
224
+ cac(params) {
225
+ return this.client.check("/business/cac", params);
226
+ }
227
+ };
228
+ var VerificationsResource = class extends Resource {
229
+ list(params = {}) {
230
+ return this.client.request("GET", "/verifications", { query: { ...params } });
231
+ }
232
+ /** One verification, with its record. */
233
+ get(id) {
234
+ return this.client.request("GET", `/verifications/${encodeURIComponent(id)}`);
235
+ }
236
+ };
237
+ var LivenessResource = class extends Resource {
238
+ /** A hosted camera check. Send the person to `session.url`, then pass `session.id` to a face match. */
239
+ createSession(params = {}) {
240
+ return this.client.create("/liveness/sessions", params, "lv");
241
+ }
242
+ getSession(id) {
243
+ return this.client.request("GET", `/liveness/sessions/${encodeURIComponent(id)}`);
244
+ }
245
+ /** Sandbox only: finish a session without a camera. */
246
+ simulate(id, params) {
247
+ return this.client.request("POST", `/liveness/sessions/${encodeURIComponent(id)}/simulate`, { body: { ...params } });
248
+ }
249
+ };
250
+ var DocumentsResource = class extends Resource {
251
+ /** Read and check a photo of an ID (NIN slip or card, licence, voter's card, passport). Images: Buffer or base64. */
252
+ verify(params) {
253
+ return this.client.create("/documents/verify", params, "doc");
254
+ }
255
+ get(id) {
256
+ return this.client.request("GET", `/documents/${encodeURIComponent(id)}`);
257
+ }
258
+ };
259
+ var AmlMonitorsResource = class extends Resource {
260
+ /** Screen a name now and keep watching it (billed monthly). */
261
+ create(params) {
262
+ return this.client.create("/aml/monitors", params, "amon");
263
+ }
264
+ list(params = {}) {
265
+ return this.client.request("GET", "/aml/monitors", { query: { ...params } });
266
+ }
267
+ get(id) {
268
+ return this.client.request("GET", `/aml/monitors/${encodeURIComponent(id)}`);
269
+ }
270
+ /** Stop watching a name. */
271
+ stop(id) {
272
+ return this.client.request("DELETE", `/aml/monitors/${encodeURIComponent(id)}`, { retryable: true });
273
+ }
274
+ };
275
+ var AmlResource = class extends Resource {
276
+ monitors = new AmlMonitorsResource(this.client);
277
+ /** Screen a person or organisation against the UN, OFAC, UK, EU and Nigeria sanctions lists. */
278
+ screen(params) {
279
+ return this.client.create("/aml/screen", params, "aml");
280
+ }
281
+ getScreening(id) {
282
+ return this.client.request("GET", `/aml/screenings/${encodeURIComponent(id)}`);
283
+ }
284
+ /** The lists screened, and how fresh each is. */
285
+ lists() {
286
+ return this.client.request("GET", "/aml/lists");
287
+ }
288
+ };
289
+ var KycResource = class extends Resource {
290
+ /** A hosted verification link: send `link.url` to your customer. */
291
+ createLink(params = {}) {
292
+ return this.client.create("/kyc/requests", params, "kyc");
293
+ }
294
+ getLink(id) {
295
+ return this.client.request("GET", `/kyc/requests/${encodeURIComponent(id)}`);
296
+ }
297
+ listLinks(params = {}) {
298
+ return this.client.request("GET", "/kyc/requests", { query: { ...params } });
299
+ }
300
+ };
301
+ var AccountResource = class extends Resource {
302
+ balance() {
303
+ return this.client.request("GET", "/balance");
304
+ }
305
+ /** Your prices per check (custom prices included). */
306
+ pricing() {
307
+ return this.client.request("GET", "/pricing");
308
+ }
309
+ };
310
+
311
+ // src/index.ts
312
+ var index_default = UVerify;
313
+ export {
314
+ UVerify,
315
+ UVerifyConnectionError,
316
+ UVerifyError,
317
+ UVerifySignatureError,
318
+ VERSION,
319
+ constructEvent,
320
+ index_default as default,
321
+ signPayload
322
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@uverifyng/node",
3
+ "version": "0.1.0",
4
+ "description": "Official Node.js library for the UVerify API: BVN, NIN and ID checks, liveness, face match, ID documents and AML screening for Nigeria.",
5
+ "license": "MIT",
6
+ "author": "Elasto Web Services Limited",
7
+ "homepage": "https://uverify.com.ng/docs",
8
+ "repository": { "type": "git", "url": "https://github.com/elastodev/uverify-sdks", "directory": "node" },
9
+ "keywords": ["uverify", "kyc", "bvn", "nin", "liveness", "face-match", "aml", "nigeria", "identity-verification"],
10
+ "type": "module",
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
17
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
18
+ }
19
+ },
20
+ "files": ["dist", "README.md"],
21
+ "engines": { "node": ">=18" },
22
+ "sideEffects": false,
23
+ "scripts": {
24
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.0.0",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.6.0",
32
+ "vitest": "^2.1.0"
33
+ }
34
+ }