@kyciris/core 0.1.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,224 +1,337 @@
1
1
  # @kyciris/core
2
2
 
3
- Core SDK package with UI-agnostic KYC (Know Your Customer) functions. Use this package if you want to build your own UI while leveraging Paytesy's verification services.
4
-
5
- ## Installation
3
+ The UI-agnostic client for the [KYCiris](https://kyciris.com) identity
4
+ verification API. Zero runtime dependencies — it uses the platform's own
5
+ `fetch`, `FormData` and `Blob`, so it runs unchanged on Node 18+, in browsers,
6
+ and in React Native.
6
7
 
7
8
  ```bash
8
9
  pnpm add @kyciris/core
9
10
  ```
10
11
 
11
- ## Usage
12
+ ## Quickstart
13
+
14
+ Point everything at the **sandbox gateway** while you build — the URL is in your
15
+ onboarding details. Sandbox and production behave identically; only the data is
16
+ throwaway. (The interactive `/docs` reference is served by a gateway running in
17
+ development mode only, so it is off on the hosted sandbox — ask us for the
18
+ endpoint reference if you need more than this page.)
19
+
20
+ ### 1. A token route on your backend
21
+
22
+ This is the one piece you have to write, and the reason is worth a sentence:
23
+ your API key can reach every identity in the project, so it stays on your
24
+ server. What the client gets is a token scoped to one end user.
12
25
 
13
- ### Initialization
26
+ ```ts
27
+ // Express
28
+ import express from 'express';
29
+ import { createKycirisClient } from '@kyciris/core';
14
30
 
15
- ```typescript
16
- import { createKYCClient } from '@kyciris/core';
31
+ const kyciris = createKycirisClient({
32
+ baseUrl: process.env.KYCIRIS_BASE_URL, // your sandbox gateway
33
+ apiKey: process.env.KYCIRIS_API_KEY, // server-side only
34
+ });
17
35
 
18
- const kyc = createKYCClient({
19
- apiKey: 'your-api-key',
20
- baseUrl: 'http://localhost:3000', // Your KYC API base URL
36
+ app.post('/api/kyc/token', requireYourOwnAuth, async (req, res) => {
37
+ // `externalId` is YOUR id for this user. Take it from the session, never
38
+ // from the request body -- otherwise anyone can mint a token for anyone.
39
+ const { token } = await kyciris.verifications.createToken(req.user.id);
40
+ res.json({ token });
21
41
  });
22
42
  ```
23
43
 
24
- ### Creating a Verification Token
44
+ ```ts
45
+ // Next.js app router — app/api/kyc/token/route.ts
46
+ import { createKycirisClient } from '@kyciris/core';
25
47
 
26
- Before starting verification, create a token for secure API access:
48
+ const kyciris = createKycirisClient({
49
+ baseUrl: process.env.KYCIRIS_BASE_URL,
50
+ apiKey: process.env.KYCIRIS_API_KEY,
51
+ });
27
52
 
28
- ```typescript
29
- const { token, expiresIn } = await kyc.createVerificationToken(
30
- 'user-123', // externalId
31
- '3' // expiry in hours (optional, default: 3)
32
- );
53
+ export async function POST() {
54
+ const user = await requireYourOwnAuth();
55
+ const { token } = await kyciris.verifications.createToken(user.id);
56
+ return Response.json({ token });
57
+ }
33
58
  ```
34
59
 
35
- ### Starting a Verification
60
+ ### 2. The flow, in your app
36
61
 
37
- ```typescript
38
- const session = await kyc.startVerification({
39
- documentType: 'IDENTITY_CARD', // or 'DRIVING_LICENSE'
40
- country: 'MZ', // Country code: MZ, AO, PT, etc.
41
- identityId: 'optional-existing-identity-id', // optional
42
- externalId: 'optional-external-ref', // optional
43
- });
62
+ ```tsx
63
+ // React (@kyciris/web) — the same shape in React Native (@kyciris/mobile)
64
+ import { KycirisVerification } from '@kyciris/web';
44
65
 
45
- console.log(session.verificationId);
46
- console.log(session.status); // 'PENDING'
66
+ <KycirisVerification
67
+ baseUrl={process.env.NEXT_PUBLIC_KYCIRIS_BASE_URL}
68
+ verificationToken={() =>
69
+ fetch('/api/kyc/token')
70
+ .then((r) => r.json())
71
+ .then((b) => b.token)
72
+ }
73
+ country="AO"
74
+ documentType="ID_CARD"
75
+ levelName="basic-kyc"
76
+ onComplete={(state) => console.log(state.status, state.outcome)}
77
+ />;
47
78
  ```
48
79
 
49
- ### Uploading Documents
80
+ Passing a **function** rather than a string means an expired token is re-fetched
81
+ instead of failing every request from then on.
50
82
 
51
- After starting verification, upload front and back of the document:
83
+ ### 3. Learn the result from a webhook, not from polling
52
84
 
53
- ```typescript
54
- // Upload front of document
55
- const frontResult = await kyc.uploadDocument({
56
- verificationId: session.verificationId,
57
- type: 'front',
58
- imageData: 'base64-encoded-image-data',
59
- });
85
+ The component tells the user what happened. Your backend should hear it from a
86
+ webhook, because a verification sent to a human reviewer can sit for hours:
60
87
 
61
- // Upload back of document
62
- const backResult = await kyc.uploadDocument({
63
- verificationId: session.verificationId,
64
- type: 'back',
65
- imageData: 'base64-encoded-image-data',
88
+ ```ts
89
+ await kyciris.webhooks.create({
90
+ url: 'https://your-app.example.com/hooks/kyciris',
91
+ eventTypes: ['*'], // every event, including ones added later
66
92
  });
67
93
  ```
68
94
 
69
- ### Uploading Selfie
95
+ The URL must be `https` with a public hostname — the gateway resolves it and
96
+ refuses anything private.
97
+
98
+ ### Checklist before you go live
99
+
100
+ - [ ] The API key is only ever read on the server (`process.env`, never
101
+ `NEXT_PUBLIC_*` / `EXPO_PUBLIC_*`). The SDK throws if it reaches a client.
102
+ - [ ] `externalId` comes from your session, not from the request.
103
+ - [ ] Your browser origin is in the gateway's `ALLOWED_ORIGINS`, or every
104
+ request fails CORS before it is even authenticated.
105
+ - [ ] You branch on `outcome`, not `status` (see below).
106
+ - [ ] A webhook endpoint is registered and reachable.
107
+
108
+ ## Two credentials, two places
109
+
110
+ This is the part worth getting right.
111
+
112
+ | | `apiKey` | `verificationToken` |
113
+ | -------- | ---------------------------------------- | ---------------------------------- |
114
+ | Scope | The whole project — every identity in it | One end user's verification |
115
+ | Lives | On your server | In the app or page |
116
+ | Header | `X-API-Key` | `x-verification-token` |
117
+ | Lifetime | Until revoked | 10m by default; the gateway decides |
70
118
 
71
- ```typescript
72
- const selfieResult = await kyc.uploadSelfie({
73
- verificationId: session.verificationId,
74
- imageData: 'base64-encoded-selfie-image',
119
+ Constructing a client with `apiKey` inside a browser or React Native app
120
+ **throws** (`CREDENTIAL_MISUSE`). A key in a shipped bundle is a key the end
121
+ user has.
122
+
123
+ ```ts
124
+ import { createKycirisClient } from '@kyciris/core';
125
+
126
+ // --- On your server
127
+ const server = createKycirisClient({
128
+ baseUrl: 'https://api.kyciris.com',
129
+ apiKey: process.env.KYCIRIS_API_KEY,
130
+ });
131
+
132
+ const { token } = await server.verifications.createToken('user-123');
133
+ // hand `token` to the client
134
+
135
+ // --- In the app
136
+ const client = createKycirisClient({
137
+ baseUrl: 'https://api.kyciris.com',
138
+ verificationToken: token,
75
139
  });
76
140
  ```
77
141
 
78
- ### Checking Status
79
-
80
- ```typescript
81
- // Get current status
82
- const status = await kyc.getStatus(session.verificationId);
83
- console.log(status.status); // 'PENDING' | 'PROCESSING' | 'APPROVED' | 'REJECTED'
84
- console.log(status.ocrData); // Extracted document data
85
- console.log(status.faceMatchScore); // Face match similarity (0-1)
86
-
87
- // Poll until completion
88
- const finalStatus = await kyc.pollStatus(
89
- session.verificationId,
90
- 3000, // poll interval in ms (optional, default: 3000)
91
- 120000 // timeout in ms (optional, default: 120000)
92
- );
142
+ Pass a **function** as `verificationToken` to have it re-fetched when needed,
143
+ rather than failing every request once it expires:
144
+
145
+ ```ts
146
+ createKycirisClient({
147
+ baseUrl,
148
+ verificationToken: () =>
149
+ fetch('/api/kyc/token')
150
+ .then((r) => r.json())
151
+ .then((b) => b.token),
152
+ });
93
153
  ```
94
154
 
95
- ### Listening to Events
155
+ ## Running a verification
156
+
157
+ `levelName` is required and must name an **active verification level** of your
158
+ project: the level says which documents are accepted and whether a selfie is
159
+ needed, and the gateway has no default. Create one once, from your backend:
160
+
161
+ ```ts
162
+ await client.levels.create({
163
+ name: 'basic-kyc',
164
+ definition: {
165
+ requiredSteps: [
166
+ { step: 'IDENTITY_DOCUMENT', required: true, documents: [{ country: 'AO', types: ['ID_CARD'] }] },
167
+ { step: 'SELFIE', required: true },
168
+ ],
169
+ },
170
+ });
171
+ ```
96
172
 
97
- ```typescript
98
- // Subscribe to status changes
99
- const unsubscribe = kyc.onEvent((event) => {
100
- if (event.type === 'statusChanged') {
101
- console.log('Status changed to:', event.status);
102
- } else if (event.type === 'error') {
103
- console.log('Error:', event.error);
104
- }
173
+ ```ts
174
+ const { verificationId } = await client.verifications.start({
175
+ country: 'AO',
176
+ documentType: 'ID_CARD',
177
+ levelName: 'basic-kyc',
105
178
  });
106
179
 
107
- // Unsubscribe when done
108
- unsubscribe();
180
+ await client.verifications.uploadDocument(verificationId, 'front', frontImage);
181
+ await client.verifications.uploadDocument(verificationId, 'back', backImage);
182
+ await client.verifications.uploadSelfie(verificationId, selfieImage);
183
+
184
+ const result = await client.verifications.waitForResult(verificationId);
185
+ console.log(result.status, result.outcome);
109
186
  ```
110
187
 
111
- ## API Reference
188
+ Images can be a `Blob`/`File`, raw bytes (`{ data: Uint8Array }`), a React
189
+ Native `{ uri }`, a `data:` URI, or bare base64. The bytes are checked against
190
+ the same magic-byte test the gateway applies, so a PDF renamed `.jpg` fails
191
+ before the upload rather than after it.
112
192
 
113
- ### Interfaces
193
+ ### Both document sides are required
114
194
 
115
- #### KYCCredentials
195
+ All three catalog documents are two-sided, and the gateway only queues OCR once
196
+ both `documentFrontPath` and `documentBackPath` exist. A flow that collects
197
+ only the front leaves the verification sitting in `PENDING` with nothing
198
+ running.
116
199
 
117
- ```typescript
118
- {
119
- apiKey: string; // Your API key for authentication
120
- baseUrl: string; // Base URL of your KYC API
121
- }
200
+ ### The flow controller
201
+
202
+ `VerificationFlow` is the sequencing above as a state machine — which step is
203
+ next, what a resumed flow skips, when to poll. It is what `@kyciris/mobile` and
204
+ `@kyciris/web` are built on.
205
+
206
+ ```ts
207
+ import { VerificationFlow } from '@kyciris/core';
208
+
209
+ const flow = new VerificationFlow(client, {
210
+ country: 'AO',
211
+ documentType: 'ID_CARD',
212
+ levelName: 'basic-kyc',
213
+ });
214
+ flow.subscribe((state) => render(state));
215
+
216
+ await flow.start(); // or resumes what is in storage
217
+ await flow.submit(image); // uploads state.currentStep, advances
122
218
  ```
123
219
 
124
- #### StartVerificationParams
220
+ Failures land in `state.error` rather than rejecting, so a button handler needs
221
+ no try/catch. `flow.cancel()` stops in-flight work on unmount.
125
222
 
126
- ```typescript
127
- {
128
- documentType: 'IDENTITY_CARD' | 'DRIVING_LICENSE';
129
- country: string; // Country code (e.g., 'MZ', 'AO', 'PT')
130
- identityId?: string; // Optional existing identity ID
131
- externalId?: string; // Optional external reference
132
- }
223
+ ### Resuming an interrupted flow
224
+
225
+ Give the client a `storage` adapter and it remembers which verification is in
226
+ progress:
227
+
228
+ ```ts
229
+ import { createKycirisClient, createMemoryStorage } from '@kyciris/core';
230
+
231
+ const client = createKycirisClient({ baseUrl, verificationToken, storage });
133
232
  ```
134
233
 
135
- #### VerificationStatus
136
-
137
- ```typescript
138
- {
139
- verificationId: string;
140
- status: 'PENDING' | 'PROCESSING' | 'APPROVED' | 'REJECTED';
141
- faceMatchScore?: number; // Similarity score 0-1
142
- ocrData?: {
143
- fullName?: string;
144
- idNumber?: string;
145
- birthDate?: string;
146
- expiryDate?: string;
147
- // ... additional country-specific fields
148
- };
149
- createdAt: string;
150
- updatedAt: string;
151
- }
234
+ Progress itself is never trusted to local state — it is read back from the
235
+ files the gateway actually holds (`GET /v1/verification/:id/media`), so it is
236
+ right after a reinstall, on a second device, and when an upload succeeded but
237
+ its response never arrived.
238
+
239
+ ```ts
240
+ const progress = await client.verifications.getProgress();
241
+ // { missingSteps: ['document_back', 'selfie'], isComplete: false, ... }
242
+
243
+ await client.verifications.uploadStep({ step: 'document_back', file });
244
+ // skipped: true, if it turns out to be there already
152
245
  ```
153
246
 
154
- ## Supported Countries
247
+ Only identifiers are stored. **The verification token is never written to
248
+ storage** — it is a bearer credential, and where it lives is your decision.
155
249
 
156
- - **MZ** (Mozambique)
157
- - **AO** (Angola)
158
- - **PT** (Portugal)
250
+ ## Server-side
159
251
 
160
- ## Error Handling
252
+ With an API key, the client also reaches the review and operations endpoints:
161
253
 
162
- All methods throw `KYCSdkError` with the following properties:
254
+ ```ts
255
+ const page = await server.verifications.list({ outcome: 'NEEDS_REVIEW' });
256
+ const summary = await server.verifications.summary();
257
+ const history = await server.verifications.events(verificationId);
163
258
 
164
- ```typescript
165
- try {
166
- await kyc.startVerification({ ... });
167
- } catch (error) {
168
- if (error instanceof KYCSdkError) {
169
- console.log(error.code); // Error code
170
- console.log(error.statusCode); // HTTP status code
171
- console.log(error.message); // Human-readable message
172
- }
173
- }
259
+ await server.verifications.decide(verificationId, {
260
+ decision: 'REJECTED',
261
+ reason: 'Document photo does not match the selfie.',
262
+ });
174
263
  ```
175
264
 
176
- ## Example
265
+ Plus `identities`, `levels`, `media`, `ocr`, `webhooks` and `analytics`.
177
266
 
178
- ```typescript
179
- import { createKYCClient } from '@kyciris/core';
267
+ Images come out through a two-step, single-use flow — a 60-second token for one
268
+ file, then the bytes:
180
269
 
181
- async function runKYC() {
182
- const kyc = createKYCClient({
183
- apiKey: process.env.KYC_API_KEY,
184
- baseUrl: 'http://localhost:3000',
185
- });
270
+ ```ts
271
+ const response = await server.media.download(verificationId, 'document-front');
272
+ ```
186
273
 
187
- // Start verification
188
- const session = await kyc.startVerification({
189
- documentType: 'IDENTITY_CARD',
190
- country: 'MZ',
191
- externalId: 'user-12345',
192
- });
274
+ ## Status, and what it means
193
275
 
194
- // Listen for events
195
- kyc.onEvent((event) => {
196
- console.log('KYC Event:', event);
197
- });
276
+ `status` is the pipeline's state machine. `outcome` is what a finished
277
+ verification _means_, and it exists because `REJECTED` is written for two
278
+ different events:
198
279
 
199
- // Upload documents (you would get imageData from your UI)
200
- await kyc.uploadDocument({
201
- verificationId: session.verificationId,
202
- type: 'front',
203
- imageData: 'base64-encoded-image...',
204
- });
280
+ | `outcome` | Meaning |
281
+ | ------------------ | ------------------------------------------------------------ |
282
+ | `APPROVED` | Passed. |
283
+ | `REJECTED` | A real decision. Retrying will not change it. |
284
+ | `FAILED_TECHNICAL` | Our pipeline broke. Not the user's fault — `reanalyze()` it. |
285
+ | `NEEDS_REVIEW` | A person should look at the images. |
205
286
 
206
- await kyc.uploadDocument({
207
- verificationId: session.verificationId,
208
- type: 'back',
209
- imageData: 'base64-encoded-image...',
210
- });
287
+ Branch on `outcome`, not on `status`, when deciding what to tell a user.
211
288
 
212
- // Upload selfie
213
- await kyc.uploadSelfie({
214
- verificationId: session.verificationId,
215
- imageData: 'base64-encoded-selfie...',
216
- });
289
+ For anything longer than a spinner, use the webhooks rather than polling: a
290
+ `REVIEW` sits with a human, potentially for hours.
217
291
 
218
- // Wait for completion
219
- const result = await kyc.pollStatus(session.verificationId);
220
- console.log('Verification result:', result.status);
221
- }
292
+ ## Errors
293
+
294
+ Everything thrown is a `KycirisError` with a `code` you can branch on, the
295
+ gateway's `requestId`, and nothing from the request — the headers hold the
296
+ credentials, and an error object is what gets logged whole.
222
297
 
223
- runKYC();
298
+ ```ts
299
+ import { isKycirisError } from '@kyciris/core';
300
+
301
+ try {
302
+ await client.verifications.start({
303
+ country: 'AO',
304
+ documentType: 'ID_CARD',
305
+ levelName: 'basic-kyc',
306
+ });
307
+ } catch (error) {
308
+ if (isKycirisError(error) && error.code === 'UNAUTHORIZED') {
309
+ // the token expired — fetch a new one
310
+ }
311
+ }
224
312
  ```
313
+
314
+ Transient failures (429, 5xx, network) are retried with jittered backoff.
315
+ Uploads are **not**, because a POST that timed out may well have landed; a 429
316
+ is, because the gateway rejected it before doing any work.
317
+
318
+ ## Configuration
319
+
320
+ | Option | Default | |
321
+ | ------------------------------ | ------- | -------------------------------------------------------------------- |
322
+ | `baseUrl` | — | Origin of the gateway. Plaintext `http` to a remote host is refused. |
323
+ | `apiKey` / `verificationToken` | — | One is required. |
324
+ | `apiVersion` | `v1` | URI version segment. |
325
+ | `timeoutMs` | `30000` | Per attempt. |
326
+ | `maxRetries` | `2` | Retries of a failed attempt. |
327
+ | `storage` | — | Enables resuming. |
328
+ | `fetch` | global | A custom implementation, for proxies or tests. |
329
+ | `headers` | — | Added to every request; cannot override the credentials. |
330
+ | `allowInsecureBaseUrl` | `false` | Permits `http` to a private network. |
331
+ | `allowApiKeyInUntrustedClient` | `false` | Only for a trusted first-party console. |
332
+
333
+ ## Supported documents
334
+
335
+ `AO`/`ID_CARD`, `AO`/`DRIVING_LICENSE`, `MZ`/`ID_CARD`. An unsupported pair is
336
+ refused client-side, naming what the country does support, rather than costing
337
+ a round trip.