@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 +273 -160
- package/dist/index.d.mts +1467 -151
- package/dist/index.d.ts +1467 -151
- package/dist/index.js +1951 -258
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1912 -254
- package/dist/index.mjs.map +1 -1
- package/package.json +39 -12
package/README.md
CHANGED
|
@@ -1,224 +1,337 @@
|
|
|
1
1
|
# @kyciris/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
26
|
+
```ts
|
|
27
|
+
// Express
|
|
28
|
+
import express from 'express';
|
|
29
|
+
import { createKycirisClient } from '@kyciris/core';
|
|
14
30
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
44
|
+
```ts
|
|
45
|
+
// Next.js app router — app/api/kyc/token/route.ts
|
|
46
|
+
import { createKycirisClient } from '@kyciris/core';
|
|
25
47
|
|
|
26
|
-
|
|
48
|
+
const kyciris = createKycirisClient({
|
|
49
|
+
baseUrl: process.env.KYCIRIS_BASE_URL,
|
|
50
|
+
apiKey: process.env.KYCIRIS_API_KEY,
|
|
51
|
+
});
|
|
27
52
|
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
###
|
|
60
|
+
### 2. The flow, in your app
|
|
36
61
|
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
### 3. Learn the result from a webhook, not from polling
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
193
|
+
### Both document sides are required
|
|
114
194
|
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
157
|
-
- **AO** (Angola)
|
|
158
|
-
- **PT** (Portugal)
|
|
250
|
+
## Server-side
|
|
159
251
|
|
|
160
|
-
|
|
252
|
+
With an API key, the client also reaches the review and operations endpoints:
|
|
161
253
|
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
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
|
-
|
|
265
|
+
Plus `identities`, `levels`, `media`, `ocr`, `webhooks` and `analytics`.
|
|
177
266
|
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
baseUrl: 'http://localhost:3000',
|
|
185
|
-
});
|
|
270
|
+
```ts
|
|
271
|
+
const response = await server.media.download(verificationId, 'document-front');
|
|
272
|
+
```
|
|
186
273
|
|
|
187
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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.
|