@kyciris/core 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,518 +1,1990 @@
1
1
  'use strict';
2
2
 
3
- var axios = require('axios');
4
-
5
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
-
7
- var axios__default = /*#__PURE__*/_interopDefault(axios);
8
-
9
- // src/index.ts
10
- var KYCSdkError = class extends Error {
11
- /**
12
- * Creates a new KYC SDK error
13
- * @param message Human-readable error message
14
- * @param code Error code for handling
15
- * @param statusCode Optional HTTP status code
16
- */
17
- constructor(message, code, statusCode) {
3
+ // src/errors.ts
4
+ var GATEWAY_ERROR_CODES = [
5
+ // OCR processing
6
+ "INVALID_OCR_DATA",
7
+ "OCR_EXTRACTION_ERROR",
8
+ // Document validation
9
+ "DOCUMENT_EXPIRED",
10
+ "INVALID_DOCUMENT_QUALITY",
11
+ // Face matching
12
+ "FACE_MISMATCH",
13
+ "LOW_FACE_MATCH_SCORE",
14
+ "NO_FACE",
15
+ // Manual review
16
+ "MANUAL_REJECTION",
17
+ // Worker terminal failures
18
+ "BAD_PAYLOAD",
19
+ "UNSUPPORTED_DOCUMENT",
20
+ "BAD_IMAGE",
21
+ // Account status
22
+ "ACCOUNT_SUSPENDED",
23
+ // Generic HTTP labels
24
+ "VALIDATION_ERROR",
25
+ "UNAUTHORIZED",
26
+ "FORBIDDEN",
27
+ "NOT_FOUND",
28
+ "RESOURCE_ALREADY_EXISTS",
29
+ "RATE_LIMIT_EXCEEDED",
30
+ "INTERNAL_SERVER_ERROR"
31
+ ];
32
+ var SDK_ERROR_CODES = [
33
+ /** The client was constructed with an unusable configuration. */
34
+ "CONFIG_ERROR",
35
+ /** A project API key was about to be sent from a browser or React Native app. */
36
+ "CREDENTIAL_MISUSE",
37
+ /** The endpoint needs a credential the client was not given. */
38
+ "MISSING_CREDENTIAL",
39
+ /** The request never reached the gateway (DNS, TLS, offline, CORS). */
40
+ "NETWORK_ERROR",
41
+ /** The request exceeded `timeoutMs`. */
42
+ "TIMEOUT",
43
+ /** An AbortSignal supplied by the caller fired. */
44
+ "ABORTED",
45
+ /** A 2xx response whose body was not the documented envelope. */
46
+ "INVALID_RESPONSE",
47
+ /** A file argument could not be turned into an upload on this platform. */
48
+ "INVALID_FILE",
49
+ /** Polling ran past its own timeout without reaching a final state. */
50
+ "POLL_TIMEOUT",
51
+ /** No verification session was found in storage to resume from. */
52
+ "SESSION_NOT_FOUND"
53
+ ];
54
+ var KycirisError = class _KycirisError extends Error {
55
+ constructor(message, init) {
18
56
  super(message);
19
- this.name = "KYCSdkError";
20
- this.code = code;
21
- this.statusCode = statusCode;
57
+ this.name = "KycirisError";
58
+ if (init.cause !== void 0) {
59
+ this.cause = init.cause;
60
+ }
61
+ this.code = init.code;
62
+ this.status = init.status;
63
+ this.requestId = init.requestId;
64
+ this.details = init.details;
65
+ this.retryable = init.retryable ?? false;
66
+ Object.setPrototypeOf(this, _KycirisError.prototype);
67
+ }
68
+ /** True when this is a KycirisError, across bundle/realm boundaries. */
69
+ static isKycirisError(value) {
70
+ return value instanceof _KycirisError || typeof value === "object" && value !== null && value.name === "KycirisError" && typeof value.code === "string";
22
71
  }
23
72
  };
24
- var _KYCCore = class _KYCCore {
25
- /**
26
- * Creates a new KYC Core instance
27
- * @param credentials API credentials (apiKey, baseUrl, and optional storage)
28
- */
29
- constructor(credentials) {
30
- this.eventCallbacks = [];
31
- this.credentials = credentials;
32
- this.storage = credentials.storage;
33
- this.client = axios__default.default.create({
34
- baseURL: credentials.baseUrl,
35
- headers: {
36
- "Content-Type": "application/json",
37
- Accept: "application/json"
38
- }
73
+ function isKycirisError(value) {
74
+ return KycirisError.isKycirisError(value);
75
+ }
76
+ function isRetryableStatus(status) {
77
+ return status === 408 || status === 429 || status >= 500 && status <= 599;
78
+ }
79
+
80
+ // src/files.ts
81
+ var MAX_IMAGE_FILE_SIZE = 8 * 1024 * 1024;
82
+ var NORMALIZED = /* @__PURE__ */ Symbol.for("kyciris.normalizedFile");
83
+ function mark(file) {
84
+ Object.defineProperty(file, NORMALIZED, { value: true, enumerable: false });
85
+ return file;
86
+ }
87
+ function isNormalizedFile(value) {
88
+ return typeof value === "object" && value !== null && value[NORMALIZED] === true;
89
+ }
90
+ function isReactNative() {
91
+ return typeof navigator !== "undefined" && navigator.product === "ReactNative";
92
+ }
93
+ var JPEG_MAGIC = [255, 216, 255];
94
+ var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
95
+ function startsWith(bytes, magic) {
96
+ if (bytes.length < magic.length) return false;
97
+ return magic.every((byte, index) => bytes[index] === byte);
98
+ }
99
+ function detectImageMimeType(bytes) {
100
+ if (bytes.length < 8) return null;
101
+ if (startsWith(bytes, JPEG_MAGIC)) return "image/jpeg";
102
+ if (startsWith(bytes, PNG_MAGIC)) return "image/png";
103
+ return null;
104
+ }
105
+ var BASE64_ALPHABET = /^[A-Za-z0-9+/]+={0,2}$/;
106
+ function decodeBase64(input) {
107
+ const normalized = input.replace(/\s/g, "");
108
+ const maybeBuffer = globalThis.Buffer;
109
+ if (maybeBuffer) {
110
+ const buffer = maybeBuffer.from(normalized, "base64");
111
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
112
+ }
113
+ if (typeof atob === "function") {
114
+ const binary = atob(normalized);
115
+ const bytes = new Uint8Array(binary.length);
116
+ for (let i = 0; i < binary.length; i += 1) {
117
+ bytes[i] = binary.charCodeAt(i);
118
+ }
119
+ return bytes;
120
+ }
121
+ throw new KycirisError(
122
+ "This runtime has neither Buffer nor atob, so base64 image data cannot be decoded. Pass a Blob or raw bytes instead.",
123
+ { code: "INVALID_FILE" }
124
+ );
125
+ }
126
+ function toBytes(data) {
127
+ return data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
128
+ }
129
+ function isPlatformUri(value) {
130
+ return /^(file|content|ph|assets-library):/i.test(value);
131
+ }
132
+ var DATA_URI_RE = /^data:([^;,]*)(;[^,]*)?,(.*)$/s;
133
+ function parseDataUri(value) {
134
+ const match = DATA_URI_RE.exec(value);
135
+ if (!match) {
136
+ throw new KycirisError("Malformed data: URI.", { code: "INVALID_FILE" });
137
+ }
138
+ const [, declaredType, parameters, payload] = match;
139
+ const isBase64 = (parameters ?? "").toLowerCase().includes("base64");
140
+ let bytes;
141
+ if (isBase64) {
142
+ bytes = decodeBase64(payload);
143
+ } else {
144
+ try {
145
+ bytes = new TextEncoder().encode(decodeURIComponent(payload));
146
+ } catch (cause) {
147
+ throw new KycirisError(
148
+ "Malformed percent-encoding in a data: URI. Image data should be base64-encoded (data:image/jpeg;base64,...) \u2014 percent-encoding is UTF-8 and cannot carry arbitrary bytes.",
149
+ { code: "INVALID_FILE", cause }
150
+ );
151
+ }
152
+ }
153
+ return { mimeType: declaredType || "application/octet-stream", bytes };
154
+ }
155
+ function assertBlobSize(blob, filename) {
156
+ if (blob.size === 0) {
157
+ throw new KycirisError(`${filename} is empty.`, { code: "INVALID_FILE" });
158
+ }
159
+ if (blob.size > MAX_IMAGE_FILE_SIZE) {
160
+ throw new KycirisError(
161
+ `${filename} is ${(blob.size / 1024 / 1024).toFixed(1)}MB; the gateway accepts at most ${MAX_IMAGE_FILE_SIZE / 1024 / 1024}MB. Re-encode at a lower quality before uploading.`,
162
+ { code: "INVALID_FILE" }
163
+ );
164
+ }
165
+ }
166
+ function assertUsableImage(bytes, filename) {
167
+ if (bytes.length === 0) {
168
+ throw new KycirisError(`${filename} is empty.`, { code: "INVALID_FILE" });
169
+ }
170
+ if (bytes.length > MAX_IMAGE_FILE_SIZE) {
171
+ throw new KycirisError(
172
+ `${filename} is ${(bytes.length / 1024 / 1024).toFixed(1)}MB; the gateway accepts at most ${MAX_IMAGE_FILE_SIZE / 1024 / 1024}MB. Re-encode at a lower quality before uploading.`,
173
+ { code: "INVALID_FILE" }
174
+ );
175
+ }
176
+ const mimeType = detectImageMimeType(bytes);
177
+ if (!mimeType) {
178
+ throw new KycirisError(
179
+ `${filename} is not a JPEG or PNG image. The gateway decides the type from the file's own bytes, not its name or Content-Type.`,
180
+ { code: "INVALID_FILE" }
181
+ );
182
+ }
183
+ return mimeType;
184
+ }
185
+ function bytesToValue(bytes, mimeType, filename) {
186
+ if (isReactNative()) {
187
+ const base64 = encodeBase64(bytes);
188
+ return mark({
189
+ value: { uri: `data:${mimeType};base64,${base64}`, name: filename, type: mimeType },
190
+ filename,
191
+ mimeType
39
192
  });
40
- this.client.interceptors.request.use((config) => {
41
- config.headers["x-api-key"] = this.credentials.apiKey;
42
- return config;
193
+ }
194
+ if (typeof Blob === "undefined") {
195
+ throw new KycirisError(
196
+ "This runtime has no Blob, so image bytes cannot be turned into an upload. Node 18+ is required.",
197
+ { code: "INVALID_FILE" }
198
+ );
199
+ }
200
+ const copy = new Uint8Array(bytes.byteLength);
201
+ copy.set(bytes);
202
+ return mark({ value: new Blob([copy], { type: mimeType }), filename, mimeType });
203
+ }
204
+ function encodeBase64(bytes) {
205
+ const maybeBuffer = globalThis.Buffer;
206
+ if (maybeBuffer) {
207
+ return maybeBuffer.from(bytes).toString("base64");
208
+ }
209
+ if (typeof btoa === "function") {
210
+ let binary = "";
211
+ const CHUNK = 32768;
212
+ for (let i = 0; i < bytes.length; i += CHUNK) {
213
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
214
+ }
215
+ return btoa(binary);
216
+ }
217
+ throw new KycirisError(
218
+ "This runtime has neither Buffer nor btoa, so image bytes cannot be base64-encoded.",
219
+ { code: "INVALID_FILE" }
220
+ );
221
+ }
222
+ function normalizeFile(input, defaultName) {
223
+ if (input == null) {
224
+ throw new KycirisError(`No image was given for ${defaultName}.`, {
225
+ code: "INVALID_FILE"
43
226
  });
44
227
  }
45
- /**
46
- * Persists the active verification session via the injected storage adapter.
47
- * No-op when no storage was provided.
48
- */
49
- async saveSession(session) {
50
- if (!this.storage) return;
51
- try {
52
- await this.storage.setItem(_KYCCore.SESSION_KEY, JSON.stringify(session));
53
- } catch {
228
+ if (isNormalizedFile(input)) return input;
229
+ if (typeof Blob !== "undefined" && input instanceof Blob) {
230
+ assertBlobSize(input, defaultName);
231
+ const filename = input.name || defaultName;
232
+ return mark({ value: input, filename, mimeType: input.type || "image/jpeg" });
233
+ }
234
+ if (typeof input === "string") {
235
+ if (input.startsWith("data:")) {
236
+ const { bytes: bytes2 } = parseDataUri(input);
237
+ const mimeType2 = assertUsableImage(bytes2, defaultName);
238
+ return bytesToValue(bytes2, mimeType2, defaultName);
54
239
  }
240
+ if (isPlatformUri(input)) {
241
+ if (!isReactNative()) {
242
+ throw new KycirisError(
243
+ `${defaultName} was given as "${input.slice(0, 12)}\u2026", a URI only React Native can read. Read the file yourself and pass a Blob or its bytes.`,
244
+ { code: "INVALID_FILE" }
245
+ );
246
+ }
247
+ return mark({
248
+ value: { uri: input, name: defaultName, type: "image/jpeg" },
249
+ filename: defaultName,
250
+ mimeType: "image/jpeg"
251
+ });
252
+ }
253
+ const trimmed = input.replace(/\s/g, "");
254
+ if (!trimmed || !BASE64_ALPHABET.test(trimmed)) {
255
+ throw new KycirisError(
256
+ `${defaultName} was given as a string that is neither a data: URI, a file URI, nor base64.`,
257
+ { code: "INVALID_FILE" }
258
+ );
259
+ }
260
+ const bytes = decodeBase64(trimmed);
261
+ const mimeType = assertUsableImage(bytes, defaultName);
262
+ return bytesToValue(bytes, mimeType, defaultName);
55
263
  }
56
- /**
57
- * Reads the persisted verification session, or null if none exists or no
58
- * storage adapter was provided.
59
- */
60
- async getSession() {
61
- if (!this.storage) return null;
264
+ if ("uri" in input && typeof input.uri === "string") {
265
+ if (input.uri.startsWith("data:")) {
266
+ const { bytes } = parseDataUri(input.uri);
267
+ const mimeType2 = assertUsableImage(bytes, input.name ?? defaultName);
268
+ return bytesToValue(bytes, mimeType2, input.name ?? defaultName);
269
+ }
270
+ if (!isReactNative()) {
271
+ throw new KycirisError(
272
+ `${defaultName} was given as a { uri } object, which only React Native can read. Read the file yourself and pass a Blob or its bytes.`,
273
+ { code: "INVALID_FILE" }
274
+ );
275
+ }
276
+ const filename = input.name ?? defaultName;
277
+ const mimeType = input.type ?? "image/jpeg";
278
+ return mark({ value: { uri: input.uri, name: filename, type: mimeType }, filename, mimeType });
279
+ }
280
+ if ("data" in input && input.data) {
281
+ const bytes = toBytes(input.data);
282
+ const filename = input.name ?? defaultName;
283
+ const mimeType = assertUsableImage(bytes, filename);
284
+ return bytesToValue(bytes, mimeType, filename);
285
+ }
286
+ throw new KycirisError(
287
+ `${defaultName} is not a Blob, a { uri } object, a { data } object, or a base64/data: string.`,
288
+ { code: "INVALID_FILE" }
289
+ );
290
+ }
291
+ async function readBlobHead(blob) {
292
+ const head = blob.slice(0, 8);
293
+ if (typeof head.arrayBuffer === "function") {
62
294
  try {
63
- const raw = await this.storage.getItem(_KYCCore.SESSION_KEY);
64
- return raw ? JSON.parse(raw) : null;
295
+ return new Uint8Array(await head.arrayBuffer());
65
296
  } catch {
66
297
  return null;
67
298
  }
68
299
  }
69
- /**
70
- * Clears the persisted verification session. Call this once a verification is
71
- * fully complete so the next flow starts fresh.
72
- */
73
- async clearSession() {
74
- if (!this.storage) return;
300
+ if (typeof FileReader === "undefined") return null;
301
+ return new Promise((resolve) => {
302
+ const reader = new FileReader();
303
+ reader.onload = () => resolve(reader.result instanceof ArrayBuffer ? new Uint8Array(reader.result) : null);
304
+ reader.onerror = () => resolve(null);
305
+ reader.onabort = () => resolve(null);
75
306
  try {
76
- await this.storage.removeItem(_KYCCore.SESSION_KEY);
307
+ reader.readAsArrayBuffer(head);
77
308
  } catch {
309
+ resolve(null);
310
+ }
311
+ });
312
+ }
313
+ async function prepareFile(input, defaultName) {
314
+ if (isNormalizedFile(input)) return input;
315
+ if (typeof Blob !== "undefined" && input instanceof Blob) {
316
+ assertBlobSize(input, defaultName);
317
+ const filename = input.name || defaultName;
318
+ const head = await readBlobHead(input);
319
+ if (!head) {
320
+ return mark({ value: input, filename, mimeType: input.type || "image/jpeg" });
321
+ }
322
+ const mimeType = detectImageMimeType(head);
323
+ if (!mimeType) {
324
+ throw new KycirisError(
325
+ `${filename} is not a JPEG or PNG image. The gateway decides the type from the file's own bytes, not its name or Content-Type.`,
326
+ { code: "INVALID_FILE" }
327
+ );
78
328
  }
329
+ return mark({ value: input, filename, mimeType });
79
330
  }
80
- /**
81
- * Registers a callback for KYC status events
82
- * @param callback Function to call when status changes
83
- * @returns Unsubscribe function to remove the callback
84
- */
85
- onEvent(callback) {
86
- this.eventCallbacks.push(callback);
87
- return () => {
88
- this.eventCallbacks = this.eventCallbacks.filter((cb) => cb !== callback);
331
+ return normalizeFile(input, defaultName);
332
+ }
333
+
334
+ // src/http/envelope.ts
335
+ function isRecord(value) {
336
+ return typeof value === "object" && value !== null;
337
+ }
338
+ function readMeta(body) {
339
+ if (!isRecord(body) || !isRecord(body.meta)) return void 0;
340
+ const { requestId, timestamp, version } = body.meta;
341
+ if (typeof requestId !== "string") return void 0;
342
+ return {
343
+ requestId,
344
+ timestamp: typeof timestamp === "string" ? timestamp : "",
345
+ version: typeof version === "string" ? version : ""
346
+ };
347
+ }
348
+ function readRequestId(body) {
349
+ return readMeta(body)?.requestId;
350
+ }
351
+ function unwrapEnvelope(body, requestPath) {
352
+ const meta = readMeta(body);
353
+ if (!isRecord(body) || !("data" in body) || !meta) {
354
+ throw new KycirisError(
355
+ `The response to ${requestPath} was not a KYCiris API envelope. Check that baseUrl points at the gateway and that nothing is rewriting responses in between.`,
356
+ { code: "INVALID_RESPONSE" }
357
+ );
358
+ }
359
+ const result = { data: body.data, meta };
360
+ if (isRecord(body.pagination)) {
361
+ const { page, limit, total } = body.pagination;
362
+ if (typeof page === "number" && typeof limit === "number" && typeof total === "number") {
363
+ result.pagination = { page, limit, total };
364
+ }
365
+ }
366
+ return result;
367
+ }
368
+ function toPage(result) {
369
+ const items = Array.isArray(result.data) ? result.data : [];
370
+ return {
371
+ items,
372
+ pagination: result.pagination ?? {
373
+ page: 1,
374
+ limit: items.length,
375
+ total: items.length
376
+ }
377
+ };
378
+ }
379
+ function readErrorEnvelope(body) {
380
+ if (!isRecord(body)) {
381
+ return typeof body === "string" && body.trim() ? { message: body.trim() } : {};
382
+ }
383
+ const error = isRecord(body.error) ? body.error : void 0;
384
+ if (error) {
385
+ return {
386
+ code: typeof error.code === "string" ? error.code : void 0,
387
+ message: typeof error.message === "string" ? error.message : void 0,
388
+ details: error.details
89
389
  };
90
390
  }
91
- /**
92
- * Emits a status event to all registered callbacks
93
- * @param event The event to emit
94
- */
95
- emitEvent(event) {
96
- this.eventCallbacks.forEach((callback) => callback(event));
391
+ const message = body.message;
392
+ return {
393
+ message: Array.isArray(message) ? message.join(", ") : typeof message === "string" ? message : void 0
394
+ };
395
+ }
396
+
397
+ // src/http/client.ts
398
+ var DEFAULT_TIMEOUT_MS = 3e4;
399
+ var DEFAULT_MAX_RETRIES = 2;
400
+ var BASE_BACKOFF_MS = 300;
401
+ var MAX_BACKOFF_MS = 5e3;
402
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
403
+ function isUntrustedClient() {
404
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
405
+ return true;
97
406
  }
98
- /**
99
- * Creates a verification token for secure API access
100
- * @param externalId External reference ID
101
- * @param expiry Token expiry in hours (default: 3)
102
- * @returns Object containing the token and its expiration time
103
- */
104
- async createVerificationToken(externalId, expiry = "3") {
105
- try {
106
- const response = await this.client.post("/verification/token", {
107
- externalId,
108
- expiry
109
- });
110
- return response.data;
111
- } catch (error) {
112
- const message = error.response?.data?.message || error.message || "Failed to create verification token";
113
- throw new KYCSdkError(message, "TOKEN_CREATE_FAILED", error.response?.status);
407
+ if (typeof window !== "undefined" && typeof window.document !== "undefined") {
408
+ return true;
409
+ }
410
+ const scope = globalThis;
411
+ if (typeof scope.importScripts === "function") return true;
412
+ return /WorkerGlobalScope$/.test(scope.constructor?.name ?? "");
413
+ }
414
+ function assertUsableBaseUrl(baseUrl, allowInsecure) {
415
+ let url;
416
+ try {
417
+ url = new URL(baseUrl);
418
+ } catch {
419
+ throw new KycirisError(
420
+ `baseUrl "${baseUrl}" is not a valid URL. Expected something like https://api.kyciris.com.`,
421
+ { code: "CONFIG_ERROR" }
422
+ );
423
+ }
424
+ if (url.protocol === "https:") return url;
425
+ if (url.protocol !== "http:") {
426
+ throw new KycirisError(`baseUrl must be http:// or https://, not ${url.protocol}`, {
427
+ code: "CONFIG_ERROR"
428
+ });
429
+ }
430
+ if (LOOPBACK_HOSTS.has(url.hostname) || allowInsecure) return url;
431
+ throw new KycirisError(
432
+ `baseUrl "${baseUrl}" is plaintext http to a remote host. API keys, verification tokens and identity-document data would travel unencrypted. Use https, or set allowInsecureBaseUrl if this is a trusted private network.`,
433
+ { code: "CONFIG_ERROR" }
434
+ );
435
+ }
436
+ function parseRetryAfter(header) {
437
+ if (!header) return void 0;
438
+ const seconds = Number(header);
439
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
440
+ const date = Date.parse(header);
441
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
442
+ return void 0;
443
+ }
444
+ function backoffDelay(attempt) {
445
+ const ceiling = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** attempt);
446
+ return Math.random() * ceiling;
447
+ }
448
+ function sleep(ms, signal) {
449
+ return new Promise((resolve, reject) => {
450
+ if (signal?.aborted) {
451
+ reject(new KycirisError("The request was aborted.", { code: "ABORTED" }));
452
+ return;
453
+ }
454
+ const timer = setTimeout(() => {
455
+ signal?.removeEventListener("abort", onAbort);
456
+ resolve();
457
+ }, ms);
458
+ function onAbort() {
459
+ clearTimeout(timer);
460
+ reject(new KycirisError("The request was aborted.", { code: "ABORTED" }));
114
461
  }
462
+ signal?.addEventListener("abort", onAbort, { once: true });
463
+ });
464
+ }
465
+ var HttpClient = class {
466
+ constructor(config) {
467
+ this.baseUrl = assertUsableBaseUrl(
468
+ config.baseUrl,
469
+ config.allowInsecureBaseUrl ?? false
470
+ );
471
+ if (!config.apiKey && !config.verificationToken) {
472
+ throw new KycirisError(
473
+ "Provide either apiKey (server-side) or verificationToken (client-side) when creating the client.",
474
+ { code: "CONFIG_ERROR" }
475
+ );
476
+ }
477
+ if (config.apiKey && isUntrustedClient() && !config.allowApiKeyInUntrustedClient) {
478
+ throw new KycirisError(
479
+ "A project API key must not be used from a browser or React Native app: it is readable by anyone who has the bundle and grants access to every identity in the project. Mint a verification token on your server (POST /verification/token) and pass it as verificationToken instead.",
480
+ { code: "CREDENTIAL_MISUSE" }
481
+ );
482
+ }
483
+ this.apiKey = config.apiKey;
484
+ this.tokenSource = config.verificationToken;
485
+ this.apiVersion = (config.apiVersion ?? "v1").replace(/^\/+|\/+$/g, "");
486
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
487
+ this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_MAX_RETRIES);
488
+ this.extraHeaders = { ...config.headers };
489
+ const fetchImpl = config.fetch ?? globalThis.fetch;
490
+ if (!fetchImpl) {
491
+ throw new KycirisError(
492
+ "No global fetch was found. Use Node 18+, or pass a fetch implementation as `fetch`.",
493
+ { code: "CONFIG_ERROR" }
494
+ );
495
+ }
496
+ this.fetchImpl = fetchImpl;
115
497
  }
116
- /**
117
- * Starts a new verification session
118
- * @param params Parameters including documentType, country, identityId, and externalId
119
- * @returns Verification session with verificationId and status
120
- */
121
- async startVerification(params) {
122
- try {
123
- const response = await this.client.post("/verification/start", params);
124
- const session = response.data;
125
- if (session.identityId && session.verificationId) {
126
- await this.saveSession({
127
- identityId: session.identityId,
128
- verificationId: session.verificationId,
129
- externalId: params.externalId,
130
- documentType: params.documentType
131
- });
498
+ /** True when the client holds a project API key. */
499
+ hasApiKey() {
500
+ return Boolean(this.apiKey);
501
+ }
502
+ /** True when the client holds (or can fetch) a verification token. */
503
+ hasVerificationToken() {
504
+ return Boolean(this.tokenSource);
505
+ }
506
+ buildUrl(path, query) {
507
+ const basePath = this.baseUrl.pathname.replace(/\/+$/, "");
508
+ const url = new URL(this.baseUrl.toString());
509
+ url.pathname = `${basePath}/${this.apiVersion}${path.startsWith("/") ? path : `/${path}`}`;
510
+ if (query) {
511
+ for (const [key, value] of Object.entries(query)) {
512
+ if (value === void 0 || value === null) continue;
513
+ if (Array.isArray(value)) {
514
+ for (const entry of value) {
515
+ if (entry !== void 0 && entry !== null) {
516
+ url.searchParams.append(key, String(entry));
517
+ }
518
+ }
519
+ } else {
520
+ url.searchParams.append(key, String(value));
521
+ }
132
522
  }
133
- this.emitEvent({
134
- type: "statusChanged",
135
- status: "PENDING"
136
- });
137
- return response.data;
138
- } catch (error) {
139
- const message = error.response?.data?.message || error.message || "Failed to start verification";
140
- throw new KYCSdkError(message, "VERIFICATION_START_FAILED", error.response?.status);
141
523
  }
524
+ return url.toString();
142
525
  }
143
- /**
144
- * Uploads a selfie image for face verification
145
- * @param params Parameters including verificationId, imageData, and optional mimeType
146
- * @returns Upload result with status
147
- */
148
- async uploadSelfie(params) {
149
- try {
150
- const formData = new FormData();
151
- formData.append("verificationId", params.verificationId);
152
- const mimeType = params.mimeType || "image/jpeg";
153
- const imageData = params.imageData;
154
- if (imageData.startsWith("file://")) {
155
- formData.append("file", {
156
- uri: imageData,
157
- type: mimeType,
158
- name: "selfie.jpg"
159
- });
160
- } else if (imageData.startsWith("data:")) {
161
- formData.append("file", {
162
- uri: imageData,
163
- type: mimeType,
164
- name: "selfie.jpg"
165
- });
526
+ async authHeaders(mode) {
527
+ if (mode === "none") return {};
528
+ if (mode === "api-key") {
529
+ if (!this.apiKey) {
530
+ throw new KycirisError(
531
+ "This endpoint requires the project API key, which is a server-side credential. A verification token is scoped to one end user and is refused here.",
532
+ { code: "MISSING_CREDENTIAL", status: 401 }
533
+ );
534
+ }
535
+ return { "X-API-Key": this.apiKey };
536
+ }
537
+ const token = await this.resolveToken();
538
+ if (mode === "token") {
539
+ if (!token) {
540
+ throw new KycirisError(
541
+ "This endpoint requires a verification token. Mint one with client.verifications.createToken(externalId) using the project API key.",
542
+ { code: "MISSING_CREDENTIAL", status: 401 }
543
+ );
544
+ }
545
+ return { "x-verification-token": token };
546
+ }
547
+ if (token) return { "x-verification-token": token };
548
+ if (this.apiKey) return { "X-API-Key": this.apiKey };
549
+ throw new KycirisError("No credential is configured for this request.", {
550
+ code: "MISSING_CREDENTIAL",
551
+ status: 401
552
+ });
553
+ }
554
+ async resolveToken() {
555
+ if (!this.tokenSource) return void 0;
556
+ if (typeof this.tokenSource === "string") return this.tokenSource;
557
+ const token = await this.tokenSource();
558
+ if (typeof token !== "string" || !token) {
559
+ throw new KycirisError(
560
+ "The verificationToken provider returned no token.",
561
+ { code: "MISSING_CREDENTIAL", status: 401 }
562
+ );
563
+ }
564
+ return token;
565
+ }
566
+ async buildForm(form, fileFields) {
567
+ if (typeof FormData === "undefined") {
568
+ throw new KycirisError(
569
+ "This runtime has no FormData, so files cannot be uploaded. Node 18+ is required.",
570
+ { code: "CONFIG_ERROR" }
571
+ );
572
+ }
573
+ const formData = new FormData();
574
+ for (const [field, value] of Object.entries(form)) {
575
+ if (value === void 0) continue;
576
+ if (fileFields.includes(field)) {
577
+ const file = await prepareFile(value, `${field}.jpg`);
578
+ formData.append(field, file.value, file.filename);
166
579
  } else {
167
- formData.append("file", {
168
- uri: `data:${mimeType};base64,${imageData}`,
169
- type: mimeType,
170
- name: "selfie.jpg"
580
+ formData.append(field, String(value));
581
+ }
582
+ }
583
+ return formData;
584
+ }
585
+ /** Runs a request, retrying where the method and failure allow it. */
586
+ async request(options) {
587
+ const method = options.method ?? "GET";
588
+ const url = this.buildUrl(options.path, options.query);
589
+ const idempotent = options.retry ?? method === "GET";
590
+ const formData = options.form ? await this.buildForm(options.form, options.fileFields ?? []) : void 0;
591
+ let attempt = 0;
592
+ for (; ; ) {
593
+ try {
594
+ return await this.attempt(options, method, url, formData);
595
+ } catch (error) {
596
+ const failure = error;
597
+ const canRetry = attempt < this.maxRetries && KycirisError.isKycirisError(failure) && failure.retryable && // A rate-limited request never ran, so repeating it is safe whatever
598
+ // the method. Everything else is only repeated when the caller says
599
+ // the call can be made twice.
600
+ (idempotent || failure.status === 429);
601
+ if (!canRetry) throw error;
602
+ const delay2 = failure.status === 429 && failure.details ? failure.details.retryAfterMs ?? backoffDelay(attempt) : backoffDelay(attempt);
603
+ await sleep(delay2, options.signal);
604
+ attempt += 1;
605
+ }
606
+ }
607
+ }
608
+ async attempt(options, method, url, formData) {
609
+ if (options.signal?.aborted) {
610
+ throw new KycirisError("The request was aborted.", { code: "ABORTED" });
611
+ }
612
+ const controller = new AbortController();
613
+ const timeoutMs = options.timeoutMs ?? this.timeoutMs;
614
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
615
+ const onCallerAbort = () => controller.abort();
616
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
617
+ try {
618
+ const headers = {
619
+ Accept: "application/json",
620
+ ...this.extraHeaders,
621
+ ...await this.authHeaders(options.auth ?? "any")
622
+ };
623
+ let body;
624
+ if (formData) {
625
+ body = formData;
626
+ } else if (options.body !== void 0) {
627
+ headers["Content-Type"] = "application/json";
628
+ body = JSON.stringify(options.body);
629
+ }
630
+ let response;
631
+ try {
632
+ response = await this.fetchImpl(url, {
633
+ method,
634
+ headers,
635
+ body,
636
+ signal: controller.signal
171
637
  });
638
+ } catch (cause) {
639
+ if (options.signal?.aborted) {
640
+ throw new KycirisError("The request was aborted.", {
641
+ code: "ABORTED",
642
+ cause
643
+ });
644
+ }
645
+ if (controller.signal.aborted) {
646
+ throw new KycirisError(
647
+ `The request to ${options.path} timed out after ${timeoutMs}ms.`,
648
+ { code: "TIMEOUT", retryable: true, cause }
649
+ );
650
+ }
651
+ throw new KycirisError(
652
+ `The request to ${options.path} could not reach the gateway. Check connectivity, the baseUrl, and (in a browser) that this origin is in the gateway's ALLOWED_ORIGINS.`,
653
+ { code: "NETWORK_ERROR", retryable: true, cause }
654
+ );
172
655
  }
173
- const response = await this.client.post("/verification/upload/selfie", formData, {
174
- headers: { "Content-Type": "multipart/form-data" }
175
- });
176
- this.emitEvent({
177
- type: "statusChanged",
178
- status: "PROCESSING"
179
- });
180
- return response.data;
181
- } catch (error) {
182
- console.log("Upload selfie error:", error.response?.data || error.message);
183
- const message = error.response?.data?.message || error.message || "Failed to upload selfie";
184
- throw new KYCSdkError(message, "SELFIE_UPLOAD_FAILED", error.response?.status);
656
+ const payload = await readBody(response);
657
+ if (!response.ok) {
658
+ throw toApiError(response, payload, options.path);
659
+ }
660
+ if (response.status === 204 || payload === void 0) {
661
+ return {
662
+ data: void 0,
663
+ meta: {
664
+ requestId: response.headers.get("x-request-id") ?? "",
665
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
666
+ version: ""
667
+ }
668
+ };
669
+ }
670
+ return unwrapEnvelope(payload, options.path);
671
+ } finally {
672
+ clearTimeout(timer);
673
+ options.signal?.removeEventListener("abort", onCallerAbort);
185
674
  }
186
675
  }
187
676
  /**
188
- * Uploads a document image (front or back)
189
- * @param params Parameters including verificationId, type, imageData, and optional mimeType
190
- * @returns Upload result with status
677
+ * Streams a response body instead of parsing it — the media download
678
+ * endpoint returns an image, not an envelope.
191
679
  */
192
- async uploadDocument(params) {
680
+ async requestRaw(options) {
681
+ if (options.signal?.aborted) {
682
+ throw new KycirisError("The request was aborted.", { code: "ABORTED" });
683
+ }
684
+ const url = this.buildUrl(options.path, options.query);
685
+ const controller = new AbortController();
686
+ const timeoutMs = options.timeoutMs ?? this.timeoutMs;
687
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
688
+ const onCallerAbort = () => controller.abort();
689
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
193
690
  try {
194
- const formData = new FormData();
195
- formData.append("verificationId", params.verificationId);
196
- formData.append("type", params.type);
197
- const mimeType = params.mimeType || "image/jpeg";
198
- const imageData = params.imageData;
199
- if (imageData.startsWith("file://")) {
200
- formData.append("file", {
201
- uri: imageData,
202
- type: mimeType,
203
- name: `${params.type}.jpg`
204
- });
205
- } else if (imageData.startsWith("data:")) {
206
- formData.append("file", {
207
- uri: imageData,
208
- type: mimeType,
209
- name: `${params.type}.jpg`
210
- });
211
- } else {
212
- formData.append("file", {
213
- uri: `data:${mimeType};base64,${imageData}`,
214
- type: mimeType,
215
- name: `${params.type}.jpg`
691
+ const headers = {
692
+ ...this.extraHeaders,
693
+ ...await this.authHeaders(options.auth ?? "any")
694
+ };
695
+ let response;
696
+ try {
697
+ response = await this.fetchImpl(url, {
698
+ method: options.method ?? "GET",
699
+ headers,
700
+ signal: controller.signal
216
701
  });
702
+ } catch (cause) {
703
+ if (options.signal?.aborted) {
704
+ throw new KycirisError("The request was aborted.", { code: "ABORTED", cause });
705
+ }
706
+ if (controller.signal.aborted) {
707
+ throw new KycirisError(
708
+ `The request to ${options.path} timed out after ${timeoutMs}ms.`,
709
+ { code: "TIMEOUT", retryable: true, cause }
710
+ );
711
+ }
712
+ throw new KycirisError(
713
+ `The request to ${options.path} could not reach the gateway.`,
714
+ { code: "NETWORK_ERROR", retryable: true, cause }
715
+ );
217
716
  }
218
- const response = await this.client.post("/verification/upload/document", formData, {
219
- headers: { "Content-Type": "multipart/form-data" }
220
- });
221
- this.emitEvent({
222
- type: "statusChanged",
223
- status: "PROCESSING"
224
- });
225
- return response.data;
226
- } catch (error) {
227
- console.log("Upload document error:", error.response?.data || error.message);
228
- const message = error.response?.data?.message || error.message || "Failed to upload document";
229
- throw new KYCSdkError(message, "DOCUMENT_UPLOAD_FAILED", error.response?.status);
717
+ if (!response.ok) {
718
+ throw toApiError(response, await readBody(response), options.path);
719
+ }
720
+ return response;
721
+ } finally {
722
+ clearTimeout(timer);
723
+ options.signal?.removeEventListener("abort", onCallerAbort);
230
724
  }
231
725
  }
232
- /**
233
- * Gets the current status of a verification session
234
- * @param verificationId The verification ID to check
235
- * @returns Current verification status including OCR data and face match score
236
- */
237
- async getStatus(verificationId) {
726
+ };
727
+ async function readBody(response) {
728
+ const contentType = response.headers.get("content-type") ?? "";
729
+ try {
730
+ if (contentType.includes("application/json")) {
731
+ return await response.json();
732
+ }
733
+ const text = await response.text();
734
+ if (!text) return void 0;
238
735
  try {
239
- const response = await this.client.get(`/verification/status/${verificationId}`);
240
- this.emitEvent({
241
- type: "statusChanged",
242
- status: response.data.status
243
- });
244
- return response.data;
245
- } catch (error) {
246
- const message = error.response?.data?.message || error.message || "Failed to get verification status";
247
- throw new KYCSdkError(message, "STATUS_CHECK_FAILED", error.response?.status);
736
+ return JSON.parse(text);
737
+ } catch {
738
+ return text;
739
+ }
740
+ } catch {
741
+ return void 0;
742
+ }
743
+ }
744
+ function toApiError(response, payload, path) {
745
+ const { code, message, details } = readErrorEnvelope(payload);
746
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
747
+ const fallback = response.status === 401 ? "The credential was rejected. Check the API key, or mint a fresh verification token \u2014 they expire." : response.status === 403 ? "The gateway refused this request." : `The gateway returned ${response.status} for ${path}.`;
748
+ return new KycirisError(message || fallback, {
749
+ code: code ?? statusFallbackCode(response.status),
750
+ status: response.status,
751
+ requestId: readRequestId(payload) ?? response.headers.get("x-request-id") ?? void 0,
752
+ details: retryAfterMs !== void 0 ? { ...typeof details === "object" && details ? details : { details }, retryAfterMs } : details,
753
+ retryable: isRetryableStatus(response.status)
754
+ });
755
+ }
756
+ function statusFallbackCode(status) {
757
+ switch (status) {
758
+ case 400:
759
+ case 422:
760
+ return "VALIDATION_ERROR";
761
+ case 401:
762
+ return "UNAUTHORIZED";
763
+ case 403:
764
+ return "FORBIDDEN";
765
+ case 404:
766
+ return "NOT_FOUND";
767
+ case 409:
768
+ return "RESOURCE_ALREADY_EXISTS";
769
+ case 429:
770
+ return "RATE_LIMIT_EXCEEDED";
771
+ default:
772
+ return "INTERNAL_SERVER_ERROR";
773
+ }
774
+ }
775
+
776
+ // src/resources/analytics.ts
777
+ var AnalyticsResource = class {
778
+ constructor(http) {
779
+ this.http = http;
780
+ }
781
+ async get(options = {}) {
782
+ const result = await this.http.request({
783
+ path: "/analytics",
784
+ auth: "api-key",
785
+ signal: options.signal
786
+ });
787
+ return result.data;
788
+ }
789
+ };
790
+
791
+ // src/poll.ts
792
+ var DEFAULT_INTERVAL_MS = 3e3;
793
+ var DEFAULT_TIMEOUT_MS2 = 12e4;
794
+ function abortError() {
795
+ return new KycirisError("Polling was aborted.", { code: "ABORTED" });
796
+ }
797
+ async function pollUntil(fetchOnce, isDone, options = {}, description = "the operation") {
798
+ const intervalMs = Math.max(0, options.intervalMs ?? DEFAULT_INTERVAL_MS);
799
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
800
+ const deadline = Date.now() + timeoutMs;
801
+ for (; ; ) {
802
+ if (options.signal?.aborted) throw abortError();
803
+ const value = await fetchOnce(options.signal);
804
+ options.onUpdate?.(value);
805
+ if (isDone(value)) return value;
806
+ const remaining = deadline - Date.now();
807
+ if (remaining <= 0) {
808
+ throw new KycirisError(
809
+ `Timed out after ${timeoutMs}ms waiting for ${description} to finish. It may still complete \u2014 read the status again later, or subscribe to the webhook instead of polling.`,
810
+ { code: "POLL_TIMEOUT", details: { lastValue: value } }
811
+ );
812
+ }
813
+ await delay(Math.min(intervalMs, remaining), options.signal);
814
+ }
815
+ }
816
+ function delay(ms, signal) {
817
+ return new Promise((resolve, reject) => {
818
+ if (signal?.aborted) {
819
+ reject(abortError());
820
+ return;
248
821
  }
822
+ const timer = setTimeout(() => {
823
+ signal?.removeEventListener("abort", onAbort);
824
+ resolve();
825
+ }, ms);
826
+ function onAbort() {
827
+ clearTimeout(timer);
828
+ reject(abortError());
829
+ }
830
+ signal?.addEventListener("abort", onAbort, { once: true });
831
+ });
832
+ }
833
+
834
+ // src/resources/identities.ts
835
+ var IdentitiesResource = class {
836
+ constructor(http) {
837
+ this.http = http;
838
+ }
839
+ /** Registers an identity independently of any verification. */
840
+ async create(fields, options = {}) {
841
+ const result = await this.http.request({
842
+ method: "POST",
843
+ path: "/identities",
844
+ body: fields,
845
+ auth: "api-key",
846
+ signal: options.signal
847
+ });
848
+ return result.data;
849
+ }
850
+ /** A page of identity summaries. Read one identity for the full record. */
851
+ async list(params = {}, options = {}) {
852
+ const result = await this.http.request({
853
+ path: "/identities",
854
+ query: { ...params },
855
+ auth: "api-key",
856
+ signal: options.signal
857
+ });
858
+ return toPage(result);
859
+ }
860
+ /** The full record, including its verification history. */
861
+ async get(id, options = {}) {
862
+ const result = await this.http.request({
863
+ path: `/identities/${encodeURIComponent(id)}`,
864
+ auth: "api-key",
865
+ signal: options.signal
866
+ });
867
+ return result.data;
868
+ }
869
+ /** This identity's verifications, without embeddings or storage paths. */
870
+ async verifications(id, options = {}) {
871
+ const result = await this.http.request({
872
+ path: `/identities/${encodeURIComponent(id)}/verifications`,
873
+ auth: "api-key",
874
+ signal: options.signal
875
+ });
876
+ return result.data ?? [];
877
+ }
878
+ async update(id, fields, options = {}) {
879
+ const result = await this.http.request({
880
+ method: "PUT",
881
+ path: `/identities/${encodeURIComponent(id)}`,
882
+ body: fields,
883
+ auth: "api-key",
884
+ signal: options.signal
885
+ });
886
+ return result.data;
887
+ }
888
+ /** Deletes the identity, its verifications, and the stored images. */
889
+ async delete(id, options = {}) {
890
+ await this.http.request({
891
+ method: "DELETE",
892
+ path: `/identities/${encodeURIComponent(id)}`,
893
+ auth: "api-key",
894
+ signal: options.signal
895
+ });
249
896
  }
250
897
  /**
251
- * Fetches an identity along with all of its verification records.
252
- * @param identityId The identity ID to fetch
253
- * @returns The identity, including the verifications array with upload paths
898
+ * Groups identities that share a value, to find duplicate registrations.
899
+ *
900
+ * Passing one field returns a single grouping; passing several returns one
901
+ * per field, which is why the return type is a union.
254
902
  */
255
- async getIdentity(identityId) {
256
- try {
257
- const response = await this.client.get(`/identities/${identityId}`);
258
- return response.data;
259
- } catch (error) {
260
- const message = error.response?.data?.message || error.message || "Failed to get identity";
261
- throw new KYCSdkError(message, "IDENTITY_FETCH_FAILED", error.response?.status);
262
- }
903
+ async findDuplicates(params = {}, options = {}) {
904
+ const result = await this.http.request({
905
+ path: "/identities/duplicates",
906
+ query: { field: params.field, id: params.id },
907
+ auth: "api-key",
908
+ signal: options.signal
909
+ });
910
+ return result.data;
911
+ }
912
+ /**
913
+ * Folds duplicates into a primary identity, moving their verifications
914
+ * across and deleting them. Requires the admin role on a user token; an API
915
+ * key is a project credential and carries no role.
916
+ */
917
+ async merge(id, duplicateIds, options = {}) {
918
+ const result = await this.http.request({
919
+ method: "POST",
920
+ path: "/identities/merge",
921
+ body: { id, duplicateIds },
922
+ auth: "api-key",
923
+ signal: options.signal
924
+ });
925
+ return result.data;
926
+ }
927
+ /**
928
+ * Compares a fresh photo against the face already on file for an identity —
929
+ * a re-authentication check, separate from the verification flow.
930
+ *
931
+ * Asynchronous: the returned check starts PENDING. Poll it with
932
+ * `faceCheckStatus`, or use `waitForFaceCheck`.
933
+ */
934
+ async faceMatch(id, file, options = {}) {
935
+ const result = await this.http.request({
936
+ method: "POST",
937
+ path: `/identities/${encodeURIComponent(id)}/face-match`,
938
+ form: { file },
939
+ fileFields: ["file"],
940
+ auth: "api-key",
941
+ signal: options.signal,
942
+ timeoutMs: options.timeoutMs
943
+ });
944
+ return result.data;
945
+ }
946
+ /** The result of a face-match check started with `faceMatch`. */
947
+ async faceCheckStatus(id, faceCheckId, options = {}) {
948
+ const result = await this.http.request({
949
+ path: `/identities/${encodeURIComponent(id)}/face-match/status/${encodeURIComponent(faceCheckId)}`,
950
+ auth: "api-key",
951
+ signal: options.signal
952
+ });
953
+ return result.data;
954
+ }
955
+ /** Polls a face-match check until it is APPROVED or REJECTED. */
956
+ async waitForFaceCheck(id, faceCheckId, options = {}) {
957
+ return pollUntil(
958
+ (signal) => this.faceCheckStatus(id, faceCheckId, { signal }),
959
+ (check) => check.status === "APPROVED" || check.status === "REJECTED",
960
+ options,
961
+ `face check ${faceCheckId}`
962
+ );
963
+ }
964
+ };
965
+
966
+ // src/resources/levels.ts
967
+ var LevelsResource = class {
968
+ constructor(http) {
969
+ this.http = http;
970
+ }
971
+ async create(params, options = {}) {
972
+ const result = await this.http.request({
973
+ method: "POST",
974
+ path: "/verification-levels",
975
+ body: params,
976
+ auth: "api-key",
977
+ signal: options.signal
978
+ });
979
+ return result.data;
980
+ }
981
+ async list(options = {}) {
982
+ const result = await this.http.request({
983
+ path: "/verification-levels",
984
+ auth: "api-key",
985
+ signal: options.signal
986
+ });
987
+ return result.data ?? [];
988
+ }
989
+ async get(id, options = {}) {
990
+ const result = await this.http.request({
991
+ path: `/verification-levels/${encodeURIComponent(id)}`,
992
+ auth: "api-key",
993
+ signal: options.signal
994
+ });
995
+ return result.data;
996
+ }
997
+ async update(id, params, options = {}) {
998
+ const result = await this.http.request({
999
+ method: "PUT",
1000
+ path: `/verification-levels/${encodeURIComponent(id)}`,
1001
+ body: params,
1002
+ auth: "api-key",
1003
+ signal: options.signal
1004
+ });
1005
+ return result.data;
1006
+ }
1007
+ async delete(id, options = {}) {
1008
+ await this.http.request({
1009
+ method: "DELETE",
1010
+ path: `/verification-levels/${encodeURIComponent(id)}`,
1011
+ auth: "api-key",
1012
+ signal: options.signal
1013
+ });
1014
+ }
1015
+ };
1016
+
1017
+ // src/resources/media.ts
1018
+ var MediaResourceApi = class {
1019
+ constructor(http) {
1020
+ this.http = http;
263
1021
  }
264
1022
  /**
265
- * Returns the required steps for a given document type, in collection order.
266
- * ID cards require both sides; driving licenses are single-sided.
1023
+ * Issues a single-use, 60-second token for one file of one verification.
1024
+ *
1025
+ * Useful on its own when the bytes should be fetched somewhere else — hand
1026
+ * the token to a browser and let it call the download URL directly, without
1027
+ * the API key ever leaving your server.
1028
+ */
1029
+ async createDownloadToken(verificationId, resource, options = {}) {
1030
+ const result = await this.http.request({
1031
+ method: "POST",
1032
+ path: `/media/token/${encodeURIComponent(verificationId)}`,
1033
+ body: { resource },
1034
+ auth: "api-key",
1035
+ signal: options.signal
1036
+ });
1037
+ return result.data;
1038
+ }
1039
+ /**
1040
+ * The URL that exchanges a download token for the file.
1041
+ *
1042
+ * Treat it as a credential: the token is the whole authorization, so the URL
1043
+ * grants the image to whoever holds it, once.
267
1044
  */
268
- getRequiredSteps(documentType) {
269
- if (documentType === "DRIVING_LICENSE") {
270
- return ["document_front", "selfie"];
1045
+ downloadUrl(token) {
1046
+ return this.http.buildUrl("/media/download", { token });
1047
+ }
1048
+ /**
1049
+ * Fetches a file, taking care of both steps.
1050
+ *
1051
+ * Returns the raw Response so a caller can stream it (to disk, or straight
1052
+ * into an HTTP response) instead of holding an identity document in memory.
1053
+ */
1054
+ async download(verificationId, resource, options = {}) {
1055
+ const { token } = await this.createDownloadToken(verificationId, resource, options);
1056
+ return this.http.requestRaw({
1057
+ path: "/media/download",
1058
+ query: { token },
1059
+ // The single-use token *is* the credential here; sending the API key as
1060
+ // well would put it on a URL-addressed request for no benefit.
1061
+ auth: "none",
1062
+ signal: options.signal
1063
+ });
1064
+ }
1065
+ /** Fetches a file and buffers it. Convenient; holds the image in memory. */
1066
+ async downloadBytes(verificationId, resource, options = {}) {
1067
+ const response = await this.download(verificationId, resource, options);
1068
+ return new Uint8Array(await response.arrayBuffer());
1069
+ }
1070
+ };
1071
+
1072
+ // src/types.ts
1073
+ var DOCUMENT_CATALOG = [
1074
+ { country: "AO", documentType: "ID_CARD" },
1075
+ { country: "AO", documentType: "DRIVING_LICENSE" },
1076
+ { country: "MZ", documentType: "ID_CARD" }
1077
+ ];
1078
+ function isSupportedDocumentPair(country, documentType) {
1079
+ return DOCUMENT_CATALOG.some(
1080
+ (entry) => entry.country === country && entry.documentType === documentType
1081
+ );
1082
+ }
1083
+ function supportedCountries() {
1084
+ return Array.from(new Set(DOCUMENT_CATALOG.map((entry) => entry.country)));
1085
+ }
1086
+ function supportedDocumentTypes(country) {
1087
+ return DOCUMENT_CATALOG.filter((entry) => entry.country === country).map(
1088
+ (entry) => entry.documentType
1089
+ );
1090
+ }
1091
+ var VERIFICATION_STATUSES = [
1092
+ "PENDING",
1093
+ "PROCESSING",
1094
+ "REVIEW",
1095
+ "APPROVED",
1096
+ "REJECTED"
1097
+ ];
1098
+ var VERIFICATION_OUTCOMES = [
1099
+ /** Passed. */
1100
+ "APPROVED",
1101
+ /** A genuine business decision. Retrying cannot change it. */
1102
+ "REJECTED",
1103
+ /** The pipeline broke. Not the user's fault; retry once fixed. */
1104
+ "FAILED_TECHNICAL",
1105
+ /** Could be either — a human should look at the images. */
1106
+ "NEEDS_REVIEW"
1107
+ ];
1108
+ var TERMINAL_VERIFICATION_STATUSES = ["APPROVED", "REJECTED"];
1109
+ function isTerminalStatus(status) {
1110
+ return TERMINAL_VERIFICATION_STATUSES.includes(status);
1111
+ }
1112
+ var PIPELINE_EVENT_KINDS = [
1113
+ /** The verification was created — the start of every timeline. */
1114
+ "CREATED",
1115
+ /** The status moved; `fromStatus`/`toStatus` say where. */
1116
+ "STATUS_CHANGED",
1117
+ /**
1118
+ * The pipeline was re-run from the uploaded files, clearing what had been
1119
+ * extracted. Not a STATUS_CHANGED: a re-run of an already-PROCESSING
1120
+ * verification would otherwise read "PROCESSING -> PROCESSING", which says
1121
+ * nothing while hiding the destructive part.
1122
+ */
1123
+ "REPROCESSED",
1124
+ /** A queue message for it exhausted every retry and was stored. */
1125
+ "JOB_PARKED",
1126
+ /** A parked message was handed back to its handler. */
1127
+ "JOB_RESENT",
1128
+ /** A parked message was abandoned. Reversible — a resend may follow. */
1129
+ "JOB_DISCARDED"
1130
+ ];
1131
+ var DUPLICATE_FIELDS = [
1132
+ "fullName",
1133
+ "idNumber",
1134
+ "mrz",
1135
+ "gender",
1136
+ "country",
1137
+ "birthDate"
1138
+ ];
1139
+ var WEBHOOK_EVENT_TYPES = [
1140
+ "VERIFICATION_STARTED",
1141
+ "VERIFICATION_COMPLETED",
1142
+ "VERIFICATION_APPROVED",
1143
+ "VERIFICATION_REJECTED",
1144
+ "VERIFICATION_REVIEW_REQUIRED",
1145
+ "OCR_COMPLETED",
1146
+ "OCR_FAILED"
1147
+ ];
1148
+ var ALL_WEBHOOK_EVENTS = "*";
1149
+
1150
+ // src/resources/ocr.ts
1151
+ var OcrResource = class {
1152
+ constructor(http) {
1153
+ this.http = http;
1154
+ }
1155
+ /**
1156
+ * Queues a document for extraction.
1157
+ *
1158
+ * Returns as soon as the job is accepted; the fields arrive later, via
1159
+ * `getStatus` or `waitForResult`.
1160
+ */
1161
+ async run(file, params, options = {}) {
1162
+ if (!isSupportedDocumentPair(params.country, params.documentType)) {
1163
+ const forCountry = supportedDocumentTypes(params.country);
1164
+ throw new KycirisError(
1165
+ forCountry.length ? `${params.country} supports ${forCountry.join(", ")}, not ${params.documentType}.` : `No documents are supported for country "${params.country}".`,
1166
+ { code: "VALIDATION_ERROR" }
1167
+ );
271
1168
  }
272
- return ["document_front", "document_back", "selfie"];
1169
+ const result = await this.http.request({
1170
+ method: "POST",
1171
+ path: "/ocr",
1172
+ // The gateway names this field `document`, not `file`.
1173
+ form: {
1174
+ document: file,
1175
+ documentType: params.documentType,
1176
+ country: params.country,
1177
+ externalId: params.externalId
1178
+ },
1179
+ fileFields: ["document"],
1180
+ signal: options.signal,
1181
+ timeoutMs: options.timeoutMs
1182
+ });
1183
+ return result.data;
1184
+ }
1185
+ /** Status of a queued job, with the extracted fields once COMPLETED. */
1186
+ async getStatus(ocrId, options = {}) {
1187
+ const result = await this.http.request({
1188
+ path: `/ocr/${encodeURIComponent(ocrId)}/status`,
1189
+ signal: options.signal
1190
+ });
1191
+ return result.data;
273
1192
  }
1193
+ /** Polls until the job is COMPLETED or FAILED. */
1194
+ async waitForResult(ocrId, options = {}) {
1195
+ return pollUntil(
1196
+ (signal) => this.getStatus(ocrId, { signal }),
1197
+ (process) => process.status === "COMPLETED" || process.status === "FAILED",
1198
+ options,
1199
+ `OCR job ${ocrId}`
1200
+ );
1201
+ }
1202
+ };
1203
+
1204
+ // src/resources/verifications.ts
1205
+ var DOCUMENT_STEPS = ["document_front", "document_back"];
1206
+ var STEP_MEDIA = {
1207
+ document_front: "document-front",
1208
+ document_back: "document-back",
1209
+ selfie: "selfie"
1210
+ };
1211
+ var VerificationsResource = class {
1212
+ constructor(http, session) {
1213
+ this.http = http;
1214
+ this.session = session;
1215
+ }
1216
+ // -------------------------------------------------------------------------
1217
+ // Credentials
1218
+ // -------------------------------------------------------------------------
274
1219
  /**
275
- * Computes which steps a user has already completed and which are still missing
276
- * for a verification, so a paused flow can be resumed without re-uploading.
1220
+ * Mints a verification token for one end user.
1221
+ *
1222
+ * Call this on **your server**, with the project API key, and hand the token
1223
+ * to the client app. The token authorizes exactly the flow below (start,
1224
+ * upload, status) for the given `externalId` and nothing else, which is what
1225
+ * keeps the project-wide API key out of a bundle an end user can read.
1226
+ *
1227
+ * The gateway owns the lifetime (VERIFICATION_TOKEN_TTL, 8h by default) — a
1228
+ * client cannot ask for a longer one.
277
1229
  *
278
- * Both arguments are optional: when omitted they fall back to the persisted
279
- * session (see the storage adapter). Pass a verificationId to target a specific
280
- * verification; otherwise the most recently updated verification is used.
1230
+ * @param externalId Your own reference for the end user
1231
+ */
1232
+ async createToken(externalId, options = {}) {
1233
+ const result = await this.http.request({
1234
+ method: "POST",
1235
+ path: "/verification/token",
1236
+ body: { externalId },
1237
+ auth: "api-key",
1238
+ signal: options.signal
1239
+ });
1240
+ return result.data;
1241
+ }
1242
+ // -------------------------------------------------------------------------
1243
+ // The end-user flow
1244
+ // -------------------------------------------------------------------------
1245
+ /**
1246
+ * Starts a verification and, when a storage adapter is configured, records
1247
+ * it so an interrupted flow can be resumed.
281
1248
  *
282
- * @param identityId Identity ID owning the verification (defaults to the session)
283
- * @param verificationId Verification ID to target (defaults to the session)
284
- * @returns Progress describing uploaded steps and the steps still required
1249
+ * The (country, documentType) pair and the level name are checked here before
1250
+ * the request goes out: the gateway rejects an unsupported pair with a 422
1251
+ * that names neither what is supported nor why, and a missing level with a
1252
+ * validation error that reads like a typo in your code rather than a policy
1253
+ * you have not created yet.
285
1254
  */
286
- async getVerificationProgress(identityId, verificationId) {
287
- const session = identityId && verificationId ? null : await this.getSession();
288
- const resolvedIdentityId = identityId ?? session?.identityId;
289
- const resolvedVerificationId = verificationId ?? session?.verificationId;
290
- if (!resolvedIdentityId) {
291
- throw new KYCSdkError(
292
- "No identityId provided and no persisted session found",
293
- "SESSION_NOT_FOUND",
294
- 404
1255
+ async start(params, options = {}) {
1256
+ if (typeof params.levelName !== "string" || !params.levelName.trim()) {
1257
+ throw new KycirisError(
1258
+ "levelName is required: a verification runs under a verification level, which says which documents are accepted and whether a selfie is needed. Create one from your backend with client.levels.create().",
1259
+ { code: "VALIDATION_ERROR", details: { levelName: params.levelName ?? null } }
295
1260
  );
296
1261
  }
297
- const identity = await this.getIdentity(resolvedIdentityId);
298
- const verifications = identity.verifications ?? [];
299
- if (verifications.length === 0) {
300
- throw new KYCSdkError("No verifications found for identity", "VERIFICATION_NOT_FOUND", 404);
301
- }
302
- const verification = resolvedVerificationId ? verifications.find((v) => v.id === resolvedVerificationId) : [...verifications].sort(
303
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
304
- )[0];
305
- if (!verification) {
306
- throw new KYCSdkError(
307
- `Verification ${resolvedVerificationId} not found for identity`,
308
- "VERIFICATION_NOT_FOUND",
309
- 404
1262
+ if (!isSupportedDocumentPair(params.country, params.documentType)) {
1263
+ const forCountry = supportedDocumentTypes(params.country);
1264
+ throw new KycirisError(
1265
+ forCountry.length ? `${params.country} supports ${forCountry.join(", ")}, not ${params.documentType}.` : `No documents are supported for country "${params.country}".`,
1266
+ { code: "VALIDATION_ERROR", details: { country: params.country, documentType: params.documentType } }
310
1267
  );
311
1268
  }
312
- const selfieUploaded = !!verification.selfiePath;
313
- const documentFrontUploaded = !!verification.documentFrontPath;
314
- const documentBackUploaded = !!verification.documentBackPath;
1269
+ const result = await this.http.request({
1270
+ method: "POST",
1271
+ path: "/verification/start",
1272
+ body: params,
1273
+ signal: options.signal
1274
+ });
1275
+ const started = result.data;
1276
+ if (started?.verificationId) {
1277
+ await this.session.save({
1278
+ verificationId: started.verificationId,
1279
+ identityId: started.identityId,
1280
+ externalId: params.externalId,
1281
+ documentType: params.documentType,
1282
+ country: params.country,
1283
+ levelName: params.levelName,
1284
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1285
+ });
1286
+ }
1287
+ return started;
1288
+ }
1289
+ /** Uploads the end user's selfie. Triggers face matching once OCR has run. */
1290
+ async uploadSelfie(verificationId, file, options = {}) {
1291
+ const result = await this.http.request({
1292
+ method: "POST",
1293
+ path: "/verification/upload/selfie",
1294
+ form: { verificationId, file },
1295
+ fileFields: ["file"],
1296
+ signal: options.signal,
1297
+ timeoutMs: options.timeoutMs
1298
+ });
1299
+ return result.data;
1300
+ }
1301
+ /**
1302
+ * Uploads one side of the identity document.
1303
+ *
1304
+ * OCR is queued once both sides are present, so a flow that collects only
1305
+ * the front never starts processing.
1306
+ */
1307
+ async uploadDocument(verificationId, side, file, options = {}) {
1308
+ const result = await this.http.request({
1309
+ method: "POST",
1310
+ path: "/verification/upload/document",
1311
+ form: { verificationId, type: side, file },
1312
+ fileFields: ["file"],
1313
+ signal: options.signal,
1314
+ timeoutMs: options.timeoutMs
1315
+ });
1316
+ return result.data;
1317
+ }
1318
+ /** Current status, face-match score and extracted document data. */
1319
+ async getStatus(verificationId, options = {}) {
1320
+ const result = await this.http.request({
1321
+ path: `/verification/status/${encodeURIComponent(verificationId)}`,
1322
+ signal: options.signal
1323
+ });
1324
+ return result.data;
1325
+ }
1326
+ /**
1327
+ * Which files exist for a verification.
1328
+ *
1329
+ * This is the credential-safe way to tell what an end user has already
1330
+ * uploaded: it works with a verification token, whereas the identity record
1331
+ * that used to serve the same purpose is API-key only.
1332
+ */
1333
+ async listMedia(verificationId, options = {}) {
1334
+ const result = await this.http.request({
1335
+ path: `/verification/${encodeURIComponent(verificationId)}/media`,
1336
+ signal: options.signal
1337
+ });
1338
+ return result.data?.available ?? [];
1339
+ }
1340
+ /**
1341
+ * Re-runs OCR and face matching from the files already uploaded.
1342
+ *
1343
+ * The remedy for a verification that failed technically (outcome
1344
+ * FAILED_TECHNICAL) or stalled. Clears the previous extraction and decision,
1345
+ * so it is not a read-only operation.
1346
+ *
1347
+ * Only the document front is required, which is why this also works for a
1348
+ * verification that never got as far as the selfie.
1349
+ */
1350
+ async reanalyze(verificationId, options = {}) {
1351
+ const result = await this.http.request({
1352
+ method: "POST",
1353
+ path: `/verification/reanalyze/${encodeURIComponent(verificationId)}`,
1354
+ signal: options.signal
1355
+ });
1356
+ return result.data;
1357
+ }
1358
+ // -------------------------------------------------------------------------
1359
+ // Resumable flow helpers
1360
+ // -------------------------------------------------------------------------
1361
+ /** The stored session, or null when there is none. */
1362
+ async getSession() {
1363
+ return this.session.read();
1364
+ }
1365
+ /**
1366
+ * Forgets the stored session. Call it once a verification is finished, so
1367
+ * the next one starts clean.
1368
+ */
1369
+ async clearSession() {
1370
+ return this.session.clear();
1371
+ }
1372
+ /**
1373
+ * What the end user has uploaded and what is still missing.
1374
+ *
1375
+ * Derived from the files that actually exist server-side rather than from
1376
+ * anything the client remembers, so it is correct after a reinstall, on a
1377
+ * second device, or when a previous attempt failed mid-upload.
1378
+ */
1379
+ async getProgress(verificationId, options = {}) {
1380
+ const id = await this.resolveVerificationId(verificationId);
1381
+ const [status, available] = await Promise.all([
1382
+ this.getStatus(id, { signal: options.signal }),
1383
+ this.listMedia(id, { signal: options.signal })
1384
+ ]);
1385
+ const present = new Set(available);
1386
+ const required = [
1387
+ ...DOCUMENT_STEPS,
1388
+ ...options.selfieRequired === false ? [] : ["selfie"]
1389
+ ];
315
1390
  const uploaded = {
316
- selfie: selfieUploaded,
317
- document_front: documentFrontUploaded,
318
- document_back: documentBackUploaded
1391
+ document_front: present.has("document-front"),
1392
+ document_back: present.has("document-back"),
1393
+ selfie: present.has("selfie")
319
1394
  };
320
- const missingSteps = this.getRequiredSteps(verification.documentType).filter(
321
- (step) => !uploaded[step]
322
- );
1395
+ const missingSteps = required.filter((step) => !present.has(STEP_MEDIA[step]));
323
1396
  return {
324
- verificationId: verification.id,
325
- documentType: verification.documentType,
326
- status: verification.status,
327
- selfieUploaded,
328
- documentFrontUploaded,
329
- documentBackUploaded,
1397
+ verificationId: id,
1398
+ status: status.status,
1399
+ outcome: status.outcome,
1400
+ uploaded,
330
1401
  missingSteps,
331
1402
  isComplete: missingSteps.length === 0
332
1403
  };
333
1404
  }
334
1405
  /**
335
- * Returns whether every required step of a verification has already been
336
- * uploaded (both document sides where applicable, plus the selfie).
1406
+ * Uploads one step, skipping it when the file is already there.
337
1407
  *
338
- * This is the reliable way to tell a user who has fully submitted their
339
- * documents (verification is now processing on the KYC backend) apart from one
340
- * who paused mid-flow with steps still missing — both look "PENDING" from the
341
- * outside. Derived purely from the identity's verification records.
1408
+ * The call a resumable UI should make: it never re-sends a step the end user
1409
+ * already completed, which matters most on a retry after a dropped
1410
+ * connection, where the upload may well have succeeded.
1411
+ */
1412
+ async uploadStep(params) {
1413
+ const verificationId = await this.resolveVerificationId(params.verificationId);
1414
+ const prepared = await prepareFile(params.file, `${params.step}.jpg`);
1415
+ const progress = await this.getProgress(verificationId, {
1416
+ selfieRequired: params.selfieRequired,
1417
+ signal: params.signal
1418
+ });
1419
+ if (progress.uploaded[params.step]) {
1420
+ return {
1421
+ message: `${params.step} was already uploaded`,
1422
+ status: progress.status,
1423
+ skipped: true
1424
+ };
1425
+ }
1426
+ const uploadOptions = {
1427
+ signal: params.signal,
1428
+ timeoutMs: params.timeoutMs
1429
+ };
1430
+ const result = params.step === "selfie" ? await this.uploadSelfie(verificationId, prepared, uploadOptions) : await this.uploadDocument(
1431
+ verificationId,
1432
+ params.step === "document_front" ? "front" : "back",
1433
+ prepared,
1434
+ uploadOptions
1435
+ );
1436
+ return { ...result, skipped: false };
1437
+ }
1438
+ /**
1439
+ * Polls the status until the verification is decided.
1440
+ *
1441
+ * Stops at APPROVED, REJECTED and — unless `stopOnReview` is false — REVIEW.
1442
+ * For anything longer than a user is willing to watch a spinner, use the
1443
+ * webhooks: polling burns a request every few seconds and still gives up
1444
+ * after `timeoutMs`.
1445
+ */
1446
+ async waitForResult(verificationId, options = {}) {
1447
+ const id = await this.resolveVerificationId(verificationId);
1448
+ const stopOnReview = options.stopOnReview ?? true;
1449
+ return pollUntil(
1450
+ (signal) => this.getStatus(id, { signal }),
1451
+ (status) => isTerminalStatus(status.status) || stopOnReview && status.status === "REVIEW",
1452
+ options,
1453
+ `verification ${id}`
1454
+ );
1455
+ }
1456
+ async resolveVerificationId(explicit) {
1457
+ if (explicit) return explicit;
1458
+ const stored = await this.session.read();
1459
+ if (stored?.verificationId) return stored.verificationId;
1460
+ throw new KycirisError(
1461
+ this.session.enabled ? "No verificationId was given and there is no stored session to resume. Start a verification first." : "No verificationId was given. Pass one, or configure `storage` so the SDK can remember the verification it started.",
1462
+ { code: "SESSION_NOT_FOUND" }
1463
+ );
1464
+ }
1465
+ // -------------------------------------------------------------------------
1466
+ // Review and operations (project API key)
1467
+ // -------------------------------------------------------------------------
1468
+ /**
1469
+ * A page of the project's verifications, newest first.
1470
+ *
1471
+ * Carries no OCR data or storage paths by design — a 20-row page is not the
1472
+ * place for identity-document PII. Read one verification for those.
1473
+ */
1474
+ async list(params = {}, options = {}) {
1475
+ const result = await this.http.request({
1476
+ path: "/verification",
1477
+ query: { ...params },
1478
+ auth: "api-key",
1479
+ signal: options.signal
1480
+ });
1481
+ return toPage(result);
1482
+ }
1483
+ /** Counts per status and outcome, plus what needs an operator's attention. */
1484
+ async summary(options = {}) {
1485
+ const result = await this.http.request({
1486
+ path: "/verification/summary",
1487
+ auth: "api-key",
1488
+ signal: options.signal
1489
+ });
1490
+ return result.data;
1491
+ }
1492
+ /**
1493
+ * A verification's recorded history, oldest first.
342
1494
  *
343
- * Returns `false` (rather than throwing) when the identity has no verification
344
- * records yet, so callers can treat "nothing uploaded" as "documents missing".
1495
+ * The verification row keeps only the last decision, so this is the only
1496
+ * place the sequence survives.
1497
+ */
1498
+ async events(verificationId, options = {}) {
1499
+ const result = await this.http.request({
1500
+ path: `/verification/events/${encodeURIComponent(verificationId)}`,
1501
+ auth: "api-key",
1502
+ signal: options.signal
1503
+ });
1504
+ return result.data ?? [];
1505
+ }
1506
+ /** Moves a PENDING verification to REVIEW so a person can look at it. */
1507
+ async sendToReview(verificationId, options = {}) {
1508
+ const result = await this.http.request({
1509
+ method: "PATCH",
1510
+ path: `/verification/review/${encodeURIComponent(verificationId)}`,
1511
+ auth: "api-key",
1512
+ signal: options.signal
1513
+ });
1514
+ return result.data;
1515
+ }
1516
+ /**
1517
+ * Records a reviewer's decision and fires the matching webhook.
345
1518
  *
346
- * @param identityId Identity ID owning the verification (defaults to the session)
347
- * @param verificationId Verification ID to target (defaults to the most recently updated)
348
- * @returns True when no steps remain to be uploaded
1519
+ * Repeating the decision a verification already has is refused with 409 —
1520
+ * that is the gateway making a second webhook impossible, not a transient
1521
+ * failure. Changing a decision (rejected in error, then approved) is allowed.
349
1522
  */
350
- async hasAllDocuments(identityId, verificationId) {
1523
+ async decide(verificationId, decision, options = {}) {
1524
+ const result = await this.http.request({
1525
+ method: "PATCH",
1526
+ path: `/verification/decision/${encodeURIComponent(verificationId)}`,
1527
+ body: decision,
1528
+ auth: "api-key",
1529
+ signal: options.signal
1530
+ });
1531
+ return result.data;
1532
+ }
1533
+ };
1534
+
1535
+ // src/resources/webhooks.ts
1536
+ var WebhooksResource = class {
1537
+ constructor(http) {
1538
+ this.http = http;
1539
+ }
1540
+ async create(params, options = {}) {
1541
+ const result = await this.http.request({
1542
+ method: "POST",
1543
+ path: "/webhooks",
1544
+ body: params,
1545
+ auth: "api-key",
1546
+ signal: options.signal
1547
+ });
1548
+ return result.data;
1549
+ }
1550
+ async list(options = {}) {
1551
+ const result = await this.http.request({
1552
+ path: "/webhooks",
1553
+ auth: "api-key",
1554
+ signal: options.signal
1555
+ });
1556
+ return result.data ?? [];
1557
+ }
1558
+ async get(id, options = {}) {
1559
+ const result = await this.http.request({
1560
+ path: `/webhooks/${encodeURIComponent(id)}`,
1561
+ auth: "api-key",
1562
+ signal: options.signal
1563
+ });
1564
+ return result.data;
1565
+ }
1566
+ async update(id, params, options = {}) {
1567
+ const result = await this.http.request({
1568
+ method: "PUT",
1569
+ path: `/webhooks/${encodeURIComponent(id)}`,
1570
+ body: params,
1571
+ auth: "api-key",
1572
+ signal: options.signal
1573
+ });
1574
+ return result.data;
1575
+ }
1576
+ async delete(id, options = {}) {
1577
+ await this.http.request({
1578
+ method: "DELETE",
1579
+ path: `/webhooks/${encodeURIComponent(id)}`,
1580
+ auth: "api-key",
1581
+ signal: options.signal
1582
+ });
1583
+ }
1584
+ /**
1585
+ * Delivery attempts, newest first — what was sent, what came back, how many
1586
+ * retries it took. The place to look when an endpoint stops receiving events.
1587
+ */
1588
+ async logs(params = {}, options = {}) {
1589
+ const result = await this.http.request({
1590
+ path: "/webhook-log",
1591
+ query: { ...params },
1592
+ auth: "api-key",
1593
+ signal: options.signal
1594
+ });
1595
+ return toPage(result);
1596
+ }
1597
+ /** One delivery attempt. */
1598
+ async log(id, options = {}) {
1599
+ const result = await this.http.request({
1600
+ path: `/webhook-log/${encodeURIComponent(id)}`,
1601
+ auth: "api-key",
1602
+ signal: options.signal
1603
+ });
1604
+ return result.data;
1605
+ }
1606
+ /**
1607
+ * Sends a recorded delivery again, to the endpoint it originally went to.
1608
+ *
1609
+ * Synchronous: the result says whether it arrived this time. A failure comes
1610
+ * back as `{ success: false, error }` rather than throwing, because the
1611
+ * gateway has already written a FAILED row either way and the caller usually
1612
+ * wants to show the reason beside the delivery it belongs to. A missing
1613
+ * delivery (404) or a deleted endpoint (409) still throw -- those are not
1614
+ * outcomes of the send.
1615
+ *
1616
+ * Not retried: a resend that timed out may well have arrived.
1617
+ */
1618
+ async resendLog(id, options = {}) {
1619
+ const result = await this.http.request({
1620
+ method: "POST",
1621
+ path: `/webhook-log/${encodeURIComponent(id)}/resend`,
1622
+ auth: "api-key",
1623
+ retry: false,
1624
+ signal: options.signal
1625
+ });
1626
+ return result.data;
1627
+ }
1628
+ };
1629
+
1630
+ // src/storage.ts
1631
+ var SESSION_STORAGE_KEY = "kyciris:session:v1";
1632
+ function isStoredSession(value) {
1633
+ return typeof value === "object" && value !== null && typeof value.verificationId === "string";
1634
+ }
1635
+ var SessionStore = class {
1636
+ constructor(storage) {
1637
+ this.storage = storage;
1638
+ }
1639
+ /** True when a storage adapter was configured. */
1640
+ get enabled() {
1641
+ return Boolean(this.storage);
1642
+ }
1643
+ async save(session) {
1644
+ if (!this.storage) return;
351
1645
  try {
352
- const progress = await this.getVerificationProgress(identityId, verificationId);
353
- return progress.isComplete;
354
- } catch (error) {
355
- if (error instanceof KYCSdkError && (error.code === "VERIFICATION_NOT_FOUND" || error.code === "SESSION_NOT_FOUND")) {
356
- return false;
357
- }
358
- throw error;
1646
+ await this.storage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
1647
+ } catch {
359
1648
  }
360
1649
  }
1650
+ async read() {
1651
+ if (!this.storage) return null;
1652
+ try {
1653
+ const raw = await this.storage.getItem(SESSION_STORAGE_KEY);
1654
+ if (!raw) return null;
1655
+ const parsed = JSON.parse(raw);
1656
+ return isStoredSession(parsed) ? parsed : null;
1657
+ } catch {
1658
+ return null;
1659
+ }
1660
+ }
1661
+ async clear() {
1662
+ if (!this.storage) return;
1663
+ try {
1664
+ await this.storage.removeItem(SESSION_STORAGE_KEY);
1665
+ } catch {
1666
+ }
1667
+ }
1668
+ };
1669
+ function createMemoryStorage() {
1670
+ const map = /* @__PURE__ */ new Map();
1671
+ return {
1672
+ getItem: (key) => map.get(key) ?? null,
1673
+ setItem: (key, value) => {
1674
+ map.set(key, value);
1675
+ },
1676
+ removeItem: (key) => {
1677
+ map.delete(key);
1678
+ }
1679
+ };
1680
+ }
1681
+
1682
+ // src/client.ts
1683
+ var KycirisClient = class {
1684
+ constructor(config) {
1685
+ this.http = new HttpClient(config);
1686
+ const session = new SessionStore(config.storage);
1687
+ this.verifications = new VerificationsResource(this.http, session);
1688
+ this.identities = new IdentitiesResource(this.http);
1689
+ this.levels = new LevelsResource(this.http);
1690
+ this.media = new MediaResourceApi(this.http);
1691
+ this.ocr = new OcrResource(this.http);
1692
+ this.webhooks = new WebhooksResource(this.http);
1693
+ this.analytics = new AnalyticsResource(this.http);
1694
+ }
361
1695
  /**
362
- * Uploads a single verification step (selfie, document front, or document back)
363
- * in a resume-aware way: it resolves the verification from the persisted session
364
- * when not given, and skips the upload if that step is already complete.
1696
+ * Whether this client holds the project API key, and so can reach the
1697
+ * server-side endpoints.
365
1698
  *
366
- * @param params The step, image data, and optional verification/identity overrides
367
- * @returns The upload result; `skipped` is true when the step was already uploaded
1699
+ * Worth checking in code that runs in both places, rather than letting a
1700
+ * MISSING_CREDENTIAL error be the way you find out.
368
1701
  */
369
- async uploadStep(params) {
370
- const session = params.verificationId && params.identityId ? null : await this.getSession();
371
- const verificationId = params.verificationId ?? session?.verificationId;
372
- const identityId = params.identityId ?? session?.identityId;
1702
+ get isServerSide() {
1703
+ return this.http.hasApiKey();
1704
+ }
1705
+ };
1706
+ function createKycirisClient(config) {
1707
+ return new KycirisClient(config);
1708
+ }
1709
+
1710
+ // src/flow.ts
1711
+ var EMPTY_UPLOADED = {
1712
+ document_front: false,
1713
+ document_back: false,
1714
+ selfie: false
1715
+ };
1716
+ var VerificationFlow = class {
1717
+ constructor(client, options) {
1718
+ this.client = client;
1719
+ this.options = options;
1720
+ this.state = {
1721
+ phase: "idle",
1722
+ verificationId: null,
1723
+ currentStep: null,
1724
+ missingSteps: [],
1725
+ uploaded: { ...EMPTY_UPLOADED },
1726
+ status: null,
1727
+ outcome: null,
1728
+ rejectionReason: null,
1729
+ error: null,
1730
+ busy: false
1731
+ };
1732
+ this.listeners = /* @__PURE__ */ new Set();
1733
+ this.aborter = null;
1734
+ if (options.onStateChange) this.listeners.add(options.onStateChange);
1735
+ }
1736
+ getState() {
1737
+ return this.state;
1738
+ }
1739
+ /** Observes state changes. Returns an unsubscribe function. */
1740
+ subscribe(listener) {
1741
+ this.listeners.add(listener);
1742
+ return () => {
1743
+ this.listeners.delete(listener);
1744
+ };
1745
+ }
1746
+ /**
1747
+ * Starts a verification, or picks up the one already in storage.
1748
+ *
1749
+ * Resuming is the default because it is almost always what the user wants:
1750
+ * an app killed between the document and the selfie should carry on, not
1751
+ * ask for the document again. Pass `{ fresh: true }` to start over.
1752
+ */
1753
+ async start(options = {}) {
1754
+ return this.guard("starting", async (signal) => {
1755
+ if (options.fresh) {
1756
+ await this.client.verifications.clearSession();
1757
+ } else {
1758
+ const existing = await this.client.verifications.getSession();
1759
+ if (existing?.verificationId) {
1760
+ return this.loadProgress(existing.verificationId, signal);
1761
+ }
1762
+ }
1763
+ const session = await this.client.verifications.start(
1764
+ {
1765
+ country: this.options.country,
1766
+ documentType: this.options.documentType,
1767
+ levelName: this.options.levelName,
1768
+ externalId: this.options.externalId
1769
+ },
1770
+ { signal }
1771
+ );
1772
+ return this.loadProgress(session.verificationId, signal);
1773
+ });
1774
+ }
1775
+ /**
1776
+ * Re-reads what the gateway holds.
1777
+ *
1778
+ * Worth calling when a component remounts, or after an upload whose response
1779
+ * never arrived: the file may well have landed, and this is what notices.
1780
+ */
1781
+ async refresh() {
1782
+ const verificationId = this.state.verificationId;
1783
+ if (!verificationId) return this.start();
1784
+ return this.guard(
1785
+ this.state.phase,
1786
+ (signal) => this.loadProgress(verificationId, signal)
1787
+ );
1788
+ }
1789
+ /**
1790
+ * Uploads an image for the current step and advances.
1791
+ *
1792
+ * A step that turns out to be already uploaded is skipped rather than sent
1793
+ * again, so retrying after a dropped connection cannot duplicate work.
1794
+ */
1795
+ async submit(file) {
1796
+ const { verificationId, currentStep } = this.state;
373
1797
  if (!verificationId) {
374
- throw new KYCSdkError(
375
- "No verificationId provided and no persisted session found",
376
- "SESSION_NOT_FOUND",
377
- 404
1798
+ return this.fail(
1799
+ new KycirisError("The flow has not been started yet.", {
1800
+ code: "SESSION_NOT_FOUND"
1801
+ })
378
1802
  );
379
1803
  }
380
- if (identityId) {
381
- const progress = await this.getVerificationProgress(identityId, verificationId);
382
- if (!progress.missingSteps.includes(params.step)) {
383
- return {
384
- message: `Step ${params.step} already uploaded`,
385
- status: progress.status,
386
- skipped: true
387
- };
388
- }
1804
+ if (!currentStep) {
1805
+ return this.fail(
1806
+ new KycirisError("There is no step waiting for an image.", {
1807
+ code: "VALIDATION_ERROR"
1808
+ })
1809
+ );
389
1810
  }
390
- if (params.step === "selfie") {
391
- return this.uploadSelfie({
1811
+ return this.guard("uploading", async (signal) => {
1812
+ await this.client.verifications.uploadStep({
1813
+ step: currentStep,
1814
+ file,
392
1815
  verificationId,
393
- imageData: params.imageData,
394
- mimeType: params.mimeType
1816
+ selfieRequired: this.options.selfieRequired,
1817
+ signal
395
1818
  });
396
- }
397
- return this.uploadDocument({
398
- verificationId,
399
- type: params.step === "document_front" ? "front" : "back",
400
- imageData: params.imageData,
401
- mimeType: params.mimeType
1819
+ return this.loadProgress(verificationId, signal);
402
1820
  });
403
1821
  }
404
- async faceMatchVerification(params) {
405
- try {
406
- const formData = new FormData();
407
- const mimeType = params.mimeType || "image/jpeg";
408
- const imageData = params.file;
409
- if (imageData.startsWith("file://")) {
410
- formData.append("file", {
411
- uri: imageData,
412
- type: mimeType,
413
- name: `selfie.jpg`
414
- });
415
- } else if (imageData.startsWith("data:")) {
416
- formData.append("file", {
417
- uri: imageData,
418
- type: mimeType,
419
- name: `selfie.jpg`
420
- });
421
- } else {
422
- formData.append("file", {
423
- uri: `data:${mimeType};base64,${imageData}`,
424
- type: mimeType,
425
- name: `selfie.jpg`
426
- });
427
- }
428
- const response = await this.client.post(
429
- `identities/${params.identityId}/face-match`,
430
- formData,
431
- {
432
- headers: { "Content-Type": "multipart/form-data" }
433
- }
1822
+ /**
1823
+ * Waits for the pipeline's decision.
1824
+ *
1825
+ * Called automatically once the last step is uploaded, unless
1826
+ * `waitForDecision` is false. Stops at REVIEW, which is a person's queue and
1827
+ * not something worth holding a spinner for.
1828
+ */
1829
+ async waitForDecision() {
1830
+ const verificationId = this.state.verificationId;
1831
+ if (!verificationId) {
1832
+ return this.fail(
1833
+ new KycirisError("The flow has not been started yet.", {
1834
+ code: "SESSION_NOT_FOUND"
1835
+ })
434
1836
  );
435
- return response.data;
436
- } catch (error) {
437
- console.log("Upload face match error:", error.response?.data || error.message);
438
- const message = error.response?.data?.message || error.message || "Failed to upload face match";
439
- throw new KYCSdkError(message, "FACEMATCH_UPLOAD_FAILED", error.response?.status);
440
- }
441
- }
442
- async faceMatchStatus({
443
- identityId,
444
- faceCheckId,
445
- interval = 3e3,
446
- timeout = 12e4
447
- }) {
448
- const startTime = Date.now();
449
- return new Promise((resolve, reject) => {
450
- const poll = async () => {
451
- try {
452
- const status = (await this.client.get(`identities/${identityId}/face-match/status/${faceCheckId}`)).data;
453
- if (status.status === "APPROVED" || status.status === "REJECTED") {
454
- resolve(status);
455
- return;
456
- }
457
- if (Date.now() - startTime > timeout) {
458
- reject(new KYCSdkError("Polling timeout", "POLLING_TIMEOUT"));
459
- return;
460
- }
461
- setTimeout(poll, interval);
462
- } catch (error) {
463
- reject(error);
464
- }
1837
+ }
1838
+ return this.guard("processing", async (signal) => {
1839
+ const status = await this.client.verifications.waitForResult(verificationId, {
1840
+ signal,
1841
+ timeoutMs: this.options.decisionTimeoutMs
1842
+ });
1843
+ return {
1844
+ phase: "finished",
1845
+ status: status.status,
1846
+ outcome: status.outcome,
1847
+ rejectionReason: status.rejectionReason
465
1848
  };
466
- poll();
467
1849
  });
468
1850
  }
469
1851
  /**
470
- * Polls for verification status until completion or timeout
471
- * @param verificationId The verification ID to check
472
- * @param interval Polling interval in milliseconds (default: 3000)
473
- * @param timeout Maximum time to wait in milliseconds (default: 120000)
474
- * @returns Final verification status when approved or rejected
1852
+ * Cancels whatever is in flight.
1853
+ *
1854
+ * A component's unmount should call this, so a poll does not keep running
1855
+ * against a screen that is gone.
475
1856
  */
476
- async pollStatus(verificationId, interval = 3e3, timeout = 12e4) {
477
- const startTime = Date.now();
478
- return new Promise((resolve, reject) => {
479
- const poll = async () => {
480
- try {
481
- const status = await this.getStatus(verificationId);
482
- if (status.status === "APPROVED" || status.status === "REJECTED") {
483
- resolve(status);
484
- return;
485
- }
486
- if (Date.now() - startTime > timeout) {
487
- reject(new KYCSdkError("Polling timeout", "POLLING_TIMEOUT"));
488
- return;
489
- }
490
- setTimeout(poll, interval);
491
- } catch (error) {
492
- reject(error);
493
- }
494
- };
495
- poll();
1857
+ cancel() {
1858
+ this.aborter?.abort();
1859
+ this.aborter = null;
1860
+ if (this.state.busy) this.patch({ busy: false });
1861
+ }
1862
+ /** Cancels, forgets the stored session, and returns to `idle`. */
1863
+ async reset() {
1864
+ this.cancel();
1865
+ await this.client.verifications.clearSession();
1866
+ this.patch({
1867
+ phase: "idle",
1868
+ verificationId: null,
1869
+ currentStep: null,
1870
+ missingSteps: [],
1871
+ uploaded: { ...EMPTY_UPLOADED },
1872
+ status: null,
1873
+ outcome: null,
1874
+ rejectionReason: null,
1875
+ error: null,
1876
+ busy: false
1877
+ });
1878
+ return this.state;
1879
+ }
1880
+ // -------------------------------------------------------------------------
1881
+ /**
1882
+ * Reads progress and derives the next phase from it.
1883
+ *
1884
+ * Returns a patch rather than applying one, so `guard` stays the single
1885
+ * place that publishes a state change.
1886
+ */
1887
+ async loadProgress(verificationId, signal) {
1888
+ const progress = await this.client.verifications.getProgress(verificationId, {
1889
+ selfieRequired: this.options.selfieRequired,
1890
+ signal
496
1891
  });
1892
+ return {
1893
+ verificationId,
1894
+ currentStep: progress.missingSteps[0] ?? null,
1895
+ missingSteps: progress.missingSteps,
1896
+ uploaded: progress.uploaded,
1897
+ status: progress.status,
1898
+ outcome: progress.outcome,
1899
+ phase: progress.isComplete ? "processing" : "collecting"
1900
+ };
497
1901
  }
498
1902
  /**
499
- * Static factory method to create a KYC client instance
500
- * @param credentials API credentials (apiKey and baseUrl)
501
- * @returns Configured KYCCore instance
1903
+ * Runs one transition: marks the flow busy, applies the resulting patch, and
1904
+ * turns any failure into `state.error` instead of a rejected promise.
1905
+ *
1906
+ * The awkward part it exists to contain is that reaching `processing` should
1907
+ * flow straight on into polling — but only when the caller asked for it, and
1908
+ * only once.
502
1909
  */
503
- static createClient(credentials) {
504
- return new _KYCCore(credentials);
1910
+ async guard(phase, run) {
1911
+ this.aborter?.abort();
1912
+ const aborter = new AbortController();
1913
+ this.aborter = aborter;
1914
+ this.patch({ phase, busy: true, error: null });
1915
+ try {
1916
+ const patch = await run(aborter.signal);
1917
+ this.patch({ ...patch, busy: false });
1918
+ const shouldWait = this.state.phase === "processing" && (this.options.waitForDecision ?? true) && // Not a nested call: waitForDecision itself lands here with phase
1919
+ // 'processing' already set, and would otherwise recurse forever.
1920
+ phase !== "processing";
1921
+ if (shouldWait) return this.waitForDecision();
1922
+ return this.state;
1923
+ } catch (error) {
1924
+ if (isKycirisError(error) && error.code === "ABORTED") {
1925
+ this.patch({ busy: false });
1926
+ return this.state;
1927
+ }
1928
+ return this.fail(error);
1929
+ } finally {
1930
+ if (this.aborter === aborter) this.aborter = null;
1931
+ }
1932
+ }
1933
+ fail(error) {
1934
+ const failure = isKycirisError(error) ? error : new KycirisError(
1935
+ error instanceof Error ? error.message : "The verification failed.",
1936
+ { code: "INTERNAL_SERVER_ERROR", cause: error }
1937
+ );
1938
+ this.patch({ phase: "failed", busy: false, error: failure });
1939
+ return this.state;
1940
+ }
1941
+ patch(patch) {
1942
+ this.state = { ...this.state, ...patch };
1943
+ for (const listener of this.listeners) listener(this.state);
505
1944
  }
506
1945
  };
507
- /** Storage key under which the active verification session is persisted */
508
- _KYCCore.SESSION_KEY = "kyc:session";
509
- var KYCCore = _KYCCore;
510
- function createKYCClient(credentials) {
511
- return KYCCore.createClient(credentials);
512
- }
513
1946
 
514
- exports.KYCCore = KYCCore;
515
- exports.KYCSdkError = KYCSdkError;
516
- exports.createKYCClient = createKYCClient;
1947
+ exports.ALL_WEBHOOK_EVENTS = ALL_WEBHOOK_EVENTS;
1948
+ exports.AnalyticsResource = AnalyticsResource;
1949
+ exports.DOCUMENT_CATALOG = DOCUMENT_CATALOG;
1950
+ exports.DUPLICATE_FIELDS = DUPLICATE_FIELDS;
1951
+ exports.GATEWAY_ERROR_CODES = GATEWAY_ERROR_CODES;
1952
+ exports.HttpClient = HttpClient;
1953
+ exports.IdentitiesResource = IdentitiesResource;
1954
+ exports.KycirisClient = KycirisClient;
1955
+ exports.KycirisError = KycirisError;
1956
+ exports.LevelsResource = LevelsResource;
1957
+ exports.MAX_IMAGE_FILE_SIZE = MAX_IMAGE_FILE_SIZE;
1958
+ exports.MediaResourceApi = MediaResourceApi;
1959
+ exports.OcrResource = OcrResource;
1960
+ exports.PIPELINE_EVENT_KINDS = PIPELINE_EVENT_KINDS;
1961
+ exports.SDK_ERROR_CODES = SDK_ERROR_CODES;
1962
+ exports.SESSION_STORAGE_KEY = SESSION_STORAGE_KEY;
1963
+ exports.SessionStore = SessionStore;
1964
+ exports.TERMINAL_VERIFICATION_STATUSES = TERMINAL_VERIFICATION_STATUSES;
1965
+ exports.VERIFICATION_OUTCOMES = VERIFICATION_OUTCOMES;
1966
+ exports.VERIFICATION_STATUSES = VERIFICATION_STATUSES;
1967
+ exports.VerificationFlow = VerificationFlow;
1968
+ exports.VerificationsResource = VerificationsResource;
1969
+ exports.WEBHOOK_EVENT_TYPES = WEBHOOK_EVENT_TYPES;
1970
+ exports.WebhooksResource = WebhooksResource;
1971
+ exports.createKycirisClient = createKycirisClient;
1972
+ exports.createMemoryStorage = createMemoryStorage;
1973
+ exports.decodeBase64 = decodeBase64;
1974
+ exports.detectImageMimeType = detectImageMimeType;
1975
+ exports.encodeBase64 = encodeBase64;
1976
+ exports.isKycirisError = isKycirisError;
1977
+ exports.isNormalizedFile = isNormalizedFile;
1978
+ exports.isReactNative = isReactNative;
1979
+ exports.isRetryableStatus = isRetryableStatus;
1980
+ exports.isSupportedDocumentPair = isSupportedDocumentPair;
1981
+ exports.isTerminalStatus = isTerminalStatus;
1982
+ exports.isUntrustedClient = isUntrustedClient;
1983
+ exports.normalizeFile = normalizeFile;
1984
+ exports.pollUntil = pollUntil;
1985
+ exports.prepareFile = prepareFile;
1986
+ exports.supportedCountries = supportedCountries;
1987
+ exports.supportedDocumentTypes = supportedDocumentTypes;
1988
+ exports.toPage = toPage;
517
1989
  //# sourceMappingURL=index.js.map
518
1990
  //# sourceMappingURL=index.js.map