attestid_js_sdk 0.1.1 → 0.1.3
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 +71 -370
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# attestid_js_sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Handles credential management, ID photo quality checks, liveness sequencing, and fraud detection — so your integration is a few lines, not hundreds.
|
|
6
|
-
|
|
7
|
-
---
|
|
3
|
+
JavaScript / TypeScript SDK for the AttestID KYC API: ID capture, photo quality checks, liveness and face match.
|
|
8
4
|
|
|
9
5
|
## Installation
|
|
10
6
|
|
|
@@ -12,25 +8,21 @@ Handles credential management, ID photo quality checks, liveness sequencing, and
|
|
|
12
8
|
npm install attestid_js_sdk
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
React
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Quick start
|
|
11
|
+
React components are available from `attestid_js_sdk/react` and require `react` and `react-dom` 18 or later.
|
|
20
12
|
|
|
21
|
-
|
|
13
|
+
## Setup
|
|
22
14
|
|
|
23
|
-
|
|
15
|
+
Create an API key in the AttestID dashboard and keep it on your backend. The key must never reach the browser; the SDK throws `INSECURE_CONFIG` if `apiKey` is used outside Node.js.
|
|
24
16
|
|
|
25
|
-
|
|
17
|
+
Your backend creates a short-lived session token for each user:
|
|
26
18
|
|
|
27
19
|
```ts
|
|
28
|
-
//
|
|
29
|
-
export async function
|
|
20
|
+
// Backend (Node.js)
|
|
21
|
+
export async function createAttestIDSession(userId: string) {
|
|
30
22
|
const res = await fetch(`${process.env.ATTESTID_API_URL}/v1/kyc/session`, {
|
|
31
23
|
method: 'POST',
|
|
32
24
|
headers: {
|
|
33
|
-
'X-API-Key': process.env.ATTESTID_API_KEY!,
|
|
25
|
+
'X-API-Key': process.env.ATTESTID_API_KEY!,
|
|
34
26
|
'X-Access-Key-Id': process.env.ATTESTID_ACCESS_KEY_ID!,
|
|
35
27
|
'Content-Type': 'application/json',
|
|
36
28
|
},
|
|
@@ -40,138 +32,101 @@ export async function attestidSession(userId: string) {
|
|
|
40
32
|
}
|
|
41
33
|
```
|
|
42
34
|
|
|
35
|
+
The browser client fetches that token from your backend:
|
|
36
|
+
|
|
43
37
|
```ts
|
|
44
|
-
//
|
|
38
|
+
// Browser
|
|
45
39
|
import { AttestID } from 'attestid_js_sdk';
|
|
46
40
|
|
|
47
41
|
const client = new AttestID({
|
|
48
|
-
apiUrl: 'https://<your-attestid-api-host>',
|
|
49
|
-
// Never embed apiKey here — fetch a short-lived sdk_token from your backend instead.
|
|
42
|
+
apiUrl: 'https://<your-attestid-api-host>',
|
|
50
43
|
getSdkToken: (userId) =>
|
|
51
44
|
fetch('/api/attestid-session', {
|
|
52
45
|
method: 'POST',
|
|
53
46
|
headers: { 'Content-Type': 'application/json' },
|
|
54
47
|
body: JSON.stringify({ userId }),
|
|
55
|
-
}).then(r => r.json()),
|
|
48
|
+
}).then((r) => r.json()),
|
|
56
49
|
});
|
|
57
50
|
```
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- **Drop-in:** render `<AttestIDVerifier client={client} />` — consent, camera capture, quality checks, the face check and the result screen in one component. See [Drop-in React component](#drop-in-react-component).
|
|
62
|
-
- **Custom UI:** drive the steps yourself with `useAttestID(client)`, and mount `<AttestIDLivenessCheck />` for the face check. See [React hook](#react-hook).
|
|
63
|
-
|
|
64
|
-
Either way, send `result.attestation` to your backend and verify it there before acting on the result — see [Verify results on your backend](#verify-results-on-your-backend).
|
|
65
|
-
|
|
66
|
-
---
|
|
52
|
+
`apiUrl` must use `https://`.
|
|
67
53
|
|
|
68
|
-
##
|
|
54
|
+
## React component
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
`AttestIDVerifier` runs the full flow: consent, document capture, quality checks, face check and result.
|
|
71
57
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Use that `api_key` (and its paired `access_key_id`) to construct `AttestID` in server mode, as shown above.
|
|
58
|
+
```tsx
|
|
59
|
+
import { AttestIDVerifier } from 'attestid_js_sdk/react';
|
|
60
|
+
import 'attestid_js_sdk/styles.css';
|
|
77
61
|
|
|
78
|
-
|
|
62
|
+
<AttestIDVerifier
|
|
63
|
+
client={client}
|
|
64
|
+
onVerified={(result) => sendToBackend(result.attestation)}
|
|
65
|
+
onError={(error) => console.error(error)}
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
79
68
|
|
|
80
|
-
|
|
69
|
+
| Prop | Type | Description |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `client` | `AttestID` | Configured client |
|
|
72
|
+
| `defaultUserId` | `string` | Pre-fills the user ID |
|
|
73
|
+
| `onVerified` | `(result: KYCResult) => void` | Called when the flow finishes |
|
|
74
|
+
| `onError` | `(error: string) => void` | Called on errors |
|
|
75
|
+
| `mobileHandoffUrl` | `string` | Page opened on the user's phone from desktop. Defaults to the current page |
|
|
76
|
+
| `allowDesktopCapture` | `boolean` | Allow webcam document capture on desktop. Default `true` |
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
on by default where it applies, and it all degrades gracefully against an
|
|
84
|
-
older API.
|
|
78
|
+
## Custom UI
|
|
85
79
|
|
|
86
|
-
|
|
87
|
-
|---|---|---|
|
|
88
|
-
| Safe automatic retries (`Idempotency-Key`) | everywhere | Nothing. Optionally tune `retry` |
|
|
89
|
-
| Device-bound capture signing | browser | Nothing. Opt out with `deviceBinding: false` |
|
|
90
|
-
| Signed results (`result.attestation`) | **your backend** | Call `verifyAttestation()` before trusting `verified` |
|
|
91
|
-
| Signed webhooks + secret rotation | **your backend** | Call `verifyWebhook()` on every delivery |
|
|
92
|
-
| Mutual TLS (optional, per account) | your backend | Pass an mTLS-bound `fetch` |
|
|
80
|
+
Use the `useAttestID` hook to build your own screens. Mount `AttestIDLivenessCheck` while `state` is `'liveness'`.
|
|
93
81
|
|
|
94
|
-
|
|
82
|
+
```tsx
|
|
83
|
+
import { useAttestID, AttestIDLivenessCheck } from 'attestid_js_sdk/react';
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
function Verify() {
|
|
86
|
+
const verification = useAttestID(client);
|
|
87
|
+
const { state, result, startVerification, canRetryLiveness, retryLiveness } = verification;
|
|
99
88
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
- **`5xx` / `429`:** the server stored nothing, so the request is rebuilt with a fresh key, and a fresh device-signing nonce when the request is a photo upload.
|
|
103
|
-
- **Any other `4xx`:** thrown immediately, never retried.
|
|
89
|
+
const submit = (idPhoto: File) =>
|
|
90
|
+
startVerification({ userId, idType: 'national_id', idPhoto, consentGivenAt });
|
|
104
91
|
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
if (state === 'liveness') return <AttestIDLivenessCheck verification={verification} />;
|
|
93
|
+
if (state === 'done') return <Result result={result} onRetry={canRetryLiveness ? retryLiveness : undefined} />;
|
|
94
|
+
// render your document capture UI and call submit(file)
|
|
95
|
+
}
|
|
107
96
|
```
|
|
108
97
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
In browser mode the SDK creates an ECDSA P-256 key with WebCrypto as
|
|
112
|
-
**non-extractable**: script, including this SDK, can sign with it but can
|
|
113
|
-
never read or export it. It's stored in IndexedDB, with an in-memory
|
|
114
|
-
fallback in private mode or webviews. On every `startVerification()` it:
|
|
98
|
+
States: `idle`, `quality_check`, `uploading`, `retrying`, `liveness`, `completing`, `done`, `error`.
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
2. signs `attestid-capture-v1|sha256(exact photo bytes)|user_id|nonce|timestamp` with a fresh single-use nonce, and sends the signature with the upload.
|
|
100
|
+
Supported `idType` values: `national_id`, `passport`, `drivers_licence`.
|
|
118
101
|
|
|
119
|
-
|
|
120
|
-
(inside the signed attestation). A photo replayed or injected straight
|
|
121
|
-
into the API, with a wrong key, a reused nonce or altered bytes, is **held
|
|
122
|
-
for fraud review**.
|
|
102
|
+
## Verify results on your backend
|
|
123
103
|
|
|
124
|
-
|
|
125
|
-
- **What the web can prove:** browsers have no hardware attestation, so web keys reach `key_bound`, never `hardware_attested` (that needs the native SDKs with App Attest / Play Integrity). It proves the photo came through this SDK on the device that holds the key, not that it came from the physical camera.
|
|
126
|
-
- **Server mode:** always off, because a backend process isn't the user's device.
|
|
104
|
+
Always verify `result.attestation` on your backend before acting on a result.
|
|
127
105
|
|
|
128
106
|
```ts
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Verify results on your backend
|
|
107
|
+
import { verifyAttestation } from 'attestid_js_sdk';
|
|
133
108
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
act on a result (open an account, release funds), verify the attestation on
|
|
138
|
-
your backend** against AttestID's public keys:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
// your backend (Node 19+, or any runtime with WebCrypto)
|
|
142
|
-
import { verifyAttestation, SignatureError } from 'attestid_js_sdk';
|
|
143
|
-
|
|
144
|
-
const claims = await verifyAttestation(result.attestation, {
|
|
145
|
-
apiUrl: process.env.ATTESTID_API_URL!, // keys fetched from /.well-known/jwks.json (cached)
|
|
146
|
-
verificationId: expectedVerificationId, // must be the session you started
|
|
147
|
-
companyId: process.env.ATTESTID_COMPANY_ID, // optional
|
|
148
|
-
document: result.document, // optional: proves the ID data wasn't altered
|
|
109
|
+
const claims = await verifyAttestation(attestation, {
|
|
110
|
+
apiUrl: process.env.ATTESTID_API_URL!,
|
|
111
|
+
verificationId,
|
|
149
112
|
});
|
|
113
|
+
|
|
150
114
|
if (!claims.verified) throw new Error('Not verified');
|
|
151
|
-
claims.capture_assurance; // 'hardware_attested' | 'key_bound' | 'none' | 'invalid'
|
|
152
115
|
```
|
|
153
116
|
|
|
154
|
-
|
|
155
|
-
(`alg: none` and HS256 are rejected), the wrong verification or company id,
|
|
156
|
-
or document data that doesn't match `document_sha256`. Signing keys rotate
|
|
157
|
-
on the server; the SDK refetches the key set when it sees a new `kid`.
|
|
117
|
+
## Webhooks
|
|
158
118
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Pass the **raw** request body (before JSON parsing) and the headers:
|
|
119
|
+
Pass the raw request body and headers:
|
|
162
120
|
|
|
163
121
|
```ts
|
|
164
|
-
import express from 'express';
|
|
165
122
|
import { verifyWebhook } from 'attestid_js_sdk';
|
|
166
123
|
|
|
167
124
|
app.post('/webhooks/attestid', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
168
125
|
try {
|
|
169
126
|
const event = await verifyWebhook(req.body, req.headers, {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
apiUrl: process.env.ATTESTID_API_URL, // also verifies the asymmetric AttestID-JWS header
|
|
127
|
+
secrets: [process.env.ATTESTID_WEBHOOK_SECRET!],
|
|
128
|
+
apiUrl: process.env.ATTESTID_API_URL,
|
|
173
129
|
});
|
|
174
|
-
// ...handle event...
|
|
175
130
|
res.sendStatus(200);
|
|
176
131
|
} catch {
|
|
177
132
|
res.sendStatus(400);
|
|
@@ -179,276 +134,22 @@ app.post('/webhooks/attestid', express.raw({ type: 'application/json' }), async
|
|
|
179
134
|
});
|
|
180
135
|
```
|
|
181
136
|
|
|
182
|
-
|
|
183
|
-
- **`apiUrl`** checks `AttestID-JWS`, a signature anyone can verify with AttestID's public key. Unlike the shared HMAC secret, it proves to a third party (an auditor) that AttestID sent it.
|
|
184
|
-
- **Replay protection:** both timestamps must be within `toleranceSeconds` (default 300).
|
|
185
|
-
|
|
186
|
-
### Mutual TLS (server mode)
|
|
187
|
-
|
|
188
|
-
If your account requires a client certificate (dashboard → Client
|
|
189
|
-
certificates), give the SDK a `fetch` that presents it. For example, with
|
|
190
|
-
Node's bundled undici:
|
|
191
|
-
|
|
192
|
-
```ts
|
|
193
|
-
import { Agent, fetch as undiciFetch } from 'undici';
|
|
194
|
-
import { readFileSync } from 'node:fs';
|
|
195
|
-
|
|
196
|
-
const mtls = new Agent({ connect: { cert: readFileSync('client.crt'), key: readFileSync('client.key') } });
|
|
197
|
-
|
|
198
|
-
const client = new AttestID({
|
|
199
|
-
apiUrl: process.env.ATTESTID_API_URL!,
|
|
200
|
-
apiKey: process.env.ATTESTID_API_KEY!,
|
|
201
|
-
accessKeyId: process.env.ATTESTID_ACCESS_KEY_ID!,
|
|
202
|
-
fetch: (url, init) => undiciFetch(url, { ...init, dispatcher: mtls } as never) as unknown as Promise<Response>,
|
|
203
|
-
});
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Generate the key and CSR on your server (`openssl ecparam -name prime256v1 -genkey -noout -out client.key && openssl req -new -key client.key -subj "/CN=attestid-client" -out client.csr`). Your private key never leaves your infrastructure.
|
|
207
|
-
|
|
208
|
-
### Runtime requirements
|
|
209
|
-
|
|
210
|
-
The verification helpers and device signing use WebCrypto
|
|
211
|
-
(`globalThis.crypto.subtle`): any modern browser, **Node.js 19+**, Deno,
|
|
212
|
-
Bun, or edge runtimes. On a runtime without it, `verifyAttestation` /
|
|
213
|
-
`verifyWebhook` throw `CRYPTO_UNAVAILABLE`, and device signing is skipped
|
|
214
|
-
silently.
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
|
|
218
|
-
## Photo quality check
|
|
219
|
-
|
|
220
|
-
Run `checkPhotoQuality` client-side before uploading to give users instant feedback and avoid unnecessary API calls:
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
import { checkPhotoQuality } from 'attestid_js_sdk';
|
|
224
|
-
|
|
225
|
-
const result = await checkPhotoQuality(file, idType);
|
|
226
|
-
// result.ok — true when all checks pass
|
|
227
|
-
// result.issues — array of human-readable strings describing problems
|
|
228
|
-
|
|
229
|
-
if (!result.ok) {
|
|
230
|
-
// Show result.issues to the user before letting them submit
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Checks performed:
|
|
235
|
-
- **File size** — rejects obviously non-image blobs
|
|
236
|
-
- **Blur** — Laplacian variance; flags camera shake or out-of-focus shots
|
|
237
|
-
- **Brightness** — flags underexposed and overexposed images
|
|
238
|
-
- **Document type** — flags a passport uploaded as an ID card (or vice versa) from its shape
|
|
239
|
-
- **Passport MRZ** — the data page must show the machine-readable zone (the two lines of `<<<` text). Rejects the cover, visa pages, a cropped-off MRZ, or glare across it. In a two-page spread the half with the MRZ is treated as the data page, and only its blur is evaluated
|
|
240
|
-
- **ID card / driver's licence text** — the front must show several printed lines in a column (name, date of birth, ID number). Rejects the back of the card, a blank surface, or a photo of something else. No MRZ is needed on cards
|
|
241
|
-
- **Face count** — where the browser supports the Shape Detection API, flags no face or more than one face
|
|
242
|
-
|
|
243
|
-
The MRZ, card-size and text checks look at shapes and layout, not characters: no OCR library is bundled, and together they take about 20-70 ms. Where Chromium's `TextDetector` is available it's used as a second opinion that can only pass a photo, never reject one. A present but partly unreadable MRZ still goes to the API, which reads it properly.
|
|
244
|
-
|
|
245
|
-
`AttestIDVerifier` and `useAttestID` won't submit a photo while `checkPhotoQuality` reports issues. If you call `startKYC` directly, gating on `checkPhotoQuality` is up to you.
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
## Drop-in React component
|
|
250
|
-
|
|
251
|
-
`AttestIDVerifier` is the fastest way to ship KYC in a React app: consent capture, in-browser camera capture with a review-before-submit step, client-side photo quality gating, liveness detection, and a result screen — all built in.
|
|
252
|
-
|
|
253
|
-
```tsx
|
|
254
|
-
import { AttestID } from 'attestid_js_sdk';
|
|
255
|
-
import { AttestIDVerifier } from 'attestid_js_sdk/react';
|
|
256
|
-
import 'attestid_js_sdk/styles.css';
|
|
257
|
-
|
|
258
|
-
const client = new AttestID({
|
|
259
|
-
apiUrl: 'https://<your-attestid-api-host>', // from your AttestID dashboard
|
|
260
|
-
getSdkToken: (userId) =>
|
|
261
|
-
fetch('/api/attestid-session', {
|
|
262
|
-
method: 'POST',
|
|
263
|
-
headers: { 'Content-Type': 'application/json' },
|
|
264
|
-
body: JSON.stringify({ userId }),
|
|
265
|
-
}).then(r => r.json()),
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
function KYCPage() {
|
|
269
|
-
return (
|
|
270
|
-
<AttestIDVerifier
|
|
271
|
-
client={client}
|
|
272
|
-
defaultUserId="user-123"
|
|
273
|
-
onVerified={(result) => console.log('Verified:', result.verified)}
|
|
274
|
-
onError={(error) => console.error('Verification error:', error)}
|
|
275
|
-
/>
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
| Prop | Type | Description |
|
|
281
|
-
|---|---|---|
|
|
282
|
-
| `client` | `AttestID` | Configured client instance |
|
|
283
|
-
| `defaultUserId` | `string?` | Pre-fills the user ID field |
|
|
284
|
-
| `onVerified` | `(result: KYCResult) => void` | Called once the flow reaches `done` |
|
|
285
|
-
| `onError` | `(error: string) => void` | Called on any error message from the flow |
|
|
286
|
-
| `mobileHandoffUrl` | `string?` | Page a desktop visitor's phone is sent to via QR/link. Defaults to the current page |
|
|
287
|
-
| `allowDesktopCapture` | `boolean?` | Let laptops/desktops capture the document with the webcam (phone QR still offered as an option). Default `true`; `false` makes capture phone-only |
|
|
288
|
-
| `allowPhotoUpload` | `boolean?` | Show an "Upload photo" option to pick an existing image instead of using the camera. For testing — uploads bypass live capture. Default `false` |
|
|
289
|
-
|
|
290
|
-
Need more control over the steps, styling, or layout? Build on `useAttestID` directly — see below.
|
|
291
|
-
|
|
292
|
-
---
|
|
293
|
-
|
|
294
|
-
## React hook
|
|
295
|
-
|
|
296
|
-
```tsx
|
|
297
|
-
import { useAttestID, AttestIDLivenessCheck } from 'attestid_js_sdk/react';
|
|
298
|
-
|
|
299
|
-
function KYCPage() {
|
|
300
|
-
const verification = useAttestID(client);
|
|
301
|
-
const { state, error, qualityIssues, result, isLoading,
|
|
302
|
-
startVerification, canRetryLiveness, retryLiveness, reset } = verification;
|
|
303
|
-
|
|
304
|
-
// consentGivenAt must be when the user actually agreed, not when the upload fires.
|
|
305
|
-
const upload = (file: File) =>
|
|
306
|
-
startVerification({ userId: 'user-123', idType: 'national_id', idPhoto: file, consentGivenAt });
|
|
307
|
-
|
|
308
|
-
if (state === 'liveness') {
|
|
309
|
-
// Runs the face check, then completes the verification.
|
|
310
|
-
return <AttestIDLivenessCheck verification={verification} onError={console.error} />;
|
|
311
|
-
}
|
|
312
|
-
if (state === 'done' && result) {
|
|
313
|
-
return (
|
|
314
|
-
<>
|
|
315
|
-
<p>{result.verified ? 'Verified' : result.message}</p>
|
|
316
|
-
{canRetryLiveness && <button onClick={retryLiveness}>Retry face check</button>}
|
|
317
|
-
<button onClick={reset}>Start again</button>
|
|
318
|
-
</>
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
// ...your consent + document capture UI, calling upload(file)...
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
`startVerification` runs the quality check first, then on pass uploads the ID and prepares the face check. If quality fails, `state` becomes `'error'` and `qualityIssues` is populated — no API call is made.
|
|
326
|
-
|
|
327
|
-
### Return values
|
|
328
|
-
|
|
329
|
-
| Property | Type | Description |
|
|
330
|
-
|---|---|---|
|
|
331
|
-
| `state` | `VerifyState` | Current flow state (see below) |
|
|
332
|
-
| `error` | `string \| null` | Last API error message |
|
|
333
|
-
| `qualityIssues` | `string[]` | Human-readable quality problems (populated on `error` after a quality failure) |
|
|
334
|
-
| `result` | `KYCResult \| null` | Final verification result |
|
|
335
|
-
| `isLoading` | `boolean` | `true` while in `quality_check`, `uploading`, `retrying`, or `completing` |
|
|
336
|
-
| `startVerification` | `(opts: StartKYCOptions) => Promise<void>` | Start the flow |
|
|
337
|
-
| `canRetryLiveness` | `boolean` | `true` when `result` failed only at the face check, so `retryLiveness()` can be offered |
|
|
338
|
-
| `retryLiveness` | `() => Promise<void>` | Get a fresh face-check session on the same verification and go back to `liveness`, without uploading the ID again |
|
|
339
|
-
| `reset` | `() => void` | Reset all state back to `idle` |
|
|
340
|
-
|
|
341
|
-
### States
|
|
342
|
-
|
|
343
|
-
| State | Meaning |
|
|
344
|
-
|---|---|
|
|
345
|
-
| `idle` | Ready to start |
|
|
346
|
-
| `quality_check` | Running client-side photo quality checks |
|
|
347
|
-
| `uploading` | Uploading ID photo and preparing the face check |
|
|
348
|
-
| `retrying` | Getting a fresh face-check session for `retryLiveness()` |
|
|
349
|
-
| `liveness` | Waiting for the face check: mount `AttestIDLivenessCheck` |
|
|
350
|
-
| `completing` | Fetching the final result |
|
|
351
|
-
| `done` | Result available in `result` |
|
|
352
|
-
| `error` | Something went wrong; see `error` or `qualityIssues` |
|
|
353
|
-
|
|
354
|
-
---
|
|
355
|
-
|
|
356
|
-
## Face check
|
|
357
|
-
|
|
358
|
-
`AttestIDVerifier` runs the face check for you. With the hook, mount `AttestIDLivenessCheck` while `state === 'liveness'`: it runs the check and completes the verification when it finishes. It renders nothing in any other state.
|
|
359
|
-
|
|
360
|
-
| Prop | Type | Description |
|
|
361
|
-
|---|---|---|
|
|
362
|
-
| `verification` | `ReturnType<typeof useAttestID>` | The object returned by `useAttestID()` (required) |
|
|
363
|
-
| `onError` | `(error: unknown) => void` | Called on a camera or face-check error |
|
|
364
|
-
| `deviceId` | `string?` | Forwarded to the backend when the verification completes |
|
|
365
|
-
| `showFlashNotice` | `boolean?` | Show the flashing-colours notice first. Default `true` |
|
|
366
|
-
| `lightingCheckMs` | `number?` | How long to coach the user towards better light before starting. Default `10000`; `0` skips it |
|
|
367
|
-
|
|
368
|
-
Things to know:
|
|
369
|
-
- **Timing.** A face-check session expires about 3 minutes after it starts. A failed or expired check needs a new session (see "Retrying a failed face check" below).
|
|
370
|
-
- **Photosensitivity.** The check can flash colours on screen. The SDK shows a notice before starting (`showFlashNotice`, default on). Keep it, or show your own.
|
|
371
|
-
- **Device binding.** With device binding on, the face-check capture is signed with the same device key as the ID photo.
|
|
372
|
-
- **Browser support.** Current Chrome, Edge, Firefox, Safari 14.1+, and iOS 14.3+, in a secure context (https, or http://localhost).
|
|
373
|
-
|
|
374
|
-
### Retrying a failed face check
|
|
375
|
-
|
|
376
|
-
When a verification fails only at the face check (`liveness_failed`, `liveness_confidence_too_low`, or `face_match_failed` with a score), the user can retry it without uploading the ID photo again; the document checks already done still count. `AttestIDVerifier` shows a **Try again** button (retake the selfie, same ID) next to **Start over** (upload a new ID) when this applies. Each failed face match still counts toward the API's per-user face-match strike limit. With the hook, offer your own button when `canRetryLiveness` is `true` and call `retryLiveness()`: `state` goes back to `liveness` and `AttestIDLivenessCheck` runs the check again.
|
|
377
|
-
|
|
378
|
-
The API refuses a retry (`KYCError`, HTTP 409/410) in these cases:
|
|
379
|
-
- suspected spoofs, face-match failures and fraud holds, which always need a new verification;
|
|
380
|
-
- after 2 retries on one verification;
|
|
381
|
-
- more than 30 minutes after the ID was uploaded.
|
|
382
|
-
|
|
383
|
-
When it refuses, start over with `reset()` / a new `startVerification()`.
|
|
384
|
-
|
|
385
|
-
---
|
|
386
|
-
|
|
387
|
-
## Camera utilities
|
|
388
|
-
|
|
389
|
-
```ts
|
|
390
|
-
import { openCameraSession, DOC_ASPECT } from 'attestid_js_sdk';
|
|
391
|
-
|
|
392
|
-
const session = await openCameraSession('national_id');
|
|
393
|
-
// session.stream — raw MediaStream
|
|
394
|
-
// session.captureFrame(frameEl, videoEl) → Promise<File> — crop to document frame
|
|
395
|
-
// session.stop() — release camera
|
|
396
|
-
|
|
397
|
-
// Aspect ratios for sizing your document overlay frame
|
|
398
|
-
// DOC_ASPECT.national_id === 85.6 / 54 (ISO 7810 ID-1)
|
|
399
|
-
// DOC_ASPECT.drivers_licence === 85.6 / 54 (ISO 7810 ID-1)
|
|
400
|
-
// DOC_ASPECT.passport === 125 / 88 (ISO 7810 ID-3)
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
---
|
|
137
|
+
## Errors
|
|
404
138
|
|
|
405
|
-
|
|
139
|
+
All errors extend `AttestIDError` and include a `code`.
|
|
406
140
|
|
|
407
|
-
|
|
|
141
|
+
| Class | When |
|
|
408
142
|
|---|---|
|
|
409
|
-
| `
|
|
410
|
-
| `
|
|
411
|
-
| `
|
|
143
|
+
| `AuthError` | Invalid credentials or session token |
|
|
144
|
+
| `KYCError` | The API rejected the request |
|
|
145
|
+
| `CameraError` | Camera access denied or capture failed |
|
|
146
|
+
| `SignatureError` | Attestation or webhook signature is invalid |
|
|
147
|
+
| `AttestIDError` | Configuration, consent or liveness errors |
|
|
412
148
|
|
|
413
|
-
|
|
149
|
+
## Requirements
|
|
414
150
|
|
|
415
|
-
|
|
151
|
+
Modern browsers over HTTPS, or Node.js 19+.
|
|
416
152
|
|
|
417
|
-
|
|
153
|
+
## License
|
|
418
154
|
|
|
419
|
-
|
|
420
|
-
import { AttestIDError, AuthError, KYCError, CameraError } from 'attestid_js_sdk';
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
await client.startVerification({ ... });
|
|
424
|
-
} catch (e) {
|
|
425
|
-
if (e instanceof AuthError) console.error('Auth failed', e.status); // 401/403
|
|
426
|
-
if (e instanceof KYCError) console.error('KYC failed', e.code, e.status);
|
|
427
|
-
if (e instanceof CameraError) console.error('Camera error', e.message);
|
|
428
|
-
if (e instanceof AttestIDError) console.error(e.code, e.message); // e.g. CONSENT_REQUIRED, INSECURE_CONFIG
|
|
429
|
-
}
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
| Class | `code` | When thrown |
|
|
433
|
-
|---|---|---|
|
|
434
|
-
| `AuthError` | `AUTH_ERROR` | Invalid API key/access key, an expired or invalid `sdk_token` (401 / 403), or `getSdkToken()` resolving without a valid token |
|
|
435
|
-
| `KYCError` | `KYC_ERROR` | API returned a non-auth error |
|
|
436
|
-
| `CameraError` | `CAMERA_ERROR` | Camera access denied or capture failed |
|
|
437
|
-
| `AttestIDError` | `CONSENT_REQUIRED` / `INSECURE_CONFIG` | Missing `consentGivenAt`; `apiKey`/`accessKeyId` configured outside a trusted Node.js backend; or `apiUrl` isn't `https://` (non-loopback host) |
|
|
438
|
-
| `SignatureError` | `SIGNATURE_INVALID` | `verifyAttestation()` / `verifyWebhook()`: bad signature, wrong verification/company id, altered document data, or a webhook timestamp outside the replay window |
|
|
439
|
-
| `AttestIDError` | `LIVENESS_UNSUPPORTED` / `LIVENESS_RECORDING_FAILED` / `LIVENESS_ABORTED` / `LIVENESS_TIMEOUT` | Face check: the browser can't run it; it failed or was cancelled; or no decision arrived in time (default 120 s) |
|
|
440
|
-
| `AttestIDError` | `JWKS_UNAVAILABLE` / `CRYPTO_UNAVAILABLE` / `INVALID_ARGUMENT` | AttestID's public keys couldn't be fetched; the runtime has no WebCrypto; `verifyWebhook()` called without `secrets` or `apiUrl` |
|
|
441
|
-
|
|
442
|
-
---
|
|
443
|
-
|
|
444
|
-
## Development
|
|
445
|
-
|
|
446
|
-
```bash
|
|
447
|
-
npm install
|
|
448
|
-
npm run build # compile to dist/
|
|
449
|
-
npm test # run tests
|
|
450
|
-
npm run test:watch # watch mode
|
|
451
|
-
npm run test:coverage # coverage report
|
|
452
|
-
npm run lint # type-check only
|
|
453
|
-
npm run dev # build in watch mode
|
|
454
|
-
```
|
|
155
|
+
MIT
|