attestid_js_sdk 0.1.11 → 0.1.13
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 +22 -0
- package/dist/core/index.cjs +166 -31
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +50 -1
- package/dist/core/index.d.ts +50 -1
- package/dist/core/index.js +165 -31
- package/dist/core/index.js.map +1 -1
- package/dist/react/index.cjs +62 -62
- package/dist/react/index.d.cts +31 -0
- package/dist/react/index.d.ts +31 -0
- package/dist/react/index.js +62 -62
- package/dist/styles/attestid.css +31 -0
- package/package.json +1 -1
package/dist/core/index.d.cts
CHANGED
|
@@ -4,6 +4,11 @@ interface RetryOptions {
|
|
|
4
4
|
attempts?: number;
|
|
5
5
|
/** Base backoff in ms (doubled each retry). Default 500. */
|
|
6
6
|
baseDelayMs?: number;
|
|
7
|
+
/**
|
|
8
|
+
* How long to keep waiting while the server says the same request is still
|
|
9
|
+
* in progress (409 idempotency_request_in_progress). Default 120 000 ms.
|
|
10
|
+
*/
|
|
11
|
+
inProgressWaitMs?: number;
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
type IdType = 'national_id' | 'passport' | 'drivers_licence';
|
|
@@ -74,10 +79,35 @@ interface LivenessCredentials {
|
|
|
74
79
|
expiration: string;
|
|
75
80
|
region: string;
|
|
76
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Whether to ask for the back of a national ID / driver's licence. Each
|
|
84
|
+
* company decides on the AttestID dashboard (Settings):
|
|
85
|
+
* - 'off' — don't ask; a back sent anyway is ignored by the API.
|
|
86
|
+
* - 'optional' — ask, and let the user skip it.
|
|
87
|
+
* - 'required' — the back must be sent and its MRZ must be readable,
|
|
88
|
+
* otherwise POST /kyc/start fails with
|
|
89
|
+
* document_back_required / document_back_unreadable.
|
|
90
|
+
* Never applies to passports.
|
|
91
|
+
*/
|
|
92
|
+
type DocumentBackPolicy = 'off' | 'optional' | 'required';
|
|
93
|
+
/** What the document capture should ask for, as set by the company. */
|
|
94
|
+
interface CaptureSettings {
|
|
95
|
+
document_back: DocumentBackPolicy;
|
|
96
|
+
}
|
|
77
97
|
interface StartKYCOptions {
|
|
78
98
|
userId: string;
|
|
79
99
|
idType: IdType;
|
|
80
100
|
idPhoto: File | Blob;
|
|
101
|
+
/**
|
|
102
|
+
* Optional photo of the BACK of a national ID / driver's licence. Many
|
|
103
|
+
* cards (ICAO 9303 TD1) print their machine-readable zone (MRZ) on the back;
|
|
104
|
+
* when this photo carries one, the API cross-checks its date of birth and
|
|
105
|
+
* expiry against the front and rejects a mismatch (document_sides_mismatch).
|
|
106
|
+
* A back with no MRZ is ignored, never a failure. Not accepted with
|
|
107
|
+
* idType 'passport'. Use checkBackPhotoQuality() to tell the user whether
|
|
108
|
+
* an MRZ was found before sending it.
|
|
109
|
+
*/
|
|
110
|
+
idPhotoBack?: File | Blob;
|
|
81
111
|
/**
|
|
82
112
|
* Stated purpose for biometric processing. Defaults to 'identity_verification'.
|
|
83
113
|
* One of: 'identity_verification', 'kyc_onboarding', 'kyc_refresh',
|
|
@@ -260,6 +290,12 @@ declare class AttestID {
|
|
|
260
290
|
* TypeScript's type check are still rejected here at runtime.
|
|
261
291
|
*/
|
|
262
292
|
startVerification(options: StartKYCOptions): Promise<StartKYCResponse>;
|
|
293
|
+
/**
|
|
294
|
+
* The company's capture settings, set on the AttestID dashboard. Read
|
|
295
|
+
* `document_back` before the document step to know whether to ask for the
|
|
296
|
+
* back of a national ID / driver's licence (see DocumentBackPolicy).
|
|
297
|
+
*/
|
|
298
|
+
getCaptureSettings(userId: string): Promise<CaptureSettings>;
|
|
263
299
|
/**
|
|
264
300
|
* Fetch temporary AWS credentials and the Cognito identity pool ID from the
|
|
265
301
|
* backend. Call this alongside startVerification() for the same userId to
|
|
@@ -502,6 +538,19 @@ interface PhotoQualityResult {
|
|
|
502
538
|
ok: boolean;
|
|
503
539
|
issues: string[];
|
|
504
540
|
}
|
|
541
|
+
interface BackPhotoQualityResult extends PhotoQualityResult {
|
|
542
|
+
/** True when an MRZ (the lines of letters, numbers and <<<) was found. */
|
|
543
|
+
hasMrz: boolean;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Checks a photo of the BACK of an ID card / driver's licence (the optional
|
|
547
|
+
* StartKYCOptions.idPhotoBack). The back is only useful to the API when it
|
|
548
|
+
* carries an ICAO 9303 MRZ, so `ok` means "an MRZ was found and is legible
|
|
549
|
+
* enough to send". When `hasMrz` is false the caller should offer a retake
|
|
550
|
+
* or let the user skip the back: many cards have no MRZ at all, and the
|
|
551
|
+
* verification never needs one.
|
|
552
|
+
*/
|
|
553
|
+
declare function checkBackPhotoQuality(photo: Blob): Promise<BackPhotoQualityResult>;
|
|
505
554
|
/**
|
|
506
555
|
* Analyses a photo for blur, exposure, and (where the browser supports it)
|
|
507
556
|
* face presence before it is submitted to the AttestID API. Returns a list
|
|
@@ -582,4 +631,4 @@ declare function currentUrlWithoutHandoffParams(): string;
|
|
|
582
631
|
/** Strips our own handoff params from the visible address bar, without a page reload. Call once, after reading them. */
|
|
583
632
|
declare function clearHandoffParamsFromAddressBar(): void;
|
|
584
633
|
|
|
585
|
-
export { AttestID, type AttestIDConfig, type AttestIDConfigBrowser, type AttestIDConfigServer, AttestIDError, type AttestationClaims, AuthError, CAPTURE_ISSUE_PROMPTS, CameraError, type CaptureAssurance, type CaptureConditions, type CaptureIssue, type ChecksInfo, DOC_ASPECT, type DocumentInfo, FLASH_CSS, HANDOFF_PARAM_KEYS, type HandoffParams, type IdType, KYCError, type KYCResult, LIVENESS_PROMPTS, type LivenessAction, type LivenessChallenge, type LivenessCredentials, type LivenessProvider, type LivenessRecording, type LivenessSegment, type LivenessVideoResult, MAX_GOOD_BRIGHTNESS, MIN_GOOD_BRIGHTNESS, MIN_GOOD_FPS, type PhotoQualityResult, type QrErrorCorrection, RECORDING_TAIL_MS, RETRYABLE_LIVENESS_REASONS, type RecordLivenessOptions, type SdkSessionResponse, SignatureError, type StartKYCOptions, type StartKYCResponse, type SubmitLivenessVideoOptions, ValidationError, type VerifyAttestationOptions, type VerifyWebhookOptions, type WaitForLivenessOptions, buildHandoffUrl, canRetryLiveness, canonicalJson, checkCaptureConditions, checkPhotoQuality, classifyConditions, clearHandoffParamsFromAddressBar, createSdkSession, currentUrlWithoutHandoffParams, faceMatchOutcome, generateHandoffToken, getQrMatrix, isMobileDevice, livenessCaptureSupported, meanLuma, openCameraSession, openFrontCamera, pickRecorderMimeType, promptFor, rawSessionId, readHandoffParams, recordLiveness, scriptDurationMs, segmentAt, verifyAttestation, verifyWebhook };
|
|
634
|
+
export { AttestID, type AttestIDConfig, type AttestIDConfigBrowser, type AttestIDConfigServer, AttestIDError, type AttestationClaims, AuthError, type BackPhotoQualityResult, CAPTURE_ISSUE_PROMPTS, CameraError, type CaptureAssurance, type CaptureConditions, type CaptureIssue, type CaptureSettings, type ChecksInfo, DOC_ASPECT, type DocumentBackPolicy, type DocumentInfo, FLASH_CSS, HANDOFF_PARAM_KEYS, type HandoffParams, type IdType, KYCError, type KYCResult, LIVENESS_PROMPTS, type LivenessAction, type LivenessChallenge, type LivenessCredentials, type LivenessProvider, type LivenessRecording, type LivenessSegment, type LivenessVideoResult, MAX_GOOD_BRIGHTNESS, MIN_GOOD_BRIGHTNESS, MIN_GOOD_FPS, type PhotoQualityResult, type QrErrorCorrection, RECORDING_TAIL_MS, RETRYABLE_LIVENESS_REASONS, type RecordLivenessOptions, type SdkSessionResponse, SignatureError, type StartKYCOptions, type StartKYCResponse, type SubmitLivenessVideoOptions, ValidationError, type VerifyAttestationOptions, type VerifyWebhookOptions, type WaitForLivenessOptions, buildHandoffUrl, canRetryLiveness, canonicalJson, checkBackPhotoQuality, checkCaptureConditions, checkPhotoQuality, classifyConditions, clearHandoffParamsFromAddressBar, createSdkSession, currentUrlWithoutHandoffParams, faceMatchOutcome, generateHandoffToken, getQrMatrix, isMobileDevice, livenessCaptureSupported, meanLuma, openCameraSession, openFrontCamera, pickRecorderMimeType, promptFor, rawSessionId, readHandoffParams, recordLiveness, scriptDurationMs, segmentAt, verifyAttestation, verifyWebhook };
|
package/dist/core/index.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ interface RetryOptions {
|
|
|
4
4
|
attempts?: number;
|
|
5
5
|
/** Base backoff in ms (doubled each retry). Default 500. */
|
|
6
6
|
baseDelayMs?: number;
|
|
7
|
+
/**
|
|
8
|
+
* How long to keep waiting while the server says the same request is still
|
|
9
|
+
* in progress (409 idempotency_request_in_progress). Default 120 000 ms.
|
|
10
|
+
*/
|
|
11
|
+
inProgressWaitMs?: number;
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
type IdType = 'national_id' | 'passport' | 'drivers_licence';
|
|
@@ -74,10 +79,35 @@ interface LivenessCredentials {
|
|
|
74
79
|
expiration: string;
|
|
75
80
|
region: string;
|
|
76
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Whether to ask for the back of a national ID / driver's licence. Each
|
|
84
|
+
* company decides on the AttestID dashboard (Settings):
|
|
85
|
+
* - 'off' — don't ask; a back sent anyway is ignored by the API.
|
|
86
|
+
* - 'optional' — ask, and let the user skip it.
|
|
87
|
+
* - 'required' — the back must be sent and its MRZ must be readable,
|
|
88
|
+
* otherwise POST /kyc/start fails with
|
|
89
|
+
* document_back_required / document_back_unreadable.
|
|
90
|
+
* Never applies to passports.
|
|
91
|
+
*/
|
|
92
|
+
type DocumentBackPolicy = 'off' | 'optional' | 'required';
|
|
93
|
+
/** What the document capture should ask for, as set by the company. */
|
|
94
|
+
interface CaptureSettings {
|
|
95
|
+
document_back: DocumentBackPolicy;
|
|
96
|
+
}
|
|
77
97
|
interface StartKYCOptions {
|
|
78
98
|
userId: string;
|
|
79
99
|
idType: IdType;
|
|
80
100
|
idPhoto: File | Blob;
|
|
101
|
+
/**
|
|
102
|
+
* Optional photo of the BACK of a national ID / driver's licence. Many
|
|
103
|
+
* cards (ICAO 9303 TD1) print their machine-readable zone (MRZ) on the back;
|
|
104
|
+
* when this photo carries one, the API cross-checks its date of birth and
|
|
105
|
+
* expiry against the front and rejects a mismatch (document_sides_mismatch).
|
|
106
|
+
* A back with no MRZ is ignored, never a failure. Not accepted with
|
|
107
|
+
* idType 'passport'. Use checkBackPhotoQuality() to tell the user whether
|
|
108
|
+
* an MRZ was found before sending it.
|
|
109
|
+
*/
|
|
110
|
+
idPhotoBack?: File | Blob;
|
|
81
111
|
/**
|
|
82
112
|
* Stated purpose for biometric processing. Defaults to 'identity_verification'.
|
|
83
113
|
* One of: 'identity_verification', 'kyc_onboarding', 'kyc_refresh',
|
|
@@ -260,6 +290,12 @@ declare class AttestID {
|
|
|
260
290
|
* TypeScript's type check are still rejected here at runtime.
|
|
261
291
|
*/
|
|
262
292
|
startVerification(options: StartKYCOptions): Promise<StartKYCResponse>;
|
|
293
|
+
/**
|
|
294
|
+
* The company's capture settings, set on the AttestID dashboard. Read
|
|
295
|
+
* `document_back` before the document step to know whether to ask for the
|
|
296
|
+
* back of a national ID / driver's licence (see DocumentBackPolicy).
|
|
297
|
+
*/
|
|
298
|
+
getCaptureSettings(userId: string): Promise<CaptureSettings>;
|
|
263
299
|
/**
|
|
264
300
|
* Fetch temporary AWS credentials and the Cognito identity pool ID from the
|
|
265
301
|
* backend. Call this alongside startVerification() for the same userId to
|
|
@@ -502,6 +538,19 @@ interface PhotoQualityResult {
|
|
|
502
538
|
ok: boolean;
|
|
503
539
|
issues: string[];
|
|
504
540
|
}
|
|
541
|
+
interface BackPhotoQualityResult extends PhotoQualityResult {
|
|
542
|
+
/** True when an MRZ (the lines of letters, numbers and <<<) was found. */
|
|
543
|
+
hasMrz: boolean;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Checks a photo of the BACK of an ID card / driver's licence (the optional
|
|
547
|
+
* StartKYCOptions.idPhotoBack). The back is only useful to the API when it
|
|
548
|
+
* carries an ICAO 9303 MRZ, so `ok` means "an MRZ was found and is legible
|
|
549
|
+
* enough to send". When `hasMrz` is false the caller should offer a retake
|
|
550
|
+
* or let the user skip the back: many cards have no MRZ at all, and the
|
|
551
|
+
* verification never needs one.
|
|
552
|
+
*/
|
|
553
|
+
declare function checkBackPhotoQuality(photo: Blob): Promise<BackPhotoQualityResult>;
|
|
505
554
|
/**
|
|
506
555
|
* Analyses a photo for blur, exposure, and (where the browser supports it)
|
|
507
556
|
* face presence before it is submitted to the AttestID API. Returns a list
|
|
@@ -582,4 +631,4 @@ declare function currentUrlWithoutHandoffParams(): string;
|
|
|
582
631
|
/** Strips our own handoff params from the visible address bar, without a page reload. Call once, after reading them. */
|
|
583
632
|
declare function clearHandoffParamsFromAddressBar(): void;
|
|
584
633
|
|
|
585
|
-
export { AttestID, type AttestIDConfig, type AttestIDConfigBrowser, type AttestIDConfigServer, AttestIDError, type AttestationClaims, AuthError, CAPTURE_ISSUE_PROMPTS, CameraError, type CaptureAssurance, type CaptureConditions, type CaptureIssue, type ChecksInfo, DOC_ASPECT, type DocumentInfo, FLASH_CSS, HANDOFF_PARAM_KEYS, type HandoffParams, type IdType, KYCError, type KYCResult, LIVENESS_PROMPTS, type LivenessAction, type LivenessChallenge, type LivenessCredentials, type LivenessProvider, type LivenessRecording, type LivenessSegment, type LivenessVideoResult, MAX_GOOD_BRIGHTNESS, MIN_GOOD_BRIGHTNESS, MIN_GOOD_FPS, type PhotoQualityResult, type QrErrorCorrection, RECORDING_TAIL_MS, RETRYABLE_LIVENESS_REASONS, type RecordLivenessOptions, type SdkSessionResponse, SignatureError, type StartKYCOptions, type StartKYCResponse, type SubmitLivenessVideoOptions, ValidationError, type VerifyAttestationOptions, type VerifyWebhookOptions, type WaitForLivenessOptions, buildHandoffUrl, canRetryLiveness, canonicalJson, checkCaptureConditions, checkPhotoQuality, classifyConditions, clearHandoffParamsFromAddressBar, createSdkSession, currentUrlWithoutHandoffParams, faceMatchOutcome, generateHandoffToken, getQrMatrix, isMobileDevice, livenessCaptureSupported, meanLuma, openCameraSession, openFrontCamera, pickRecorderMimeType, promptFor, rawSessionId, readHandoffParams, recordLiveness, scriptDurationMs, segmentAt, verifyAttestation, verifyWebhook };
|
|
634
|
+
export { AttestID, type AttestIDConfig, type AttestIDConfigBrowser, type AttestIDConfigServer, AttestIDError, type AttestationClaims, AuthError, type BackPhotoQualityResult, CAPTURE_ISSUE_PROMPTS, CameraError, type CaptureAssurance, type CaptureConditions, type CaptureIssue, type CaptureSettings, type ChecksInfo, DOC_ASPECT, type DocumentBackPolicy, type DocumentInfo, FLASH_CSS, HANDOFF_PARAM_KEYS, type HandoffParams, type IdType, KYCError, type KYCResult, LIVENESS_PROMPTS, type LivenessAction, type LivenessChallenge, type LivenessCredentials, type LivenessProvider, type LivenessRecording, type LivenessSegment, type LivenessVideoResult, MAX_GOOD_BRIGHTNESS, MIN_GOOD_BRIGHTNESS, MIN_GOOD_FPS, type PhotoQualityResult, type QrErrorCorrection, RECORDING_TAIL_MS, RETRYABLE_LIVENESS_REASONS, type RecordLivenessOptions, type SdkSessionResponse, SignatureError, type StartKYCOptions, type StartKYCResponse, type SubmitLivenessVideoOptions, ValidationError, type VerifyAttestationOptions, type VerifyWebhookOptions, type WaitForLivenessOptions, buildHandoffUrl, canRetryLiveness, canonicalJson, checkBackPhotoQuality, checkCaptureConditions, checkPhotoQuality, classifyConditions, clearHandoffParamsFromAddressBar, createSdkSession, currentUrlWithoutHandoffParams, faceMatchOutcome, generateHandoffToken, getQrMatrix, isMobileDevice, livenessCaptureSupported, meanLuma, openCameraSession, openFrontCamera, pickRecorderMimeType, promptFor, rawSessionId, readHandoffParams, recordLiveness, scriptDurationMs, segmentAt, verifyAttestation, verifyWebhook };
|
package/dist/core/index.js
CHANGED
|
@@ -99,6 +99,8 @@ function randomId() {
|
|
|
99
99
|
// src/core/http.ts
|
|
100
100
|
var RETRY_FRESH_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
101
101
|
var MAX_RETRY_AFTER_MS = 1e4;
|
|
102
|
+
var DEFAULT_IN_PROGRESS_WAIT_MS = 12e4;
|
|
103
|
+
var IN_PROGRESS_ERROR = "idempotency_request_in_progress";
|
|
102
104
|
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
103
105
|
var sleep = defaultSleep;
|
|
104
106
|
function retryAfterMs(res, fallback) {
|
|
@@ -106,9 +108,19 @@ function retryAfterMs(res, fallback) {
|
|
|
106
108
|
const secs = raw ? Number(raw) : NaN;
|
|
107
109
|
return Number.isFinite(secs) && secs >= 0 ? Math.min(secs * 1e3, MAX_RETRY_AFTER_MS) : fallback;
|
|
108
110
|
}
|
|
111
|
+
async function isInProgress(res) {
|
|
112
|
+
try {
|
|
113
|
+
const body = await (res.clone ? res.clone() : res).json();
|
|
114
|
+
return (body?.detail?.error ?? body?.error) === IN_PROGRESS_ERROR;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
109
119
|
async function postIdempotent(fetchImpl, url, build, options = {}) {
|
|
110
120
|
const attempts = Math.max(1, options.attempts ?? 3);
|
|
111
121
|
const base = options.baseDelayMs ?? 500;
|
|
122
|
+
const inProgressBudget = options.inProgressWaitMs ?? DEFAULT_IN_PROGRESS_WAIT_MS;
|
|
123
|
+
let inProgressWaited = 0;
|
|
112
124
|
let key = randomId();
|
|
113
125
|
let req = await build();
|
|
114
126
|
for (let attempt = 1; ; attempt++) {
|
|
@@ -125,11 +137,17 @@ async function postIdempotent(fetchImpl, url, build, options = {}) {
|
|
|
125
137
|
await sleep(base * 2 ** (attempt - 1));
|
|
126
138
|
continue;
|
|
127
139
|
}
|
|
128
|
-
if (
|
|
140
|
+
if (res.ok) return res;
|
|
129
141
|
if (res.status === 409) {
|
|
130
|
-
await
|
|
142
|
+
if (!await isInProgress(res)) return res;
|
|
143
|
+
const delay = retryAfterMs(res, base);
|
|
144
|
+
if (inProgressWaited + delay > inProgressBudget) return res;
|
|
145
|
+
inProgressWaited += delay;
|
|
146
|
+
await sleep(delay);
|
|
147
|
+
attempt--;
|
|
131
148
|
continue;
|
|
132
149
|
}
|
|
150
|
+
if (last) return res;
|
|
133
151
|
if (RETRY_FRESH_STATUSES.has(res.status)) {
|
|
134
152
|
await sleep(retryAfterMs(res, base * 2 ** (attempt - 1)));
|
|
135
153
|
key = randomId();
|
|
@@ -186,6 +204,8 @@ function countTextRows(grey, w, h, x0, aw) {
|
|
|
186
204
|
return textRows;
|
|
187
205
|
}
|
|
188
206
|
var EDGE_DELTA = 28;
|
|
207
|
+
var EDGE_DELTA_MIN = 12;
|
|
208
|
+
var EDGE_DELTA_P99_FRACTION = 0.8;
|
|
189
209
|
var NOISY_MEDIAN_STEP = 9;
|
|
190
210
|
var SKEW_TANS = [0, 0.0175, -0.0175, 0.0349, -0.0349, 0.0524, -0.0524, 0.0699, -0.0699];
|
|
191
211
|
var STRIPS = 8;
|
|
@@ -201,14 +221,15 @@ function idTextNotFoundMessage(idType) {
|
|
|
201
221
|
const doc = idType === "drivers_licence" ? "driver's licence" : "ID card";
|
|
202
222
|
return `We could not detect readable printed details on your ${doc}. Photograph the front of the card (the side with your photo, name and ID number) so it fills the frame, in good light and without glare.`;
|
|
203
223
|
}
|
|
204
|
-
function measureTextBands(grey, w, h, x0 = 0, aw = w) {
|
|
224
|
+
function measureTextBands(grey, w, h, x0 = 0, aw = w, edgeDeltaOverride) {
|
|
205
225
|
const src = medianStep(grey, w, h, x0, aw) >= NOISY_MEDIAN_STEP ? boxSmooth(grey, w, h, x0, aw) : null;
|
|
206
226
|
const at = src ? (y, x) => src[y * aw + x] : (y, x) => grey[y * w + x0 + x];
|
|
227
|
+
const edgeDelta = edgeDeltaOverride ?? edgeThreshold(at, aw, h);
|
|
207
228
|
const ex = [];
|
|
208
229
|
const ey = [];
|
|
209
230
|
for (let y = 0; y < h; y++) {
|
|
210
231
|
for (let x = 0; x < aw - 1; x++) {
|
|
211
|
-
if (Math.abs(at(y, x + 1) - at(y, x)) >
|
|
232
|
+
if (Math.abs(at(y, x + 1) - at(y, x)) > edgeDelta) {
|
|
212
233
|
ex.push(x);
|
|
213
234
|
ey.push(y);
|
|
214
235
|
}
|
|
@@ -311,6 +332,7 @@ function measureTextBands(grey, w, h, x0 = 0, aw = w) {
|
|
|
311
332
|
height,
|
|
312
333
|
strip: s,
|
|
313
334
|
firstCell: first,
|
|
335
|
+
lastCell: last,
|
|
314
336
|
spanFrac: span / cps,
|
|
315
337
|
coverage: active / span,
|
|
316
338
|
density: inside / (span * CELL * height),
|
|
@@ -346,6 +368,25 @@ function medianStep(grey, w, h, x0, aw) {
|
|
|
346
368
|
}
|
|
347
369
|
return 255;
|
|
348
370
|
}
|
|
371
|
+
function edgeThreshold(at, aw, h) {
|
|
372
|
+
const hist = new Uint32Array(256);
|
|
373
|
+
let n = 0;
|
|
374
|
+
for (let y = 0; y < h; y++) {
|
|
375
|
+
for (let x = 0; x < aw - 1; x++) {
|
|
376
|
+
hist[Math.min(255, Math.round(Math.abs(at(y, x + 1) - at(y, x))))]++;
|
|
377
|
+
n++;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let acc = 0, p99 = 255;
|
|
381
|
+
for (let v = 0; v < 256; v++) {
|
|
382
|
+
acc += hist[v];
|
|
383
|
+
if (acc >= n * 0.99) {
|
|
384
|
+
p99 = v;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return Math.max(EDGE_DELTA_MIN, Math.min(EDGE_DELTA, Math.round(p99 * EDGE_DELTA_P99_FRACTION)));
|
|
389
|
+
}
|
|
349
390
|
function boxSmooth(grey, w, h, x0, aw) {
|
|
350
391
|
const out = new Float32Array(aw * h);
|
|
351
392
|
for (let y = 0; y < h; y++) {
|
|
@@ -391,15 +432,17 @@ function periodicity(profile, glyphHeight) {
|
|
|
391
432
|
}
|
|
392
433
|
return best;
|
|
393
434
|
}
|
|
394
|
-
function analyseTextLayout(grey, w, h, x0 = 0, aw = w) {
|
|
395
|
-
const { bands, fullWidthLines } = measureTextBands(grey, w, h, x0, aw);
|
|
435
|
+
function analyseTextLayout(grey, w, h, x0 = 0, aw = w, edgeDeltaOverride) {
|
|
436
|
+
const { bands, fullWidthLines } = measureTextBands(grey, w, h, x0, aw, edgeDeltaOverride);
|
|
396
437
|
const stripCells = Math.ceil(Math.max(1, Math.floor(aw / STRIPS)) / CELL);
|
|
397
438
|
const clean = bands.filter((b) => b.spanFrac >= 0.2 && b.coverage >= 0.35 && b.bleed <= TEXT_MAX_BLEED && b.spanFrac * stripCells * CELL / b.height >= 3);
|
|
398
439
|
const textLines = clusterBands(clean).length;
|
|
399
440
|
let alignedLines = 0;
|
|
400
441
|
for (const b of clean) {
|
|
401
|
-
const
|
|
402
|
-
|
|
442
|
+
const sameStrip = clean.filter((o) => o.strip === b.strip);
|
|
443
|
+
const left = sameStrip.filter((o) => Math.abs(o.firstCell - b.firstCell) <= 1).length;
|
|
444
|
+
const right = sameStrip.filter((o) => Math.abs(o.lastCell - b.lastCell) <= 1).length;
|
|
445
|
+
alignedLines = Math.max(alignedLines, left, right);
|
|
403
446
|
}
|
|
404
447
|
const isMrzLine = (l) => l.bleed <= MRZ_MAX_BLEED && l.periodicity >= MRZ_MIN_PERIODICITY;
|
|
405
448
|
const stacked = (p, q) => {
|
|
@@ -407,19 +450,23 @@ function analyseTextLayout(grey, w, h, x0 = 0, aw = w) {
|
|
|
407
450
|
const pitch = q.center - p.center;
|
|
408
451
|
return pitch >= 1.3 * avgH && pitch <= 2.8 * avgH && Math.max(p.height, q.height) / Math.min(p.height, q.height) <= 1.4;
|
|
409
452
|
};
|
|
410
|
-
let
|
|
453
|
+
let hasMrz2 = false;
|
|
411
454
|
let runStart = 0;
|
|
412
455
|
for (let i = 1; i <= fullWidthLines.length; i++) {
|
|
413
456
|
if (i < fullWidthLines.length && stacked(fullWidthLines[i - 1], fullWidthLines[i])) continue;
|
|
414
457
|
const run = fullWidthLines.slice(runStart, i);
|
|
415
458
|
if (run.length >= 2 && run.length <= 3) {
|
|
416
459
|
for (let j = 1; j < run.length; j++) {
|
|
417
|
-
if (isMrzLine(run[j - 1]) && isMrzLine(run[j]))
|
|
460
|
+
if (isMrzLine(run[j - 1]) && isMrzLine(run[j])) hasMrz2 = true;
|
|
418
461
|
}
|
|
419
462
|
}
|
|
420
463
|
runStart = i;
|
|
421
464
|
}
|
|
422
|
-
return { textLines, alignedLines, hasMrz };
|
|
465
|
+
return { textLines, alignedLines, hasMrz: hasMrz2 };
|
|
466
|
+
}
|
|
467
|
+
var MRZ_RETRY_EDGE_DELTA = 14;
|
|
468
|
+
function hasMrz(grey, w, h, x0 = 0, aw = w) {
|
|
469
|
+
return analyseTextLayout(grey, w, h, x0, aw).hasMrz || analyseTextLayout(grey, w, h, x0, aw, MRZ_RETRY_EDGE_DELTA).hasMrz;
|
|
423
470
|
}
|
|
424
471
|
async function detectText(source) {
|
|
425
472
|
const TextDetectorCtor = globalThis.TextDetector;
|
|
@@ -448,14 +495,14 @@ function laplacianVariance(grey, w, h, x0, aw) {
|
|
|
448
495
|
function checkPassportDocument(grey, w, h, browserFoundMrz = false) {
|
|
449
496
|
const ratio = w / h;
|
|
450
497
|
const regions = ratio > 2 ? [{ x0: 0, aw: Math.floor(w / 2) }, { x0: Math.floor(w / 2), aw: w - Math.floor(w / 2) }] : [{ x0: 0, aw: w }];
|
|
451
|
-
let dataPage = regions.find(({ x0, aw }) =>
|
|
498
|
+
let dataPage = regions.find(({ x0, aw }) => hasMrz(grey, w, h, x0, aw));
|
|
452
499
|
if (!dataPage && browserFoundMrz) {
|
|
453
500
|
dataPage = regions.reduce((best, r) => countTextRows(grey, w, h, r.x0, r.aw) > countTextRows(grey, w, h, best.x0, best.aw) ? r : best);
|
|
454
501
|
}
|
|
455
502
|
if (!dataPage) return [MRZ_NOT_FOUND_MESSAGE];
|
|
456
503
|
if (laplacianVariance(grey, w, h, dataPage.x0, dataPage.aw) < BLUR_THRESHOLD) {
|
|
457
504
|
return [
|
|
458
|
-
"The data page is too blurry
|
|
505
|
+
"The data page is too blurry. Hold the camera steady and make sure the MRZ text at the bottom of the data page is in sharp focus."
|
|
459
506
|
];
|
|
460
507
|
}
|
|
461
508
|
return [];
|
|
@@ -463,6 +510,7 @@ function checkPassportDocument(grey, w, h, browserFoundMrz = false) {
|
|
|
463
510
|
function checkReadableText(grey, w, h, idType, browserTextBlocks = null) {
|
|
464
511
|
if ((browserTextBlocks ?? 0) >= 3) return [];
|
|
465
512
|
const layout = analyseTextLayout(grey, w, h);
|
|
513
|
+
if (layout.hasMrz) return [];
|
|
466
514
|
if (layout.textLines >= MIN_ID_TEXT_LINES && layout.alignedLines >= MIN_ID_ALIGNED_LINES) return [];
|
|
467
515
|
return [idTextNotFoundMessage(idType)];
|
|
468
516
|
}
|
|
@@ -471,7 +519,7 @@ function checkDocumentIdentity(grey, w, h, idType, precomputedTotalTextRows) {
|
|
|
471
519
|
const totalTextRows = precomputedTotalTextRows ?? countTextRows(grey, w, h, 0, w);
|
|
472
520
|
if (totalTextRows < Math.max(2, h * 0.02)) {
|
|
473
521
|
return [
|
|
474
|
-
"This does not look like an identity document
|
|
522
|
+
"This does not look like an identity document: no printed text was detected. Please photograph your ID card or passport data page on a plain, well-lit background."
|
|
475
523
|
];
|
|
476
524
|
}
|
|
477
525
|
if (idType === "passport") {
|
|
@@ -487,14 +535,68 @@ function checkDocumentIdentity(grey, w, h, idType, precomputedTotalTextRows) {
|
|
|
487
535
|
}
|
|
488
536
|
return [];
|
|
489
537
|
}
|
|
538
|
+
var BACK_MRZ_NOT_FOUND_MESSAGE = "We could not read the machine-readable zone (MRZ) on the back of your card: the 3 lines of letters, numbers and <<< along the bottom. This usually means the photo is blurry or the card is too small in the frame. Fill the frame with the back of the card, hold the phone steady, and wait for the <<< lines to come into focus before capturing. Avoid glare on the card.";
|
|
539
|
+
function checkBackOfCard(grey, w, h, browserFoundMrz = false) {
|
|
540
|
+
let total = 0;
|
|
541
|
+
for (let i = 0; i < grey.length; i++) total += grey[i];
|
|
542
|
+
const brightness = total / grey.length;
|
|
543
|
+
const issues = [];
|
|
544
|
+
if (brightness < DARK_THRESHOLD) issues.push("Image is too dark. Move to a well-lit area.");
|
|
545
|
+
if (brightness > BRIGHT_THRESHOLD)
|
|
546
|
+
issues.push("Image is overexposed. Avoid direct light or glare shining onto the card.");
|
|
547
|
+
const mrzFound = browserFoundMrz || hasMrz(grey, w, h);
|
|
548
|
+
if (mrzFound && laplacianVariance(grey, w, h, 0, w) < BLUR_THRESHOLD) {
|
|
549
|
+
issues.push("The back of the card is too blurry. Hold the camera steady so the lines of <<< are sharp.");
|
|
550
|
+
}
|
|
551
|
+
if (!mrzFound && issues.length === 0) issues.push(BACK_MRZ_NOT_FOUND_MESSAGE);
|
|
552
|
+
return { ok: issues.length === 0, issues, hasMrz: mrzFound };
|
|
553
|
+
}
|
|
554
|
+
async function checkBackPhotoQuality(photo) {
|
|
555
|
+
if (photo.size < MIN_FILE_BYTES) {
|
|
556
|
+
return { ok: false, hasMrz: false, issues: ["File is too small. This does not appear to be a real photo."] };
|
|
557
|
+
}
|
|
558
|
+
if (!await hasSupportedMagicBytes(photo)) {
|
|
559
|
+
return { ok: false, hasMrz: false, issues: ["Unsupported file type. Please upload a JPEG or PNG photo."] };
|
|
560
|
+
}
|
|
561
|
+
return new Promise((resolve) => {
|
|
562
|
+
const img = new Image();
|
|
563
|
+
const url = URL.createObjectURL(photo);
|
|
564
|
+
img.onerror = () => {
|
|
565
|
+
URL.revokeObjectURL(url);
|
|
566
|
+
resolve({ ok: false, hasMrz: false, issues: ["Could not read the image. Please try a different photo."] });
|
|
567
|
+
};
|
|
568
|
+
img.onload = async () => {
|
|
569
|
+
URL.revokeObjectURL(url);
|
|
570
|
+
const scale = Math.min(1, 640 / Math.max(img.width, img.height));
|
|
571
|
+
const w = Math.round(img.width * scale);
|
|
572
|
+
const h = Math.round(img.height * scale);
|
|
573
|
+
const canvas = document.createElement("canvas");
|
|
574
|
+
canvas.width = w;
|
|
575
|
+
canvas.height = h;
|
|
576
|
+
const ctx = canvas.getContext("2d");
|
|
577
|
+
ctx.imageSmoothingEnabled = false;
|
|
578
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
579
|
+
const textPromise = detectText(canvas);
|
|
580
|
+
const { data } = ctx.getImageData(0, 0, w, h);
|
|
581
|
+
const grey = new Float32Array(w * h);
|
|
582
|
+
for (let i = 0; i < grey.length; i++) {
|
|
583
|
+
grey[i] = 0.299 * data[i * 4] + 0.587 * data[i * 4 + 1] + 0.114 * data[i * 4 + 2];
|
|
584
|
+
}
|
|
585
|
+
const texts = await textPromise;
|
|
586
|
+
const browserFoundMrz = texts?.some((t) => looksLikeMrzLine(t.rawValue ?? "")) ?? false;
|
|
587
|
+
resolve(checkBackOfCard(grey, w, h, browserFoundMrz));
|
|
588
|
+
};
|
|
589
|
+
img.src = url;
|
|
590
|
+
});
|
|
591
|
+
}
|
|
490
592
|
async function checkPhotoQuality(photo, idType = "national_id") {
|
|
491
593
|
const issues = [];
|
|
492
594
|
if (photo.size < MIN_FILE_BYTES) {
|
|
493
|
-
issues.push("File is too small
|
|
595
|
+
issues.push("File is too small. This does not appear to be a real photo.");
|
|
494
596
|
return { ok: false, issues };
|
|
495
597
|
}
|
|
496
598
|
if (!await hasSupportedMagicBytes(photo)) {
|
|
497
|
-
issues.push("Unsupported file type
|
|
599
|
+
issues.push("Unsupported file type. Please upload a JPEG or PNG photo.");
|
|
498
600
|
return { ok: false, issues };
|
|
499
601
|
}
|
|
500
602
|
return new Promise((resolve) => {
|
|
@@ -502,7 +604,7 @@ async function checkPhotoQuality(photo, idType = "national_id") {
|
|
|
502
604
|
const url = URL.createObjectURL(photo);
|
|
503
605
|
img.onerror = () => {
|
|
504
606
|
URL.revokeObjectURL(url);
|
|
505
|
-
resolve({ ok: false, issues: ["Could not read the image
|
|
607
|
+
resolve({ ok: false, issues: ["Could not read the image. Please try a different photo."] });
|
|
506
608
|
};
|
|
507
609
|
img.onload = async () => {
|
|
508
610
|
URL.revokeObjectURL(url);
|
|
@@ -528,11 +630,11 @@ async function checkPhotoQuality(photo, idType = "national_id") {
|
|
|
528
630
|
}
|
|
529
631
|
const brightness = totalLum / len;
|
|
530
632
|
if (idType !== "passport" && laplacianVariance(grey, w, h, 0, w) < BLUR_THRESHOLD)
|
|
531
|
-
issues.push("Photo is too blurry
|
|
633
|
+
issues.push("Photo is too blurry. Hold the camera steady and make sure the document is in sharp focus.");
|
|
532
634
|
if (brightness < DARK_THRESHOLD)
|
|
533
|
-
issues.push("Image is too dark
|
|
635
|
+
issues.push("Image is too dark. Move to a well-lit area.");
|
|
534
636
|
if (brightness > BRIGHT_THRESHOLD)
|
|
535
|
-
issues.push("Image is overexposed
|
|
637
|
+
issues.push("Image is overexposed. Avoid direct light or glare shining onto the document.");
|
|
536
638
|
const totalTextRows = countTextRows(grey, w, h, 0, w);
|
|
537
639
|
const identityIssues = checkDocumentIdentity(grey, w, h, idType, totalTextRows);
|
|
538
640
|
issues.push(...identityIssues);
|
|
@@ -564,12 +666,14 @@ var fetchOf = (t) => t?.fetch ?? ((input, init) => fetch(input, init));
|
|
|
564
666
|
async function extractErrorMessage(res) {
|
|
565
667
|
const text = await res.text();
|
|
566
668
|
try {
|
|
567
|
-
const
|
|
669
|
+
const parsed = JSON.parse(text);
|
|
670
|
+
const detail = parsed.detail;
|
|
568
671
|
if (typeof detail === "string") return detail;
|
|
569
672
|
if (detail !== null && typeof detail === "object") {
|
|
570
673
|
const message = detail.message;
|
|
571
674
|
if (typeof message === "string") return message;
|
|
572
675
|
}
|
|
676
|
+
if (detail === void 0 && typeof parsed.message === "string") return parsed.message;
|
|
573
677
|
} catch {
|
|
574
678
|
}
|
|
575
679
|
return text;
|
|
@@ -604,13 +708,22 @@ function sdkHeaders(sdkToken, livenessFlows) {
|
|
|
604
708
|
}
|
|
605
709
|
async function startKYC(apiUrl, sdkToken, options, transport, signCapture2, livenessFlows) {
|
|
606
710
|
if (!await hasSupportedMagicBytes(options.idPhoto)) {
|
|
607
|
-
throw new ValidationError("Unsupported image format
|
|
711
|
+
throw new ValidationError("Unsupported image format. Only JPEG and PNG photos are accepted.");
|
|
712
|
+
}
|
|
713
|
+
if (options.idPhotoBack) {
|
|
714
|
+
if (options.idType === "passport") {
|
|
715
|
+
throw new ValidationError("idPhotoBack is only accepted for national IDs and driver's licences.");
|
|
716
|
+
}
|
|
717
|
+
if (!await hasSupportedMagicBytes(options.idPhotoBack)) {
|
|
718
|
+
throw new ValidationError("Unsupported image format for the back of the card. Only JPEG and PNG photos are accepted.");
|
|
719
|
+
}
|
|
608
720
|
}
|
|
609
721
|
const build = async () => {
|
|
610
722
|
const form = new FormData();
|
|
611
723
|
form.append("user_id", options.userId);
|
|
612
724
|
form.append("id_type", options.idType);
|
|
613
725
|
form.append("id_photo", options.idPhoto);
|
|
726
|
+
if (options.idPhotoBack) form.append("id_photo_back", options.idPhotoBack);
|
|
614
727
|
form.append("consent_given_at", options.consentGivenAt.toISOString());
|
|
615
728
|
form.append("processing_purpose", options.processingPurpose ?? "identity_verification");
|
|
616
729
|
const signature = signCapture2 ? await signCapture2() : null;
|
|
@@ -622,6 +735,13 @@ async function startKYC(apiUrl, sdkToken, options, transport, signCapture2, live
|
|
|
622
735
|
const res = await postIdempotent(fetchOf(transport), `${apiUrl}/v1/kyc/start`, build, transport?.retry);
|
|
623
736
|
return handleResponse(res);
|
|
624
737
|
}
|
|
738
|
+
async function getCaptureSettings(apiUrl, sdkToken, transport) {
|
|
739
|
+
const res = await fetchOf(transport)(`${apiUrl}/v1/kyc/capture-settings`, {
|
|
740
|
+
headers: { "Authorization": `Bearer ${sdkToken}` }
|
|
741
|
+
});
|
|
742
|
+
if (res.status === 404) return { document_back: "off" };
|
|
743
|
+
return handleResponse(res);
|
|
744
|
+
}
|
|
625
745
|
async function getLivenessCredentials(apiUrl, sdkToken, transport) {
|
|
626
746
|
const res = await fetchOf(transport)(`${apiUrl}/v1/kyc/liveness-credentials`, {
|
|
627
747
|
headers: { "Authorization": `Bearer ${sdkToken}` }
|
|
@@ -761,22 +881,26 @@ async function registerDeviceKey(fetchImpl, apiUrl, token, pair) {
|
|
|
761
881
|
});
|
|
762
882
|
return res.device_key_id;
|
|
763
883
|
}
|
|
764
|
-
function captureMessage(imageSha256Hex, userId, nonce, ts) {
|
|
884
|
+
function captureMessage(imageSha256Hex, userId, nonce, ts, backImageSha256Hex) {
|
|
885
|
+
if (backImageSha256Hex !== void 0) {
|
|
886
|
+
return `attestid-capture-v2|${imageSha256Hex}|${backImageSha256Hex}|${userId}|${nonce}|${ts}`;
|
|
887
|
+
}
|
|
765
888
|
return `attestid-capture-v1|${imageSha256Hex}|${userId}|${nonce}|${ts}`;
|
|
766
889
|
}
|
|
767
|
-
async function signCapture(fetchImpl, apiUrl, token, userId, photo) {
|
|
890
|
+
async function signCapture(fetchImpl, apiUrl, token, userId, photo, backPhoto) {
|
|
768
891
|
try {
|
|
769
892
|
if (!deviceBindingSupported()) return null;
|
|
770
893
|
const pair = await getDeviceKeyPair();
|
|
771
894
|
const deviceKeyId = await registerDeviceKey(fetchImpl, apiUrl, token, pair);
|
|
772
895
|
const imageHash = bytesToHex(await sha256(new Uint8Array(await photo.arrayBuffer())));
|
|
896
|
+
const backHash = backPhoto ? bytesToHex(await sha256(new Uint8Array(await backPhoto.arrayBuffer()))) : void 0;
|
|
773
897
|
const nonce = await challenge(fetchImpl, apiUrl, token);
|
|
774
898
|
const ts = Math.floor(Date.now() / 1e3);
|
|
775
899
|
const sig = new Uint8Array(
|
|
776
900
|
await subtle().sign(
|
|
777
901
|
{ name: "ECDSA", hash: "SHA-256" },
|
|
778
902
|
pair.privateKey,
|
|
779
|
-
buf(utf8(captureMessage(imageHash, userId, nonce, ts)))
|
|
903
|
+
buf(utf8(captureMessage(imageHash, userId, nonce, ts, backHash)))
|
|
780
904
|
)
|
|
781
905
|
);
|
|
782
906
|
return {
|
|
@@ -841,12 +965,12 @@ function assertSecureApiUrl(apiUrl) {
|
|
|
841
965
|
try {
|
|
842
966
|
url = new URL(apiUrl);
|
|
843
967
|
} catch {
|
|
844
|
-
throw new AttestIDError(`apiUrl must be a valid absolute URL
|
|
968
|
+
throw new AttestIDError(`apiUrl must be a valid absolute URL, got "${apiUrl}".`, "INSECURE_CONFIG");
|
|
845
969
|
}
|
|
846
970
|
if (url.protocol === "https:") return;
|
|
847
971
|
if (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname)) return;
|
|
848
972
|
throw new AttestIDError(
|
|
849
|
-
`apiUrl must use https
|
|
973
|
+
`apiUrl must use https://, got "${apiUrl}". Sending it over plain http exposes your API key, SDK token, and uploaded ID photos to anyone on the network path. http://localhost (or 127.0.0.1/::1) is allowed for local development against a backend running without TLS.`,
|
|
850
974
|
"INSECURE_CONFIG"
|
|
851
975
|
);
|
|
852
976
|
}
|
|
@@ -863,7 +987,7 @@ var AttestID = class {
|
|
|
863
987
|
assertSecureApiUrl(config.apiUrl);
|
|
864
988
|
if (isServerConfig(config) && !isTrustedServerEnvironment()) {
|
|
865
989
|
throw new AttestIDError(
|
|
866
|
-
"apiKey/accessKeyId must only be used in a trusted Node.js backend process
|
|
990
|
+
"apiKey/accessKeyId must only be used in a trusted Node.js backend process. A bundler would ship this long-lived secret to every client, letting anyone extract it. In browser or mobile app code, configure `getSdkToken` instead: a callback that fetches an already-minted sdk_token from your own backend, which holds apiKey server-side and calls POST /kyc/session. See the integration guide for the backend-proxy pattern.",
|
|
867
991
|
"INSECURE_CONFIG"
|
|
868
992
|
);
|
|
869
993
|
}
|
|
@@ -892,7 +1016,7 @@ var AttestID = class {
|
|
|
892
1016
|
const minted = sessionPromise.then((session) => {
|
|
893
1017
|
if (!session?.sdk_token) {
|
|
894
1018
|
throw new AuthError(
|
|
895
|
-
"getSdkToken() did not return a valid sdk_token
|
|
1019
|
+
"getSdkToken() did not return a valid sdk_token. Check that your backend's /api/attestid-session endpoint (or equivalent) returned a successful response."
|
|
896
1020
|
);
|
|
897
1021
|
}
|
|
898
1022
|
this.tokenExpiresAt = Date.now() + session.expires_in * 1e3;
|
|
@@ -922,16 +1046,25 @@ var AttestID = class {
|
|
|
922
1046
|
async startVerification(options) {
|
|
923
1047
|
if (!options.consentGivenAt) {
|
|
924
1048
|
throw new AttestIDError(
|
|
925
|
-
"consentGivenAt is required
|
|
1049
|
+
"consentGivenAt is required: capture the timestamp of an explicit user consent action before starting verification",
|
|
926
1050
|
"CONSENT_REQUIRED"
|
|
927
1051
|
);
|
|
928
1052
|
}
|
|
929
1053
|
const token = await this.getSdkToken(options.userId);
|
|
930
1054
|
const { apiUrl } = this.config;
|
|
931
1055
|
const fetchImpl = this.transport.fetch ?? ((input, init) => fetch(input, init));
|
|
932
|
-
const signer = this.deviceBinding ? () => signCapture(fetchImpl, apiUrl, token, options.userId, options.idPhoto) : void 0;
|
|
1056
|
+
const signer = this.deviceBinding ? () => signCapture(fetchImpl, apiUrl, token, options.userId, options.idPhoto, options.idPhotoBack) : void 0;
|
|
933
1057
|
return startKYC(apiUrl, token, options, this.transport, signer, this.livenessFlows);
|
|
934
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* The company's capture settings, set on the AttestID dashboard. Read
|
|
1061
|
+
* `document_back` before the document step to know whether to ask for the
|
|
1062
|
+
* back of a national ID / driver's licence (see DocumentBackPolicy).
|
|
1063
|
+
*/
|
|
1064
|
+
async getCaptureSettings(userId) {
|
|
1065
|
+
const token = await this.getSdkToken(userId);
|
|
1066
|
+
return getCaptureSettings(this.config.apiUrl, token, this.transport);
|
|
1067
|
+
}
|
|
935
1068
|
/**
|
|
936
1069
|
* Fetch temporary AWS credentials and the Cognito identity pool ID from the
|
|
937
1070
|
* backend. Call this alongside startVerification() for the same userId to
|
|
@@ -2248,6 +2381,7 @@ export {
|
|
|
2248
2381
|
buildHandoffUrl,
|
|
2249
2382
|
canRetryLiveness,
|
|
2250
2383
|
canonicalJson,
|
|
2384
|
+
checkBackPhotoQuality,
|
|
2251
2385
|
checkCaptureConditions,
|
|
2252
2386
|
checkPhotoQuality,
|
|
2253
2387
|
classifyConditions,
|