@certysign/sdk 2.4.0 → 2.5.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 +76 -0
- package/package.json +54 -54
- package/src/index.js +228 -214
- package/src/lib/BillingResource.js +90 -0
- package/src/lib/EnvelopeResource.js +306 -249
- package/src/lib/HashSigningResource.js +9 -2
- package/src/lib/HttpClient.js +211 -198
- package/src/lib/IdentityResource.js +143 -0
- package/src/lib/SignerResource.js +120 -0
- package/src/lib/SigningResource.js +11 -7
|
@@ -105,13 +105,20 @@ class HashSigningResource {
|
|
|
105
105
|
location = 'Nairobi, Kenya',
|
|
106
106
|
signerName,
|
|
107
107
|
signerEmail,
|
|
108
|
-
metadata = {}
|
|
108
|
+
metadata = {},
|
|
109
|
+
// Second factor. Single-document signing challenges the named signer: the
|
|
110
|
+
// first call returns 2FA_REQUIRED with a verificationNonce and sends them a
|
|
111
|
+
// code; re-send the same request carrying both. Batch signing is exempt.
|
|
112
|
+
twoFactorCode,
|
|
113
|
+
twoFactorMethod,
|
|
114
|
+
verificationNonce
|
|
109
115
|
} = options;
|
|
110
116
|
|
|
111
117
|
if (!documentHash) throw new Error('signHash: documentHash is required');
|
|
112
118
|
|
|
113
119
|
return this._http.post('/sdk/v1/sign/hash', {
|
|
114
|
-
data: { documentHash, hashAlgorithm, fileName, reason, location, signerName, signerEmail, metadata
|
|
120
|
+
data: { documentHash, hashAlgorithm, fileName, reason, location, signerName, signerEmail, metadata,
|
|
121
|
+
twoFactorCode, twoFactorMethod, verificationNonce }
|
|
115
122
|
});
|
|
116
123
|
}
|
|
117
124
|
|
package/src/lib/HttpClient.js
CHANGED
|
@@ -1,198 +1,211 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview CertySign SDK — HTTP client layer
|
|
3
|
-
*
|
|
4
|
-
* Wraps axios with:
|
|
5
|
-
* - Automatic API key header injection
|
|
6
|
-
* - Idempotency key support
|
|
7
|
-
* - Retry with exponential back-off (network errors, 429, 503)
|
|
8
|
-
* - Consistent error normalisation
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
'use strict';
|
|
12
|
-
|
|
13
|
-
const axios = require('axios');
|
|
14
|
-
const FormData = require('form-data');
|
|
15
|
-
const crypto = require('crypto');
|
|
16
|
-
|
|
17
|
-
/** Default base URL — override per environment */
|
|
18
|
-
const DEFAULT_BASE_URL = 'https://core.certysign.io';
|
|
19
|
-
|
|
20
|
-
/** Statuses that should trigger a retry */
|
|
21
|
-
const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {Object} HttpClientOptions
|
|
25
|
-
* @property {string} publicKey - API public key (cs_pk_...)
|
|
26
|
-
* @property {string} secretKey - API secret key (cs_sk_...)
|
|
27
|
-
* @property {string} [baseUrl] - Override default gateway URL
|
|
28
|
-
* @property {number} [timeout] - Request timeout ms (default 30000)
|
|
29
|
-
* @property {number} [retries] - Max retries on transient errors (default 3)
|
|
30
|
-
* @property {boolean} [debug] - Log requests/responses
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
class HttpClient {
|
|
34
|
-
/**
|
|
35
|
-
* @param {HttpClientOptions} options
|
|
36
|
-
*/
|
|
37
|
-
constructor(options) {
|
|
38
|
-
if (!options.publicKey) throw new Error('CertySign SDK: publicKey is required');
|
|
39
|
-
if (!options.secretKey) throw new Error('CertySign SDK: secretKey is required');
|
|
40
|
-
|
|
41
|
-
this.publicKey = options.publicKey;
|
|
42
|
-
this.secretKey = options.secretKey;
|
|
43
|
-
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
44
|
-
this.timeout = options.timeout ?? 30_000;
|
|
45
|
-
this.retries = options.retries ?? 3;
|
|
46
|
-
this.debug = options.debug ?? false;
|
|
47
|
-
|
|
48
|
-
this._client = axios.create({
|
|
49
|
-
baseURL: this.baseUrl,
|
|
50
|
-
timeout: this.timeout,
|
|
51
|
-
headers: {
|
|
52
|
-
'Content-Type': 'application/json',
|
|
53
|
-
'Accept': 'application/json',
|
|
54
|
-
'User-Agent': `CertySign-SDK-Node/1.0.0 Node/${process.version}`
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── Core request execution ───────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Execute an HTTP request with retry logic.
|
|
63
|
-
*
|
|
64
|
-
* @param {string} method - HTTP verb (GET, POST, PUT, DELETE, PATCH)
|
|
65
|
-
* @param {string} path - Path relative to baseUrl
|
|
66
|
-
* @param {Object} [options]
|
|
67
|
-
* @param {Object} [options.data] - JSON body
|
|
68
|
-
* @param {Object} [options.params] - Query params
|
|
69
|
-
* @param {Object} [options.headers] - Extra headers
|
|
70
|
-
* @param {FormData} [options.formData] - Multipart form (takes precedence over data)
|
|
71
|
-
* @param {string} [options.idempotencyKey] - Idempotency key (auto-generated if omitted)
|
|
72
|
-
* @param {string} [options.accept] - Override Accept header
|
|
73
|
-
* @param {'json'|'arraybuffer'|'text'} [options.responseType] - Expected response type
|
|
74
|
-
* @returns {Promise<any>}
|
|
75
|
-
*/
|
|
76
|
-
async request(method, path, options = {}) {
|
|
77
|
-
const {
|
|
78
|
-
data,
|
|
79
|
-
params,
|
|
80
|
-
headers: extraHeaders = {},
|
|
81
|
-
formData,
|
|
82
|
-
idempotencyKey,
|
|
83
|
-
accept,
|
|
84
|
-
responseType = 'json'
|
|
85
|
-
} = options;
|
|
86
|
-
|
|
87
|
-
const idempKey = idempotencyKey || `sdk_${crypto.randomBytes(16).toString('hex')}`;
|
|
88
|
-
|
|
89
|
-
const headers = {
|
|
90
|
-
'X-API-Key-Id': this.publicKey,
|
|
91
|
-
'X-API-Key-Secret': this.secretKey,
|
|
92
|
-
'X-Idempotency-Key': idempKey,
|
|
93
|
-
...extraHeaders
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
if (accept) headers['Accept'] = accept;
|
|
97
|
-
|
|
98
|
-
const config = {
|
|
99
|
-
method,
|
|
100
|
-
url: path,
|
|
101
|
-
headers,
|
|
102
|
-
params,
|
|
103
|
-
responseType
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
if (formData) {
|
|
107
|
-
config.data = formData;
|
|
108
|
-
config.headers = { ...config.headers, ...formData.getHeaders() };
|
|
109
|
-
delete config.headers['Content-Type']; // Let axios set multipart boundary
|
|
110
|
-
} else if (data !== undefined) {
|
|
111
|
-
config.data = data;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
let attempt = 0;
|
|
115
|
-
|
|
116
|
-
while (true) { // eslint-disable-line no-constant-condition
|
|
117
|
-
attempt++;
|
|
118
|
-
|
|
119
|
-
if (this.debug) {
|
|
120
|
-
console.debug(`[CertySign SDK] ${method.toUpperCase()} ${path} (attempt ${attempt})`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const response = await this._client.request(config);
|
|
125
|
-
return response.data;
|
|
126
|
-
|
|
127
|
-
} catch (err) {
|
|
128
|
-
const status = err.response?.status;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CertySign SDK — HTTP client layer
|
|
3
|
+
*
|
|
4
|
+
* Wraps axios with:
|
|
5
|
+
* - Automatic API key header injection
|
|
6
|
+
* - Idempotency key support
|
|
7
|
+
* - Retry with exponential back-off (network errors, 429, 503)
|
|
8
|
+
* - Consistent error normalisation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const axios = require('axios');
|
|
14
|
+
const FormData = require('form-data');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
|
|
17
|
+
/** Default base URL — override per environment */
|
|
18
|
+
const DEFAULT_BASE_URL = 'https://core.certysign.io';
|
|
19
|
+
|
|
20
|
+
/** Statuses that should trigger a retry */
|
|
21
|
+
const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} HttpClientOptions
|
|
25
|
+
* @property {string} publicKey - API public key (cs_pk_...)
|
|
26
|
+
* @property {string} secretKey - API secret key (cs_sk_...)
|
|
27
|
+
* @property {string} [baseUrl] - Override default gateway URL
|
|
28
|
+
* @property {number} [timeout] - Request timeout ms (default 30000)
|
|
29
|
+
* @property {number} [retries] - Max retries on transient errors (default 3)
|
|
30
|
+
* @property {boolean} [debug] - Log requests/responses
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
class HttpClient {
|
|
34
|
+
/**
|
|
35
|
+
* @param {HttpClientOptions} options
|
|
36
|
+
*/
|
|
37
|
+
constructor(options) {
|
|
38
|
+
if (!options.publicKey) throw new Error('CertySign SDK: publicKey is required');
|
|
39
|
+
if (!options.secretKey) throw new Error('CertySign SDK: secretKey is required');
|
|
40
|
+
|
|
41
|
+
this.publicKey = options.publicKey;
|
|
42
|
+
this.secretKey = options.secretKey;
|
|
43
|
+
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
44
|
+
this.timeout = options.timeout ?? 30_000;
|
|
45
|
+
this.retries = options.retries ?? 3;
|
|
46
|
+
this.debug = options.debug ?? false;
|
|
47
|
+
|
|
48
|
+
this._client = axios.create({
|
|
49
|
+
baseURL: this.baseUrl,
|
|
50
|
+
timeout: this.timeout,
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'Accept': 'application/json',
|
|
54
|
+
'User-Agent': `CertySign-SDK-Node/1.0.0 Node/${process.version}`
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Core request execution ───────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute an HTTP request with retry logic.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} method - HTTP verb (GET, POST, PUT, DELETE, PATCH)
|
|
65
|
+
* @param {string} path - Path relative to baseUrl
|
|
66
|
+
* @param {Object} [options]
|
|
67
|
+
* @param {Object} [options.data] - JSON body
|
|
68
|
+
* @param {Object} [options.params] - Query params
|
|
69
|
+
* @param {Object} [options.headers] - Extra headers
|
|
70
|
+
* @param {FormData} [options.formData] - Multipart form (takes precedence over data)
|
|
71
|
+
* @param {string} [options.idempotencyKey] - Idempotency key (auto-generated if omitted)
|
|
72
|
+
* @param {string} [options.accept] - Override Accept header
|
|
73
|
+
* @param {'json'|'arraybuffer'|'text'} [options.responseType] - Expected response type
|
|
74
|
+
* @returns {Promise<any>}
|
|
75
|
+
*/
|
|
76
|
+
async request(method, path, options = {}) {
|
|
77
|
+
const {
|
|
78
|
+
data,
|
|
79
|
+
params,
|
|
80
|
+
headers: extraHeaders = {},
|
|
81
|
+
formData,
|
|
82
|
+
idempotencyKey,
|
|
83
|
+
accept,
|
|
84
|
+
responseType = 'json'
|
|
85
|
+
} = options;
|
|
86
|
+
|
|
87
|
+
const idempKey = idempotencyKey || `sdk_${crypto.randomBytes(16).toString('hex')}`;
|
|
88
|
+
|
|
89
|
+
const headers = {
|
|
90
|
+
'X-API-Key-Id': this.publicKey,
|
|
91
|
+
'X-API-Key-Secret': this.secretKey,
|
|
92
|
+
'X-Idempotency-Key': idempKey,
|
|
93
|
+
...extraHeaders
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (accept) headers['Accept'] = accept;
|
|
97
|
+
|
|
98
|
+
const config = {
|
|
99
|
+
method,
|
|
100
|
+
url: path,
|
|
101
|
+
headers,
|
|
102
|
+
params,
|
|
103
|
+
responseType
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (formData) {
|
|
107
|
+
config.data = formData;
|
|
108
|
+
config.headers = { ...config.headers, ...formData.getHeaders() };
|
|
109
|
+
delete config.headers['Content-Type']; // Let axios set multipart boundary
|
|
110
|
+
} else if (data !== undefined) {
|
|
111
|
+
config.data = data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let attempt = 0;
|
|
115
|
+
|
|
116
|
+
while (true) { // eslint-disable-line no-constant-condition
|
|
117
|
+
attempt++;
|
|
118
|
+
|
|
119
|
+
if (this.debug) {
|
|
120
|
+
console.debug(`[CertySign SDK] ${method.toUpperCase()} ${path} (attempt ${attempt})`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const response = await this._client.request(config);
|
|
125
|
+
return response.data;
|
|
126
|
+
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const status = err.response?.status;
|
|
129
|
+
const code = err.response?.data?.code;
|
|
130
|
+
|
|
131
|
+
// A 409 usually means "your request conflicts with reality" and retrying is
|
|
132
|
+
// pointless. IDEMPOTENCY_IN_PROGRESS is the exception: an identical request
|
|
133
|
+
// with this same key is mid-flight, and the right move is to wait for it
|
|
134
|
+
// rather than fail. Retrying is safe precisely BECAUSE the key is the same —
|
|
135
|
+
// the server will hand back that request's result, not run a second one.
|
|
136
|
+
const waitingOnTwin = status === 409 && code === 'IDEMPOTENCY_IN_PROGRESS';
|
|
137
|
+
|
|
138
|
+
const canRetry = RETRYABLE_STATUS.has(status) || waitingOnTwin || !status;
|
|
139
|
+
|
|
140
|
+
if (canRetry && attempt <= this.retries) {
|
|
141
|
+
// Honour Retry-After when the server states one, rather than guessing.
|
|
142
|
+
const retryAfter = Number(err.response?.headers?.['retry-after']);
|
|
143
|
+
const delay = Number.isFinite(retryAfter) && retryAfter > 0
|
|
144
|
+
? Math.min(retryAfter * 1000, 10000)
|
|
145
|
+
: Math.min(200 * 2 ** (attempt - 1), 4000);
|
|
146
|
+
if (this.debug) console.debug(`[CertySign SDK] Retrying in ${delay}ms (status=${status})`);
|
|
147
|
+
await _sleep(delay);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw _normaliseError(err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get(path, options) { return this.request('GET', path, options); }
|
|
157
|
+
post(path, options) { return this.request('POST', path, options); }
|
|
158
|
+
put(path, options) { return this.request('PUT', path, options); }
|
|
159
|
+
patch(path, options) { return this.request('PATCH', path, options); }
|
|
160
|
+
delete(path, options) { return this.request('DELETE', path, options); }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
function _sleep(ms) {
|
|
166
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Normalise an axios error into a plain CertySignError.
|
|
171
|
+
* @param {Error} err
|
|
172
|
+
* @returns {CertySignError}
|
|
173
|
+
*/
|
|
174
|
+
function _normaliseError(err) {
|
|
175
|
+
if (err.response) {
|
|
176
|
+
const body = err.response.data;
|
|
177
|
+
const e = new CertySignError(
|
|
178
|
+
body?.message || `HTTP ${err.response.status}`,
|
|
179
|
+
err.response.status,
|
|
180
|
+
body?.code
|
|
181
|
+
);
|
|
182
|
+
e.details = body;
|
|
183
|
+
e.headers = err.response.headers;
|
|
184
|
+
return e;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (err.request) {
|
|
188
|
+
return new CertySignError(`No response received: ${err.message}`, 0, 'NETWORK_ERROR');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return new CertySignError(err.message, 0, 'SDK_INTERNAL_ERROR');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Error class thrown by all SDK methods on failure.
|
|
196
|
+
*
|
|
197
|
+
* @property {number} statusCode - HTTP status code (0 for network errors)
|
|
198
|
+
* @property {string} code - Machine-readable error code (e.g. 'INVALID_API_KEY')
|
|
199
|
+
* @property {Object} details - Full error body from the API
|
|
200
|
+
*/
|
|
201
|
+
class CertySignError extends Error {
|
|
202
|
+
constructor(message, statusCode, code) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.name = 'CertySignError';
|
|
205
|
+
this.statusCode = statusCode;
|
|
206
|
+
this.code = code;
|
|
207
|
+
this.details = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { HttpClient, CertySignError };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview IdentityResource — Identity-as-a-Service
|
|
3
|
+
*
|
|
4
|
+
* Verify a national ID against the issuing authority (IPRS for Kenya) and get
|
|
5
|
+
* the biographic record back — the same pipeline CertySign uses to onboard its
|
|
6
|
+
* own signers.
|
|
7
|
+
*
|
|
8
|
+
* Registry-first: an ID already verified anywhere on the CertySign network is
|
|
9
|
+
* served from our registry (`source: 'registry'`) with no authority call.
|
|
10
|
+
*
|
|
11
|
+
* Requires the `identity:verify` scope on your API key. That scope is NOT granted
|
|
12
|
+
* by default — enable it under Settings → Security → SDK API Keys → Permissions.
|
|
13
|
+
*
|
|
14
|
+
* PRIVACY: this endpoint processes another person's personal data. You must
|
|
15
|
+
* attest that the data subject consented; the attestation is recorded in your
|
|
16
|
+
* tenant's audit trail on every call. The full ID number is never echoed back —
|
|
17
|
+
* store the returned `identityReference` instead of the raw number.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
/** Countries / ID types the service can reach today. */
|
|
23
|
+
const SUPPORTED = Object.freeze({ KE: Object.freeze(['NATIONAL_ID']) });
|
|
24
|
+
|
|
25
|
+
class IdentityResource {
|
|
26
|
+
/** @param {import('./HttpClient').HttpClient} http */
|
|
27
|
+
constructor(http) {
|
|
28
|
+
this._http = http;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verify a national ID against the issuing authority.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} params
|
|
35
|
+
* @param {string} params.country - ISO-3166 alpha-2, e.g. 'KE'
|
|
36
|
+
* @param {string} params.idNumber - The subject's national ID number
|
|
37
|
+
* @param {string} [params.idType] - Defaults to 'NATIONAL_ID'
|
|
38
|
+
* @param {Object} params.consent - Lawful-basis attestation (REQUIRED)
|
|
39
|
+
* @param {boolean} params.consent.obtained - Must be true
|
|
40
|
+
* @param {string} [params.consent.reference] - Your own record id for the consent
|
|
41
|
+
* @param {string} [params.consent.purpose] - Why you're verifying, e.g. 'KYC onboarding'
|
|
42
|
+
* @param {string} [params.firstName] - Optional cross-check hint
|
|
43
|
+
* @param {string} [params.lastName] - Optional cross-check hint
|
|
44
|
+
* @param {string} [params.dob] - Optional cross-check hint (YYYY-MM-DD)
|
|
45
|
+
* @param {boolean} [params.forceRefresh] - Skip the registry/negative cache and
|
|
46
|
+
* go straight to the authority
|
|
47
|
+
* @param {string} [params.idempotencyKey] - Sent as X-Idempotency-Key
|
|
48
|
+
*
|
|
49
|
+
* @returns {Promise<Object>} `{ success, data }` where data contains
|
|
50
|
+
* `verified`, `decision`, `identityReference`, `idLast4`, `person`,
|
|
51
|
+
* `source` ('registry' | 'authority'), `cached`, `resultCode`, `checkedAt`.
|
|
52
|
+
*
|
|
53
|
+
* @throws {CertySignError} 403 CONSENT_REQUIRED, 402 INSUFFICIENT_BALANCE,
|
|
54
|
+
* 422 COUNTRY_NOT_SUPPORTED / ID_TYPE_NOT_SUPPORTED, 502 VERIFICATION_UNAVAILABLE
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const { data } = await client.identity.verify({
|
|
58
|
+
* country: 'KE',
|
|
59
|
+
* idNumber: '12345678',
|
|
60
|
+
* consent: { obtained: true, reference: 'loan-8821', purpose: 'KYC onboarding' }
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* if (data.verified) {
|
|
64
|
+
* console.log('Verified:', data.person.fullName, data.person.dob);
|
|
65
|
+
* // Store this instead of the raw ID number:
|
|
66
|
+
* await db.users.update(userId, { identityRef: data.identityReference });
|
|
67
|
+
* } else {
|
|
68
|
+
* console.log('Not verified:', data.decision, data.resultText);
|
|
69
|
+
* }
|
|
70
|
+
*/
|
|
71
|
+
verify(params = {}) {
|
|
72
|
+
const {
|
|
73
|
+
country, idNumber, idType = 'NATIONAL_ID', consent,
|
|
74
|
+
firstName, lastName, middleName, dob, phoneNumber,
|
|
75
|
+
forceRefresh, idempotencyKey,
|
|
76
|
+
} = params;
|
|
77
|
+
|
|
78
|
+
// Fail fast on the client so an obviously-malformed call never costs a
|
|
79
|
+
// round-trip (or a charge).
|
|
80
|
+
if (!country) throw new Error('identity.verify: country is required (e.g. "KE")');
|
|
81
|
+
if (!idNumber) throw new Error('identity.verify: idNumber is required');
|
|
82
|
+
if (!consent || consent.obtained !== true) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'identity.verify: consent.obtained must be true — you must attest that the data ' +
|
|
85
|
+
'subject consented to this verification. Pass consent: { obtained: true, reference?, purpose? }'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ctry = String(country).toUpperCase().trim();
|
|
90
|
+
const type = String(idType).toUpperCase().trim();
|
|
91
|
+
if (SUPPORTED[ctry] && !SUPPORTED[ctry].includes(type)) {
|
|
92
|
+
throw new Error(`identity.verify: idType '${type}' is not supported for ${ctry} (supported: ${SUPPORTED[ctry].join(', ')})`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const body = {
|
|
96
|
+
country: ctry,
|
|
97
|
+
idType: type,
|
|
98
|
+
idNumber: String(idNumber).trim(),
|
|
99
|
+
consent: {
|
|
100
|
+
obtained: true,
|
|
101
|
+
...(consent.reference ? { reference: consent.reference } : {}),
|
|
102
|
+
...(consent.purpose ? { purpose: consent.purpose } : {}),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
if (firstName) body.firstName = firstName;
|
|
106
|
+
if (lastName) body.lastName = lastName;
|
|
107
|
+
if (middleName) body.middleName = middleName;
|
|
108
|
+
if (dob) body.dob = dob;
|
|
109
|
+
if (phoneNumber) body.phoneNumber = phoneNumber;
|
|
110
|
+
if (forceRefresh === true) body.forceRefresh = true;
|
|
111
|
+
|
|
112
|
+
return this._http.post('/sdk/v1/identity/verify', {
|
|
113
|
+
data: body,
|
|
114
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convenience wrapper: resolve to `true` / `false` instead of the full record.
|
|
120
|
+
* Any API error still throws — a thrown error means "we could not check",
|
|
121
|
+
* which is NOT the same as "not verified", so don't collapse the two.
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} params - Same shape as {@link IdentityResource#verify}
|
|
124
|
+
* @returns {Promise<boolean>}
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* if (await client.identity.isVerified({
|
|
128
|
+
* country: 'KE', idNumber: '12345678',
|
|
129
|
+
* consent: { obtained: true, purpose: 'KYC onboarding' }
|
|
130
|
+
* })) { … }
|
|
131
|
+
*/
|
|
132
|
+
async isVerified(params = {}) {
|
|
133
|
+
const res = await this.verify(params);
|
|
134
|
+
return res?.data?.verified === true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Countries and ID types this SDK build knows the service supports. */
|
|
138
|
+
static get supported() {
|
|
139
|
+
return SUPPORTED;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { IdentityResource };
|