@sekyro/web 0.1.0-rc.7 → 0.1.0-rc.8

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +25 -16
  3. package/README.md +3 -5
  4. package/THIRD-PARTY-NOTICES.txt +154 -0
  5. package/dist/background-monitor-A7FKJOAG.js +1 -0
  6. package/dist/capture-session-doc-YBE3UMJJ.js +1 -0
  7. package/dist/{capture-session-v3-Okq0EQHG.d.ts → capture-session-v3-CQK-G-qX.d.ts} +72 -3
  8. package/dist/{capture-session-v3-UQU_WjO2.d.cts → capture-session-v3-Dvob-uH2.d.cts} +72 -3
  9. package/dist/chunk-4RS2YIL4.js +1 -0
  10. package/dist/chunk-7V2M6Y2W.js +1 -0
  11. package/dist/chunk-7ZCZY3ZP.js +1 -0
  12. package/dist/chunk-A3FUGHAH.js +2 -0
  13. package/dist/chunk-BPE5NSQ2.js +1 -0
  14. package/dist/chunk-CJHMFPL6.js +1 -0
  15. package/dist/chunk-DFTCAWMP.js +1 -0
  16. package/dist/chunk-DK5YPBDQ.js +1 -0
  17. package/dist/chunk-DQWW4OCK.js +1 -0
  18. package/dist/chunk-FHJ7HO3M.js +1 -0
  19. package/dist/chunk-G6LKLN4U.js +1 -0
  20. package/dist/chunk-GI3UFGRQ.js +1 -0
  21. package/dist/{chunk-IYUOYINS.js → chunk-LQ7C44JG.js} +1 -1
  22. package/dist/chunk-M43ET4NJ.js +1 -0
  23. package/dist/chunk-MXWLCDFX.js +1 -0
  24. package/dist/chunk-MY6EXEL2.js +1 -0
  25. package/dist/chunk-O5WKNZ2C.js +1 -0
  26. package/dist/chunk-OKWG3PVH.js +1 -0
  27. package/dist/chunk-POIQGEZM.js +1 -0
  28. package/dist/chunk-RI3E7GBN.js +1 -0
  29. package/dist/chunk-RNRFHUJA.js +1 -0
  30. package/dist/chunk-RVHAKRAI.js +1 -0
  31. package/dist/chunk-SMKCBUJL.js +1 -0
  32. package/dist/chunk-TDLIXFDT.js +1 -0
  33. package/dist/chunk-XLN5OT25.js +1 -0
  34. package/dist/chunk-YDQBVWXG.js +1 -0
  35. package/dist/chunk-YPG7CB4O.js +1 -0
  36. package/dist/client/index.cjs +1 -1
  37. package/dist/client/index.d.cts +13 -3
  38. package/dist/client/index.d.ts +13 -3
  39. package/dist/client/index.js +1 -1
  40. package/dist/core/index.cjs +1 -1
  41. package/dist/core/index.d.cts +3 -3
  42. package/dist/core/index.d.ts +3 -3
  43. package/dist/core/index.js +1 -1
  44. package/dist/core/ocr/worker.bundle.js +42 -0
  45. package/dist/core/ocr/worker.cjs +42 -0
  46. package/dist/core/ocr/worker.d.cts +84 -0
  47. package/dist/core/ocr/worker.d.ts +84 -0
  48. package/dist/core/ocr/worker.js +41 -0
  49. package/dist/core/v3/detect-worker.cjs +1 -1
  50. package/dist/core/v3/detect-worker.d.cts +1 -1
  51. package/dist/core/v3/detect-worker.d.ts +1 -1
  52. package/dist/core/v3/detect-worker.js +1 -1
  53. package/dist/create-document-capture-T4APHJMY.js +1 -0
  54. package/dist/create-portrait-capture-B57GE5FU.js +1 -0
  55. package/dist/create-verification-C0AVIcVW.d.ts +2787 -0
  56. package/dist/create-verification-DoFpgzok.d.cts +2787 -0
  57. package/dist/{detect-worker-CKXHlXx9.d.cts → detect-worker-DclNxkZT.d.cts} +6 -0
  58. package/dist/{detect-worker-CKXHlXx9.d.ts → detect-worker-DclNxkZT.d.ts} +6 -0
  59. package/dist/detector-scanic-YCDNLCGV.js +1 -0
  60. package/dist/icao-gate-D8qm1O2t.d.cts +1655 -0
  61. package/dist/icao-gate-D8qm1O2t.d.ts +1655 -0
  62. package/dist/icao-gate-PWBMKRGC.js +1 -0
  63. package/dist/icao-tuning-4GGVX6B4.js +1 -0
  64. package/dist/index.cjs +1 -1
  65. package/dist/index.d.cts +52 -6
  66. package/dist/index.d.ts +52 -6
  67. package/dist/index.js +1 -1
  68. package/dist/internal/index.cjs +1 -1
  69. package/dist/internal/index.d.cts +345 -308
  70. package/dist/internal/index.d.ts +345 -308
  71. package/dist/internal/index.js +1 -1
  72. package/dist/keyframe-rank-YAJLYJYF.js +1 -0
  73. package/dist/landmarker-KB7LVXAU.js +1 -0
  74. package/dist/mrz-band-VYHOTDHY.js +1 -0
  75. package/dist/plan-runner-UBK4B4CG.js +1 -0
  76. package/dist/segmenter-USLNBX65.js +1 -0
  77. package/dist/turnstile-FATNZ4AG.js +1 -0
  78. package/package.json +22 -4
  79. package/sbom.cdx.json +8 -8
  80. package/dist/chunk-B45J4KC3.js +0 -1
  81. package/dist/chunk-BMA36W2W.js +0 -1
  82. package/dist/chunk-P7ECRN47.js +0 -1
  83. package/dist/chunk-YOB4ZTYN.js +0 -1
  84. package/dist/chunk-YQBK6OIM.js +0 -1
  85. package/dist/create-verification-C2JhOO6Y.d.cts +0 -533
  86. package/dist/create-verification-CGazwtAh.d.ts +0 -533
  87. package/dist/detector-scanic-VQFFXKTL.js +0 -1
  88. package/dist/hosted-client-kBeq-mnJ.d.cts +0 -602
  89. package/dist/hosted-client-kBeq-mnJ.d.ts +0 -602
@@ -0,0 +1,1655 @@
1
+ interface Point {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ interface Box {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+ /** The face geometry the gate needs, in SOURCE (stream) pixels. */
12
+ interface FaceGeometry {
13
+ /** Eye centres, ordered by image x (`left` = smaller x). */
14
+ leftEye: Point;
15
+ rightEye: Point;
16
+ /** Inter-ocular distance (px). */
17
+ iod: number;
18
+ /** Bounding box of the landmark mesh (forehead to chin). */
19
+ box: Box;
20
+ /** Nose tip (landmark 1): splits the face into halves for the lighting ratio. */
21
+ nose: Point;
22
+ /** Eye-contour boxes (glare), same order as the eyes. */
23
+ leftEyeBox: Box;
24
+ rightEyeBox: Box;
25
+ }
26
+ /**
27
+ * Normalised Face Landmarker landmarks (0..1) → source-pixel geometry.
28
+ * `null` when the mesh is too short to carry the eyes.
29
+ */
30
+ declare function faceGeometry(landmarks: readonly Point[], width: number, height: number): FaceGeometry | null;
31
+ interface HeadPose {
32
+ yaw: number;
33
+ pitch: number;
34
+ roll: number;
35
+ }
36
+ /**
37
+ * Yaw / pitch / roll (degrees) of the Face Landmarker's facial transformation
38
+ * matrix (4×4, `layout` of `data`; MediaPipe emits column-major). Rotation
39
+ * order R = Rz(roll)·Ry(yaw)·Rx(pitch): yaw turns about the vertical axis,
40
+ * pitch nods, roll tilts. Only magnitudes are gated, so sign conventions never
41
+ * change a verdict. `null` for a malformed matrix.
42
+ */
43
+ declare function poseFromMatrix(data: readonly number[], layout?: "column" | "row"): HeadPose | null;
44
+ /** Roll from the eye line (degrees) — the fallback when no matrix is available. */
45
+ declare function eyeLineRoll(g: Pick<FaceGeometry, "leftEye" | "rightEye">): number;
46
+ /** The ICAO canvas (icao.py): 480×640, eye midpoint at x = 240, eye line at y = 289, IOD 120–160. */
47
+ declare const ICAO_CANVAS: {
48
+ readonly width: 480;
49
+ readonly height: 640;
50
+ readonly eyeX: 240;
51
+ readonly eyeY: 289;
52
+ readonly iodMin: 120;
53
+ readonly iodMax: 160;
54
+ };
55
+ /**
56
+ * The source-pixel window the server's ICAO crop is cut from: scale 1 when the
57
+ * IOD already lies in 120–160, else the scale that brings it to 120 (icao.py:
58
+ * "120 when outside the band"), the eye midpoint at (240, 289) of the window.
59
+ */
60
+ declare function icaoWindow(g: Pick<FaceGeometry, "leftEye" | "rightEye" | "iod">): Box;
61
+ /** How far a window spills outside the frame (0 = inside), as a fraction of its size. */
62
+ declare function windowOverflow(window: Box, frameWidth: number, frameHeight: number): number;
63
+ /**
64
+ * The region of the full-resolution still the SDK uploads: the ICAO window
65
+ * grown by `margin` about its centre (so the server re-crops with its own
66
+ * landmarks), at least `minWidth × minHeight`, 3:4 portrait, then fitted into
67
+ * the frame — the margin shrinks first, then the box slides inside, and only
68
+ * a window larger than the frame is cut to it. Integer pixels.
69
+ */
70
+ declare function portraitCropRegion(g: Pick<FaceGeometry, "leftEye" | "rightEye" | "iod">, frameWidth: number, frameHeight: number, opts: {
71
+ margin: number;
72
+ minWidth: number;
73
+ minHeight: number;
74
+ }): Box;
75
+
76
+ /** Server-tunable thresholds (`capture.icao.backgroundCheck`); defaults below. */
77
+ interface BackgroundCheckTuning {
78
+ /** Run the check at all (ICAO on). Default `true`. */
79
+ enabled: boolean;
80
+ /** Measurements per second (the face gate runs faster). Default 3. */
81
+ hz: number;
82
+ /** The segmenter must load within this window, else the strip fallback. Default 8000 ms. */
83
+ segmenterTimeoutMs: number;
84
+ /** A segmentation slower than this (moving average) switches to the strip fallback. Default 120 ms. */
85
+ segmenterBudgetMs: number;
86
+ /** Person confidence at or above this is the person (excluded). Default 0.5. */
87
+ maskThreshold: number;
88
+ /** Max RMS colour standard deviation (0..255). Default 28. */
89
+ maxColorStd: number;
90
+ /** Max share of textured neighbour pairs (0..1). Default 0.08. */
91
+ maxEdgeDensity: number;
92
+ /** Luma step (0..255) that counts as an edge. Default 24. */
93
+ edgeLuma: number;
94
+ /** Max left/right or top/bottom brightness difference (0..1). Default 0.18. */
95
+ maxGradient: number;
96
+ /** Fewer background pixels than this = not measurable (never blocks). Default 300. */
97
+ minPixels: number;
98
+ /** A non-uniform background holds the shot at most this long, then the shot proceeds. Default 4000 ms. */
99
+ timeoutMs: number;
100
+ }
101
+ /** Person confidence per pixel (0..1 floats, or 0..255 bytes), any resolution (scaled to the frame). */
102
+ interface PersonMask {
103
+ data: Float32Array | Uint8Array | Uint8ClampedArray;
104
+ width: number;
105
+ height: number;
106
+ }
107
+ /** One measurement — numbers only (telemetry and the `gate` block). */
108
+ interface BackgroundUniformity {
109
+ method: "segmenter" | "strip";
110
+ uniform: boolean;
111
+ colorStd: number;
112
+ edgeDensity: number;
113
+ gradient: number;
114
+ /** Background pixels measured (after sampling). */
115
+ pixels: number;
116
+ }
117
+
118
+ /** The device (coaching) limits of each profile — the server limits moved towards strict. */
119
+ declare const ICAO_DEVICE_LIMITS: {
120
+ readonly id_document: {
121
+ readonly pose: {
122
+ readonly yawDeg: 7;
123
+ readonly pitchDeg: 7;
124
+ readonly rollDeg: 5;
125
+ readonly pitchOffsetDeg: 4;
126
+ };
127
+ readonly eyes: {
128
+ readonly relativeMin: 0.62;
129
+ readonly blinkMax: 0.48;
130
+ };
131
+ readonly mouth: {
132
+ readonly jawOpenMax: 0.13;
133
+ readonly smileMax: 0.28;
134
+ readonly puckerMax: 0.28;
135
+ };
136
+ readonly lighting: {
137
+ readonly faceLuma: readonly [95, 185];
138
+ readonly halfRatio: readonly [0.77, 1.31];
139
+ readonly clippedMax: 0.015;
140
+ };
141
+ readonly background: {
142
+ readonly required: true;
143
+ readonly lumaMin: 165;
144
+ readonly lumaSdMax: 20;
145
+ };
146
+ readonly resolution: {
147
+ readonly minIodPx: 110;
148
+ readonly minWidth: 480;
149
+ readonly minHeight: 640;
150
+ };
151
+ };
152
+ readonly selfie: {
153
+ readonly pose: {
154
+ readonly yawDeg: 7;
155
+ readonly pitchDeg: 7;
156
+ readonly rollDeg: 7;
157
+ readonly pitchOffsetDeg: 4;
158
+ };
159
+ readonly eyes: {
160
+ readonly relativeMin: 0.62;
161
+ readonly blinkMax: 0.48;
162
+ };
163
+ readonly mouth: {
164
+ readonly jawOpenMax: 0.13;
165
+ readonly smileMax: 0.28;
166
+ readonly puckerMax: 0.28;
167
+ };
168
+ readonly lighting: {
169
+ readonly faceLuma: readonly [95, 185];
170
+ readonly halfRatio: readonly [0.77, 1.31];
171
+ readonly clippedMax: 0.015;
172
+ };
173
+ readonly background: {
174
+ readonly required: false;
175
+ readonly lumaMin: 165;
176
+ readonly lumaSdMax: 20;
177
+ };
178
+ readonly resolution: {
179
+ readonly minIodPx: 100;
180
+ readonly minWidth: 480;
181
+ readonly minHeight: 640;
182
+ };
183
+ };
184
+ };
185
+
186
+ /** The ICAO threshold set (`id_document`: ID photo; `selfie`: no background rule). */
187
+ type IcaoProfile = keyof typeof ICAO_DEVICE_LIMITS;
188
+ /** `capture.icao` as the server sends it (every field optional on the wire). */
189
+ interface IcaoCaptureTuning {
190
+ /** The threshold set these limits come from (default `id_document`; it also picks the fallbacks). */
191
+ profile?: "id_document" | "selfie";
192
+ yawDeg?: number;
193
+ pitchDeg?: number;
194
+ /**
195
+ * Subtracted from the measured pitch before `pitchDeg` applies (degrees). Where
196
+ * "looking straight" sits relative to the Landmarker's zero; kyc-portrait applies the
197
+ * same `PORTRAIT_POSE_PITCH_OFFSET_DEG`, so keep them equal (D9a, 2026-10-22).
198
+ */
199
+ pitchOffsetDeg?: number;
200
+ rollDeg?: number;
201
+ holdMs?: number;
202
+ /** `alert` policy: the clip starts this long after the face was accepted even if the ICAO gate is not ready (default 3000). */
203
+ alertGraceMs?: number;
204
+ eyes?: {
205
+ relativeMin?: number;
206
+ blinkMax?: number;
207
+ earMin?: number;
208
+ };
209
+ mouth?: {
210
+ jawOpenMax?: number;
211
+ smileMax?: number;
212
+ puckerMax?: number;
213
+ };
214
+ lighting?: {
215
+ faceLuma?: [number, number];
216
+ halfRatio?: [number, number];
217
+ clippedMax?: number;
218
+ };
219
+ /** `required: false` (the `selfie` profile): the background is not evaluated, coached or reported. */
220
+ background?: {
221
+ required?: boolean;
222
+ lumaMin?: number;
223
+ lumaSdMax?: number;
224
+ };
225
+ resolution?: {
226
+ minIodPx?: number;
227
+ minWidth?: number;
228
+ minHeight?: number;
229
+ };
230
+ /** Client-only knobs (no server field today; read when the server adds them). */
231
+ holdFrames?: number;
232
+ cadenceHz?: number;
233
+ baseline?: {
234
+ windowMs?: number;
235
+ minSamples?: number;
236
+ percentile?: number;
237
+ };
238
+ sharpnessMin?: number;
239
+ glare?: {
240
+ specularMax?: number;
241
+ };
242
+ softGraceMs?: number;
243
+ cropMargin?: number;
244
+ camera?: {
245
+ width?: number;
246
+ height?: number;
247
+ };
248
+ /**
249
+ * The client background-uniformity check (ID mode; defaults in
250
+ * core/portrait/background-uniformity.ts `BACKGROUND_CHECK_DEFAULTS`).
251
+ * Skipped entirely when `background.required` is false.
252
+ */
253
+ backgroundCheck?: Partial<BackgroundCheckTuning>;
254
+ }
255
+ /** Fully resolved limits the gate runs on. */
256
+ interface ResolvedIcaoTuning {
257
+ profile: IcaoProfile;
258
+ yawDeg: number;
259
+ pitchDeg: number;
260
+ pitchOffsetDeg: number;
261
+ rollDeg: number;
262
+ holdMs: number;
263
+ alertGraceMs: number;
264
+ eyes: {
265
+ relativeMin: number;
266
+ blinkMax: number;
267
+ };
268
+ mouth: {
269
+ jawOpenMax: number;
270
+ smileMax: number;
271
+ puckerMax: number;
272
+ };
273
+ lighting: {
274
+ faceLuma: [number, number];
275
+ halfRatio: [number, number];
276
+ clippedMax: number;
277
+ };
278
+ background: {
279
+ required: boolean;
280
+ lumaMin: number;
281
+ lumaSdMax: number;
282
+ };
283
+ resolution: {
284
+ minIodPx: number;
285
+ minWidth: number;
286
+ minHeight: number;
287
+ };
288
+ holdFrames: number;
289
+ cadenceHz: number;
290
+ baseline: {
291
+ windowMs: number;
292
+ minSamples: number;
293
+ percentile: number;
294
+ };
295
+ sharpnessMin: number;
296
+ glare: {
297
+ specularMax: number;
298
+ };
299
+ softGraceMs: number;
300
+ cropMargin: number;
301
+ camera: {
302
+ width: number;
303
+ height: number;
304
+ };
305
+ backgroundCheck: BackgroundCheckTuning;
306
+ /** The server's `capture.tuningVersion`, echoed on submit (`null` when absent). */
307
+ tuningVersion: string | null;
308
+ }
309
+ /** Bundled fallbacks: the `id_document` device limits (never looser than the server). */
310
+ declare const ICAO_FALLBACK: Omit<ResolvedIcaoTuning, "tuningVersion">;
311
+ /**
312
+ * Resolve `capture.icao` (any shape, possibly absent) over {@link ICAO_FALLBACK},
313
+ * field by field: an out-of-range or malformed field keeps its fallback.
314
+ */
315
+ declare function resolveIcaoTuning(raw: unknown, tuningVersion?: unknown): ResolvedIcaoTuning;
316
+
317
+ /** Session lifecycle status (spec §4 two-field status/outcome model). */
318
+ type SessionStatus = "created" | "in_progress" | "review_pending" | "completed" | "expired" | "failed";
319
+ /** Terminal verdict, orthogonal to `status`; `null` until terminal/review. */
320
+ type SessionOutcome = "approved" | "review" | "rejected";
321
+ /** Per-check seven-state machine (spec §4.3). */
322
+ type CheckState = "pending" | "input_required" | "processing" | "passed" | "failed" | "review" | "skipped";
323
+ /**
324
+ * The frozen check-type vocabulary (CHECK_TYPES). `phone_otp` joined in Task 13;
325
+ * `nfc_chip` (M-nfc) is read by the native SDKs only; `email_otp` (Plan 8,
326
+ * profile v2 §4.6.5) confirms an email address with a one-time code;
327
+ * `eid_auth` (SDK spec §3.2) is the national eID sign-in (Houwiyeti in MR).
328
+ */
329
+ type CheckType = "liveness" | "face_match" | "doc_ocr" | "population_registry" | "phone_otp" | "nfc_chip" | "email_otp" | "portrait_icao" | "eid_auth";
330
+ type LivenessReason = "no_face" | "media_unreadable" | "clip_too_short" | "spoof_detected" | "review_band" | "liveness_inconclusive" | "device_integrity_failed" | "mobile_required" | "uncalibrated_capture_channel";
331
+ type PopulationReason = "nni_not_found" | "nni_invalid" | "registry_unavailable" | "result_from_cache" | "nationality_not_allowed" | "underage" | "age_unverifiable" | "name_mismatch" | "birth_date_mismatch" | "nationality_mismatch" | "nni_recheck";
332
+ type FaceMatchReason = "no_face_source" | "no_face_reference" | "low_quality_reference" | "below_threshold" | "review_band" | "match_inconclusive" | "subject_revoked" | "probe_not_from_liveness";
333
+ type DocReason = "side_missing" | "mrz_checksum_failed" | "unreadable" | "expired_document" | "authenticity_review" | "document_authenticity" | "registry_mismatch" | "document_nni_mismatch" | "nationality_unreadable" | "document_type_not_accepted" | "nationality_not_allowed" | "underage" | "age_unverifiable" | "name_mismatch" | "birth_date_mismatch" | "nationality_mismatch" | "document_number_mismatch";
334
+ /**
335
+ * Profile v2 step 3 matching reasons: a field the partner sent at session
336
+ * create (`subject`) differs from the reference identity (document or
337
+ * registry). All four are final. `document_number_mismatch` is `doc_ocr` only.
338
+ */
339
+ type MatchReason = "name_mismatch" | "birth_date_mismatch" | "nationality_mismatch" | "document_number_mismatch";
340
+ /**
341
+ * Profile v2 step 4 decision-mode reasons (the app decides automatically
342
+ * instead of sending the case to human review). These park the step at
343
+ * `input_required` while attempts remain: `liveness_inconclusive` /
344
+ * `match_inconclusive` (middle score band) and `document_authenticity`
345
+ * (retake the original card). `mobile_required` is kept for compatibility:
346
+ * the backend now answers a non-phone capture with a request-level
347
+ * `409 mobile_required` (continue on a phone; nothing used, not billed).
348
+ */
349
+ type DecisionReason = "liveness_inconclusive" | "match_inconclusive" | "document_authenticity" | "mobile_required";
350
+ /**
351
+ * phone_otp reasons (Task 13). All transient — the caller fixes them by
352
+ * re-entering / resending: `otp_mismatch` (wrong or missing code),
353
+ * `otp_expired` (code lapsed or no active send), `otp_send_failed` (the OTP
354
+ * service was unreachable / a send returned no token).
355
+ */
356
+ type PhoneOtpReason = "otp_mismatch" | "otp_expired" | "otp_send_failed" | "otp_channel_unavailable" | "otp_destination_not_allowed" | "otp_rate_limited" | "otp_destination_unknown";
357
+ /**
358
+ * email_otp reasons (Plan 8, profile v2 §4.6.5). All transient: a wrong or
359
+ * expired code, a send outage, too many sends, an address that cannot receive
360
+ * mail (`email_invalid`), or a disposable address the app blocks
361
+ * (`email_disposable`) — the person enters another address or resends.
362
+ */
363
+ type EmailOtpReason = "otp_mismatch" | "otp_expired" | "otp_send_failed" | "otp_rate_limited" | "email_invalid" | "email_disposable";
364
+ /**
365
+ * nfc_chip reasons (M-nfc, SDK spec §4.3). Native SDKs read the chip; the web
366
+ * never does — on web the step arrives `skipped` (`platform_unsupported`) or
367
+ * the session shows `failureReason: 'platform_unsupported'`.
368
+ */
369
+ type NfcChipReason = "chip_read_failed" | "chip_access_denied" | "chip_parse_failed" | "chip_not_present" | "nfc_unavailable" | "chip_document_mismatch" | "chip_integrity_failed" | "registry_mismatch" | "document_nni_mismatch" | "platform_unsupported";
370
+ /**
371
+ * eid_auth reasons (SDK spec §3.2, generic national eID; Houwiyeti in MR).
372
+ * Retryable: `eid_identifier_invalid` (re-enter the number), `eid_code_invalid`
373
+ * (re-enter the code, see `eid.codeAttemptsRemaining`), `eid_code_expired`
374
+ * (ask for a new code), `eid_unavailable` (the provider is down; try again or
375
+ * use the camera), `eid_resend_too_soon` (wait for `eid.resendAvailableAt`),
376
+ * `eid_resend_limit` (no more codes; type the last code received).
377
+ * Final: `eid_locked` (too many wrong codes), `eid_not_enrolled` (the number
378
+ * has no account in the provider's app).
379
+ * With `eid_not_enrolled`, `eid_unavailable` or `eid_locked` the person may
380
+ * switch to the camera path when the session offers it.
381
+ */
382
+ type EidReason = "eid_identifier_invalid" | "eid_code_invalid" | "eid_code_expired" | "eid_locked" | "eid_not_enrolled" | "eid_unavailable" | "eid_resend_too_soon" | "eid_resend_limit";
383
+ /**
384
+ * Any frozen reason code. The backend declares the field `string`, but the
385
+ * taxonomy is frozen (do-not-drift) — the union IS the contract. NB: liveness
386
+ * and face_match both carry `review_band`.
387
+ */
388
+ type CheckReason = LivenessReason | PopulationReason | FaceMatchReason | DocReason | PhoneOtpReason | EmailOtpReason | NfcChipReason | PortraitIcaoReason | EidReason;
389
+ /**
390
+ * Session-level failure reasons: the session was stopped by a policy gate,
391
+ * not by a failed check. All are terminal (no retry on this session):
392
+ * - `location_not_allowed` / `anonymizer_blocked` (profile v2 step 2): the
393
+ * person's IP location is not allowed, or an anonymizer (VPN / proxy / Tor)
394
+ * was detected while the app blocks them;
395
+ * - `capture_tampered` (step 4): the device reported a tampered capture
396
+ * (virtual camera, hooking, emulator…) while the app blocks it;
397
+ * - `device_velocity_exceeded` (step 4): this device already verified too
398
+ * many different people while the app blocks it;
399
+ * - `device_cooldown` (step 4): this device had too many rejected sessions —
400
+ * {@link HostedSessionView.retryAfter} says when it may try again;
401
+ * - `platform_unsupported` (M-nfc): a chip read is required and this client
402
+ * can't read one (web); the session stays open for a native app.
403
+ */
404
+ type SessionFailureReason = "location_not_allowed" | "anonymizer_blocked" | "capture_tampered" | "device_velocity_exceeded" | "device_cooldown" | "platform_unsupported";
405
+ /**
406
+ * The profile-v2 policy reasons a session can end on: the two session-level
407
+ * reasons plus the failed check's policy reasons (`document_type_not_accepted`,
408
+ * `nationality_not_allowed`, `underage`, `age_unverifiable`,
409
+ * `document_nni_mismatch`).
410
+ */
411
+ type PolicyFailureReason = SessionFailureReason | "document_type_not_accepted" | "nationality_not_allowed" | "underage" | "age_unverifiable" | "document_nni_mismatch";
412
+ /**
413
+ * How the person proves their identity when the app makes the document
414
+ * optional for registry nationals (profile v2 step 3): type their national
415
+ * number (NNI) and skip the document, or photograph their document.
416
+ * `document_chip` is offered only to native clients with NFC (M-nfc).
417
+ * `national_eid` (SDK spec §3.2) signs in with the country's national eID
418
+ * (Houwiyeti in MR) instead of the camera path — see
419
+ * {@link MethodChoiceView.nationalEid}.
420
+ */
421
+ type IdentityMethod = "national_number" | "document_photo" | "document_chip" | "national_eid";
422
+ /**
423
+ * The national eID provider a session offers (`MethodChoiceView.nationalEid`).
424
+ * `provider` is `{ISO2}.{provider}` (e.g. `MR.houwiyeti`); `providerName` is
425
+ * the display name ("Houwiyeti").
426
+ */
427
+ interface NationalEidOption {
428
+ provider: string;
429
+ providerName: string;
430
+ /** ISO 3166-1 alpha-2 country of the provider. */
431
+ country: string;
432
+ /**
433
+ * What follows a passed eID (flow model 2026-10-23): `end` (the session ends,
434
+ * the default) or `continue` (the live photo follows). Absent on older APIs = `end`.
435
+ */
436
+ afterPass?: EidAfterPass;
437
+ }
438
+ /** What follows a passed national eID (`nationalEid.afterPass`). */
439
+ type EidAfterPass = "end" | "continue";
440
+ /**
441
+ * The section a step belongs to (flow model 2026-10-23, SDK spec §3.7): Step 1
442
+ * `contact`, Step 2 `identity`, Step 3 `live_photo`. Open: a value this SDK
443
+ * does not know is kept as is, so a client still places the step.
444
+ */
445
+ type StepSection = "contact" | "identity" | "live_photo" | (string & {});
446
+ /** A contact channel of a `one_of` session (`otpChoice.options`). */
447
+ type ContactChannel = "phone" | "email";
448
+ /**
449
+ * Step 1 contact choice (`HostedSessionView.otpChoice`), present only on a
450
+ * `contact.mode = one_of` session: the person confirms a phone number OR an
451
+ * email address. `selected` is `null` until they choose
452
+ * ({@link HostedClient.selectContact}, or implicitly with the first OTP
453
+ * submission); `locked` once a contact step passed.
454
+ */
455
+ interface OtpChoiceView {
456
+ options: ContactChannel[];
457
+ selected: ContactChannel | null;
458
+ locked: boolean;
459
+ }
460
+ /** Where the identity summary's data came from. */
461
+ type IdentitySummarySource = "registry" | "document" | "chip";
462
+ /**
463
+ * Step 2b summary and confirm (`HostedSessionView.identitySummary`): the
464
+ * identity data read so far, shown to the person before the live photo. Only
465
+ * on the session-token view and only while the confirmation is pending; masks
466
+ * keep the last 4 characters. Never log or store these values.
467
+ */
468
+ interface IdentitySummaryView {
469
+ /**
470
+ * `full` (document / chip: the record and the photo) or `partial` (the
471
+ * registry from a typed NNI: the short name "Mohamed S.", the birth year,
472
+ * the NNI's last 4, no photo).
473
+ */
474
+ kind: "full" | "partial" | (string & {});
475
+ source: IdentitySummarySource | (string & {});
476
+ /** The full name (`full`), or the short form "Mohamed S." (`partial`). */
477
+ fullName?: string;
478
+ /** `YYYY-MM-DD` (`full`). */
479
+ birthDate?: string;
480
+ /** `YYYY` (`partial`). */
481
+ birthYear?: string;
482
+ /** ISO 3166-1 alpha-2 (or alpha-3) nationality. */
483
+ nationality?: string;
484
+ documentType?: string;
485
+ documentNumberMasked?: string;
486
+ nniMasked?: string;
487
+ /** `YYYY-MM-DD`. */
488
+ expiryDate?: string;
489
+ /** A portrait can be fetched ({@link HostedClient.identityPortrait}). */
490
+ portrait: {
491
+ available: boolean;
492
+ };
493
+ /** A retake is still possible (attempts left, not a `supplied` summary). */
494
+ retakeAvailable: boolean;
495
+ /** What a retake re-opens. */
496
+ retakeTarget?: "document" | "national_number" | (string & {});
497
+ }
498
+ /** The answer to the identity summary ({@link HostedClient.confirmIdentity}). */
499
+ type IdentityConfirmDecision = "confirm" | "retake";
500
+ /**
501
+ * The identity-method choice of a session (`HostedSessionView.methodChoice`).
502
+ * While `available && selected === null` no check can be submitted (409
503
+ * `method_required`): the page asks first and sends the answer with
504
+ * `POST /hosted/method` ({@link HostedClient.selectMethod}).
505
+ */
506
+ interface MethodChoiceView {
507
+ /** The session offers the choice (the app's document mode is `optional`). */
508
+ available: boolean;
509
+ /** The chosen method, or `null` while the person has not chosen. */
510
+ selected: IdentityMethod | null;
511
+ /** The methods to offer, in display order (`national_eid` first when offered). */
512
+ options: IdentityMethod[];
513
+ /**
514
+ * The national eID provider behind the `national_eid` option ("Verify with
515
+ * Houwiyeti"). Absent when the option is not offered (and on older APIs).
516
+ */
517
+ nationalEid?: NationalEidOption;
518
+ }
519
+ /** A document type the backend reads (kyc-docs-v2 vocabulary; `carte_identite` = identity card of any issuer). */
520
+ type AcceptedDocumentType = "carte_identite" | "passport" | "residence_permit";
521
+ /**
522
+ * Accepted document types per issuing country (profile v2 §5.3a): an issuer
523
+ * uses `byIssuer[iso2] ?? default`; `"*"` = every type.
524
+ */
525
+ interface HostedAcceptedDocuments {
526
+ default: "*" | AcceptedDocumentType[];
527
+ byIssuer?: Record<string, AcceptedDocumentType[]>;
528
+ }
529
+ /**
530
+ * phone_otp DISPLAY-SAFE surface (Task 16). The backend stashes the OTP
531
+ * service `verifToken` on the check's persisted outputs (the doc_ocr partial
532
+ * mechanism), but that token is NEVER projected here — only the fields the
533
+ * hosted OTP step renders. `numberSource` (org-authored config) drives the
534
+ * send-phase UI (prefilled display vs entry); the rest appear ONLY once a send
535
+ * has parked the check (`sentToMasked` present ⇒ verify phase).
536
+ */
537
+ interface HostedOtpView {
538
+ /**
539
+ * Per-step `config.numberSource` — drives the send-phase UI (prefilled
540
+ * display vs entry). Narrowed to the frontend contract; the backend field is
541
+ * `string` (dynamic config) but the projection only ever emits these three.
542
+ */
543
+ numberSource?: "prefilled" | "user_entered" | "prefilled_or_entered";
544
+ /** Masked target the last send went to (e.g. `+222 ** ** 34 56`); absent pre-send. */
545
+ sentToMasked?: string;
546
+ /** Delivery channel of the last send. */
547
+ channel?: "sms" | "whatsapp";
548
+ /** ISO-8601 OTP expiry from the last send — drives the verify-phase countdown. */
549
+ expiresAt?: string;
550
+ /**
551
+ * Profile v2 §4.6.4: the channels the person may choose from, in display
552
+ * order (WhatsApp first; a channel the org or app turned off is absent).
553
+ * Absent on a session created before channel choice (no picker; SMS).
554
+ */
555
+ channels?: ("whatsapp" | "sms")[];
556
+ /** ISO-8601 end of the server-side resend cooldown (after a send). */
557
+ resendAvailableAt?: string;
558
+ }
559
+ /**
560
+ * email_otp DISPLAY-SAFE surface (Plan 8, profile v2 §4.6.5): never the
561
+ * provider handle, never the address (only its mask).
562
+ */
563
+ interface HostedEmailOtpView {
564
+ /** Where the address comes from; drives the send-phase UI (notice vs entry). */
565
+ emailSource?: "prefilled" | "user_entered" | "prefilled_or_entered";
566
+ /** Masked target of the last send (e.g. `a•••@e•••.com`); absent pre-send. */
567
+ sentToMasked?: string;
568
+ /** ISO-8601 code expiry from the last send. */
569
+ expiresAt?: string;
570
+ /** ISO-8601 end of the server-side resend cooldown (after a send). */
571
+ resendAvailableAt?: string;
572
+ }
573
+ /**
574
+ * portrait_icao reasons (SDK spec §4.9, ID mode). The first failing ICAO
575
+ * criterion; every one is retryable (re-run only the ICAO gate).
576
+ * `media_unreadable` (no report) is shared with liveness.
577
+ */
578
+ type PortraitIcaoReason = "icao_pose" | "icao_eyes" | "icao_mouth" | "icao_occlusion" | "icao_lighting" | "icao_background" | "icao_resolution" | "portrait_not_from_liveness";
579
+ /** One ICAO criterion of the server's portrait report (SDK spec §4.9). */
580
+ type IcaoCriterion = "pose" | "eyes" | "mouth" | "occlusion" | "glare" | "lighting" | "background" | "resolution";
581
+ /** The ICAO policy of a session's selfie (spec 2026-10-24 selfie ICAO policy §10). */
582
+ interface HostedIcaoPolicy {
583
+ /** `reject`: the gate blocks and the server asks for retakes; `alert`: coaching only, never blocking. */
584
+ policy: "reject" | "alert";
585
+ /** The threshold set: `id_document` (ID photo, uniform background) or `selfie` (no background rule). */
586
+ profile: "id_document" | "selfie";
587
+ }
588
+ /** `HostedSessionView.portrait`. */
589
+ type HostedPortraitView = NonNullable<HostedSessionView["portrait"]>;
590
+ /** One row of the portrait_icao report: the server's measurement — coaching only. */
591
+ interface IcaoReportEntry {
592
+ criterion: IcaoCriterion;
593
+ value: number | null;
594
+ threshold: number | string | null;
595
+ pass: boolean;
596
+ }
597
+ /**
598
+ * eid_auth DISPLAY-SAFE surface (SDK spec §3.2): never the identifier, never
599
+ * the code. `awaiting` drives the screen: `identifier` (type the national
600
+ * number, unless `identifierBound`) → `code` (type the code shown in the
601
+ * provider's app).
602
+ */
603
+ interface HostedEidView {
604
+ /** `{ISO2}.{provider}`, e.g. `MR.houwiyeti`. */
605
+ provider: string;
606
+ /** Display name, e.g. "Houwiyeti". */
607
+ providerName: string;
608
+ /** ISO 3166-1 alpha-2 country of the provider. */
609
+ country: string;
610
+ /** `app_code`: the provider's app shows a code the person types here (the only challenge today). */
611
+ challenge: "app_code";
612
+ awaiting: "identifier" | "code";
613
+ /** The session is bound to the national number — `start` sends no identifier. */
614
+ identifierBound: boolean;
615
+ /** ISO-8601 expiry of the current code. */
616
+ expiresAt?: string;
617
+ /** ISO-8601 time from which a new code may be asked for. */
618
+ resendAvailableAt?: string;
619
+ /** New codes the person may still ask for in this session. */
620
+ resendsRemaining?: number;
621
+ /** Wrong codes left before `eid_locked`. */
622
+ codeAttemptsRemaining?: number;
623
+ /** What follows a pass (flow model 2026-10-23); absent = `end`. */
624
+ afterPass?: EidAfterPass;
625
+ }
626
+ /** `nfc_chip` step hints (M-nfc). `chipHint` is UI copy only, never a gate. */
627
+ interface NfcStepView {
628
+ mode: "preferred" | "mandatory";
629
+ access: ("PACE" | "BAC")[];
630
+ optionalDataGroups: ("DG11" | "DG12" | "DG13")[];
631
+ chipHint: {
632
+ default: Record<string, true | "unknown">;
633
+ byIssuer: Record<string, Record<string, true | "unknown">>;
634
+ };
635
+ }
636
+ /**
637
+ * Per-check projection in a {@link HostedSessionView} — the spec'd hosted step
638
+ * shape. NEVER carries `calibratedScore` or the raw check `outputs`; the only
639
+ * output-derived data is the phone_otp `HostedOtpView` display subset
640
+ * (masked number/channel/expiry — never the verifToken).
641
+ */
642
+ interface HostedStepView {
643
+ checkType: CheckType;
644
+ state: CheckState;
645
+ /** Frozen-taxonomy reason code explaining a `failed`/`review` state. */
646
+ reason?: CheckReason;
647
+ /**
648
+ * Submissions the page may still make for this check (`max(0, cap - attempts)`).
649
+ * Meaningful only while the check is executable — a finalized check may still
650
+ * report more than 0, but re-submitting it 409s regardless.
651
+ */
652
+ attemptsRemaining: number;
653
+ /** doc_ocr only: sides already accepted into the partial accumulator (['recto'] or ['verso']). */
654
+ acceptedSides?: ("recto" | "verso")[];
655
+ /** phone_otp only: the display-safe OTP send/config surface (never the verifToken). */
656
+ otp?: HostedOtpView;
657
+ /** email_otp only: the display-safe email send surface (never the handle or the address). */
658
+ emailOtp?: HostedEmailOtpView;
659
+ /**
660
+ * The check failed after using all of its attempts — no retry is left.
661
+ * Absent on older APIs (treat absent as `false`).
662
+ */
663
+ attemptsExhausted?: boolean;
664
+ /**
665
+ * population_registry only: the session is not bound to an NNI, but the
666
+ * document step already read a valid one — the page submits the registry
667
+ * check with an EMPTY input (no NNI field) and the backend uses the
668
+ * document's number. Absent on older APIs.
669
+ */
670
+ nniFromDocument?: boolean;
671
+ /**
672
+ * liveness / face_match (profile v2 step 4, decision mode `automatic`): the
673
+ * capture must come from a phone — the liveness band is calibrated on
674
+ * mobile captures only. A desktop page hands the person off to their phone
675
+ * instead of capturing (a desktop submission answers `mobile_required`).
676
+ * Absent on older APIs and in `review` mode.
677
+ */
678
+ requiresMobile?: boolean;
679
+ /** nfc_chip only (M-nfc). */
680
+ nfc?: NfcStepView;
681
+ /** portrait_icao only (SDK spec §4.9): the last attempt's per-criterion report. */
682
+ icao?: {
683
+ report: IcaoReportEntry[];
684
+ };
685
+ /** eid_auth only (SDK spec §3.2): the display-safe eID surface. */
686
+ eid?: HostedEidView;
687
+ /**
688
+ * The section of the flow this step belongs to (flow model 2026-10-23).
689
+ * Absent on older APIs (derive it from the check type).
690
+ */
691
+ section?: StepSection;
692
+ /**
693
+ * doc_ocr only, on an open step: the client reads the chip next, so the MRZ is
694
+ * read on device for the chip key (`mrz_for_chip`). Native NFC clients only.
695
+ */
696
+ docCapture?: {
697
+ mode: "mrz_for_chip" | (string & {});
698
+ };
699
+ }
700
+ /** A locale the Sekyro AI-hosted voice clips are recorded in. */
701
+ type HostedVoiceLocale = "ar" | "fr" | "en";
702
+ /**
703
+ * The session's Sekyro AI-hosted voice-guidance clips (the active clip set).
704
+ *
705
+ * `clips[locale]` maps a voice key (`fcv3.*`, the `@sekyro/react` voice-key
706
+ * namespace) to an ABSOLUTE URL on the Sekyro AI assets host
707
+ * (`https://assets.sekyro.ai/voice/<setId>/<locale>/<name>.mp3`; staging:
708
+ * `assets-staging.sekyro.ai`). URLs are immutable per `setId`. A locale or key
709
+ * may be missing — the player falls back to the device voice for it.
710
+ *
711
+ * A page with a Content-Security-Policy must allow the assets origin in
712
+ * `media-src` for the clips to play (see the partner web-SDK guide, CSP note).
713
+ */
714
+ interface HostedVoiceView {
715
+ /** Identifier of the active clip set (part of every clip URL). */
716
+ setId: string;
717
+ /** Per-locale map of voice key (`fcv3.*`) → absolute clip URL. */
718
+ clips: {
719
+ readonly [L in HostedVoiceLocale]?: Readonly<Record<string, string>>;
720
+ };
721
+ }
722
+ /** A branding colour key (each value is `#rrggbb`). */
723
+ type HostedBrandingColorKey = "brand" | "brandFg" | "bg" | "surface" | "fg" | "fgMuted" | "danger" | "success" | "warning";
724
+ /**
725
+ * The org / app branding of a session (`HostedSessionView.branding`): only the
726
+ * fields set in the dashboard, merged per field (app over org). A field the
727
+ * org and app left unset is ABSENT — keep your default for it; the whole
728
+ * object is absent when nothing is set. `logoUrl` is a short-lived signed URL
729
+ * (15 minutes).
730
+ */
731
+ interface HostedBranding {
732
+ /** Signed URL of the logo (PNG or sanitized SVG); absent when none is set. */
733
+ logoUrl?: string;
734
+ /** Colours as `#rrggbb`. */
735
+ colors?: Partial<Record<HostedBrandingColorKey, string>>;
736
+ /** Corner radius step. */
737
+ radius?: "none" | "sm" | "md" | "lg" | "full";
738
+ /** Font choice (system fallbacks when the font is not installed). */
739
+ fontFamily?: "system" | "inter" | "ibm-plex-sans-arabic" | "noto-sans-arabic" | "roboto";
740
+ }
741
+ /**
742
+ * Token-addressed hosted-page bootstrap (`GET /hosted/session`). A separate
743
+ * shape from the partner `SessionView`: `sessionId` instead of `id`,
744
+ * hosted step hints, the consent flag, and the org display name (may be `''`
745
+ * when the org lookup degrades).
746
+ */
747
+ /**
748
+ * Server-delivered capture tuning (`HostedSessionView.capture`, SDK spec §3.5).
749
+ * Coaching values only. `@sekyro/web` does not apply it yet (its built-in v3
750
+ * values stay in force until the adopting minor release); native SDKs apply it
751
+ * field by field over their bundled fallbacks and ignore unknown fields.
752
+ */
753
+ interface HostedCaptureTuning {
754
+ tuningVersion?: string;
755
+ liveness?: Record<string, unknown>;
756
+ document?: Record<string, unknown>;
757
+ /** SDK spec §5.4 ID mode: the strict ICAO gate's coaching limits (present with `portrait`). */
758
+ icao?: IcaoCaptureTuning;
759
+ /**
760
+ * Step 3 one-capture live photo (flow model 2026-10-23): one camera session
761
+ * records the clip AND a full-resolution still (long side at least
762
+ * `still.minLongSidePx`, JPEG `still.jpegQuality`).
763
+ */
764
+ livePhoto?: HostedLivePhotoTuning;
765
+ }
766
+ /** `capture.livePhoto` (flow model 2026-10-23, Step 3). */
767
+ interface HostedLivePhotoTuning {
768
+ oneCapture: boolean;
769
+ still: {
770
+ minLongSidePx: number;
771
+ jpegQuality: number;
772
+ };
773
+ }
774
+ /**
775
+ * {@link HostedCaptureTuning} with the native-only document fields typed. The wire shape is
776
+ * the same; `HostedSessionView.capture` stays the open {@link HostedCaptureTuning} so existing
777
+ * consumers keep compiling. Narrow it where the typed fields are read:
778
+ * `const capture = view.capture as HostedNativeCaptureTuning | undefined`.
779
+ */
780
+ interface HostedNativeCaptureTuning extends HostedCaptureTuning {
781
+ document?: HostedDocumentCaptureTuning;
782
+ }
783
+ /**
784
+ * `capture.document`. Only the native-only fields the SDKs share a name for are typed;
785
+ * the rest (gate, aspect, upload, …) stay open. Web ignores the whole block.
786
+ */
787
+ interface HostedDocumentCaptureTuning {
788
+ [field: string]: unknown;
789
+ /** Native document scan machine thresholds (web does not read them). */
790
+ scan?: HostedDocumentScanTuning;
791
+ /** Native on-device OCR tuning (web ignores it; SDK spec §5.2). */
792
+ ocr?: HostedOcrTuning;
793
+ }
794
+ /** `capture.document.scan` — native scan thresholds; only the blur gate is typed here. */
795
+ interface HostedDocumentScanTuning {
796
+ [field: string]: unknown;
797
+ /** Blur gate: a captured photo whose native sharpness is under this is refused and retaken. */
798
+ blurRefuseSharpnessMax?: number;
799
+ }
800
+ /** `capture.document.ocr` — native on-device OCR tuning (web ignores it; SDK spec §5.2). */
801
+ interface HostedOcrTuning {
802
+ enabledOnline?: boolean;
803
+ enabledOffline?: boolean;
804
+ warmup?: boolean;
805
+ maxDeviceLatencyMs?: number;
806
+ deviceTimeoutMs?: number;
807
+ minAvailMemoryMb?: number;
808
+ minConfidenceForRetake?: number;
809
+ minSegmentsForRetake?: number;
810
+ submitGraceMs?: number;
811
+ reportSampleRate?: number;
812
+ readBudgetMs?: number;
813
+ classifyBudgetMs?: number;
814
+ deferArabicOnline?: boolean;
815
+ detectorLimit?: number;
816
+ detectorThreadsMax?: number;
817
+ recognizerThreads?: number;
818
+ recognitionWorkersMax?: number;
819
+ linesInFlight?: number;
820
+ widestLineFirst?: boolean;
821
+ scriptRouting?: boolean;
822
+ arabicColumnFirst?: boolean;
823
+ confidentLatinScore?: number;
824
+ minLatinLetters?: number;
825
+ arabicColumnStart?: number;
826
+ arabicOnlyScore?: number;
827
+ portraitProbeLimit?: number;
828
+ }
829
+ /** Native device attestation parameters (`HostedSessionView.integrity`, SDK spec §6.7). Web ignores it. */
830
+ interface HostedIntegrity {
831
+ mode: "off" | "signal" | "enforce";
832
+ android?: {
833
+ cloudProjectNumber: number;
834
+ };
835
+ ios?: {
836
+ appAttest: true;
837
+ };
838
+ }
839
+ interface HostedSessionView {
840
+ sessionId: string;
841
+ status: SessionStatus;
842
+ outcome: SessionOutcome | null;
843
+ steps: HostedStepView[];
844
+ /** ISO-8601 timestamp (Date on the server, string on the wire). */
845
+ expiresAt: string;
846
+ consentRecorded: boolean;
847
+ orgName: string;
848
+ /**
849
+ * The FIRST allowlisted web-SDK origin of the owning org (Task 12), or null
850
+ * when none is registered. The hosted page posts the SDK popup postMessage
851
+ * events (event name + sessionId ONLY) to exactly this origin — never `'*'`.
852
+ * The backend declares it required (`string | null`); optional here so
853
+ * pre-Task-12 consumers/fixtures stay valid (additive field, always on the
854
+ * wire from Task 12 onward).
855
+ */
856
+ sdkOrigin?: string | null;
857
+ /**
858
+ * Cloudflare Turnstile site key the browser must solve an execute-mode
859
+ * challenge against before submitting `liveness` (Plan A Task 5): the org's
860
+ * own widget key when the request origin is one of the org's allowlisted SDK
861
+ * origins, else the hosted widget key, else `null`. `null` means the session
862
+ * cannot be attested from the browser — the SDK sends `{sandbox: true}`,
863
+ * which the backend honours ONLY on sandbox sessions (fails closed on live).
864
+ */
865
+ turnstileSiteKey: string | null;
866
+ /**
867
+ * The session was minted with a server-bound NNI (`POST /sessions`
868
+ * `input.nni`, Plan A Task 2): `population_registry` is submitted with an
869
+ * EMPTY input and the backend reads the sealed value. `false` means the
870
+ * person must supply the NNI (hosted population form); the SDK never submits
871
+ * the registry check on an unbound session. The VALUE is never exposed. The
872
+ * backend declares it required; optional here so pre-Task-8 fixtures stay
873
+ * valid (additive field, always on the wire from Task 8 onward).
874
+ */
875
+ nniBound?: boolean;
876
+ /**
877
+ * The Sekyro AI-hosted voice-guidance clips of this session, or `null` when no
878
+ * clip set is active (the device voice is used). Optional: older APIs omit
879
+ * the field — treat absent like `null`.
880
+ */
881
+ voice?: HostedVoiceView | null;
882
+ /**
883
+ * Why the session ended, when a profile-v2 policy stopped it: a
884
+ * {@link SessionFailureReason}, or the failed check's reason when it is a
885
+ * policy reason (eligibility, or `document_nni_mismatch`). Absent otherwise
886
+ * (and on older APIs).
887
+ */
888
+ failureReason?: PolicyFailureReason;
889
+ /**
890
+ * The document types the person may pick, resolved for issuer `MR`,
891
+ * `carte_identite`/`passport` only (older pickers). Absent on older APIs —
892
+ * treat absent as both.
893
+ */
894
+ acceptedDocumentTypes?: AcceptedDocumentType[];
895
+ /**
896
+ * Accepted document types per issuing country — drives the issuer-first
897
+ * picker. Absent on older APIs (fall back to `acceptedDocumentTypes` for MR).
898
+ */
899
+ acceptedDocuments?: HostedAcceptedDocuments;
900
+ /**
901
+ * The identity-method choice (profile v2 step 3). Absent when the session
902
+ * has no choice (and on older APIs). While `available && selected === null`
903
+ * every check submission answers 409 `method_required`.
904
+ */
905
+ methodChoice?: MethodChoiceView;
906
+ /**
907
+ * ISO-8601 time from which this device may verify again — set with
908
+ * `failureReason: 'device_cooldown'` (profile v2 step 4). Absent otherwise
909
+ * (and on older APIs).
910
+ */
911
+ retryAfter?: string;
912
+ /**
913
+ * A returning-user session (profile v2 step 5): the person verified before
914
+ * and this session only confirms it is still them — liveness and a face
915
+ * match against their stored photo. Hosts read it to say "Confirm it's
916
+ * you". Absent on full sessions (and on older APIs).
917
+ */
918
+ returning?: true;
919
+ /**
920
+ * The org / app branding (settings spec §5.1). Absent on older APIs — keep
921
+ * the default theme. Map it with `brandingToTheme()` from `@sekyro/react`.
922
+ */
923
+ branding?: HostedBranding;
924
+ /** Capture coaching tuning (SDK spec §3.5). Absent on older APIs. */
925
+ capture?: HostedCaptureTuning;
926
+ /** Native device attestation parameters; absent for web-only apps and on older APIs. */
927
+ integrity?: HostedIntegrity;
928
+ /**
929
+ * SDK spec §3.5: present when the session runs the ICAO portrait step — the
930
+ * client runs the ICAO gate (read it with `icaoPolicyOf`). `icao` is absent on
931
+ * older servers, which send `{ purpose: "id_document" }` alone (= `reject`, `id_document`).
932
+ */
933
+ portrait?: {
934
+ purpose: "id_document";
935
+ /** Present whenever a `portrait_icao` step runs (absent on older servers): `reject` / `id_document`. */
936
+ icao?: HostedIcaoPolicy;
937
+ } | {
938
+ /** A verification selfie checked against the ICAO selfie rules (selfie ICAO policy). */
939
+ purpose: "verification";
940
+ icao: HostedIcaoPolicy;
941
+ };
942
+ /** Step 1 contact choice of a `one_of` session (flow model 2026-10-23). */
943
+ otpChoice?: OtpChoiceView;
944
+ /** Step 2b identity summary, present only while its confirmation is pending. */
945
+ identitySummary?: IdentitySummaryView;
946
+ }
947
+ /** `POST /hosted/consent` result — 201 on first record, 200 `{alreadyRecorded: true}` on replay. */
948
+ interface RecordConsentResult {
949
+ alreadyRecorded: boolean;
950
+ }
951
+ /** Consent locale — the page's `[locale]` URL segment echoed back (HostedConsentSchema picklist). */
952
+ type ConsentLocale = "ar" | "fr" | "en";
953
+ /** Per-check projection in a {@link SessionView} — enum/id only, never civil data. */
954
+ interface SessionStepView {
955
+ checkType: CheckType;
956
+ state: CheckState;
957
+ reason?: CheckReason;
958
+ calibratedScore?: number;
959
+ /** The check failed after using all of its attempts. Absent on older APIs. */
960
+ attemptsExhausted?: boolean;
961
+ /** portrait_icao only (SDK spec §4.9): the last attempt's per-criterion report. */
962
+ report?: IcaoReportEntry[];
963
+ }
964
+ /**
965
+ * Client-facing session projection returned by
966
+ * `POST /sessions/:id/checks/:checkType`. Whitelist-only — never the client token.
967
+ */
968
+ interface SessionView {
969
+ id: string;
970
+ status: SessionStatus;
971
+ outcome: SessionOutcome | null;
972
+ /** Profile-v2 policy stop reason; absent on ordinary failures and older APIs. */
973
+ failureReason?: PolicyFailureReason;
974
+ steps: SessionStepView[];
975
+ /** ISO-8601 timestamp (Date on the server, string on the wire). */
976
+ expiresAt: string;
977
+ /** ISO-8601 timestamp (Date on the server, string on the wire). */
978
+ createdAt?: string;
979
+ }
980
+ /** Media class — selects size cap, MIME allowlist and promoted prefix (media.policy.ts). */
981
+ type MediaKind = "image" | "clip";
982
+ /**
983
+ * Body of `POST /sessions/:id/media`. `contentType`/`contentLength` are the
984
+ * exact values SIGNED into the presigned PUT — the PUT must send them verbatim.
985
+ * Allowlists (media.policy.ts): image → image/jpeg | image/png (5 MB cap);
986
+ * clip → video/webm | video/mp4 (15 MB cap). BARE types only — no `;codecs=` params.
987
+ */
988
+ interface PresignRequest {
989
+ kind: MediaKind;
990
+ contentType: string;
991
+ contentLength: number;
992
+ /** D8: the live-camera capture proof (image / clip from the SDK capture flows only). */
993
+ capture?: CaptureProof;
994
+ }
995
+ /**
996
+ * D8 — the proof a live-camera image / clip presign carries: a single-use nonce
997
+ * from `POST /sessions/:id/media/capture-nonces`, plus the capture engine and how
998
+ * long the capture took. A picked file (or a client without the capture flow)
999
+ * sends none; the backend then flags (or, in `enforce`, refuses) the upload.
1000
+ */
1001
+ interface CaptureProof {
1002
+ nonce: string;
1003
+ engine?: string;
1004
+ captureMs?: number;
1005
+ }
1006
+ /** D8: `POST /sessions/:id/media/capture-nonces` response. */
1007
+ interface CaptureNoncesResult {
1008
+ nonces: string[];
1009
+ /** Seconds each nonce stays usable. */
1010
+ expiresIn: number;
1011
+ }
1012
+ /** D8: what a capture flow knows about its capture when it presigns. */
1013
+ interface CaptureContext {
1014
+ /** Capture engine id (`v1` / `v3` liveness, `document`, `portrait`). */
1015
+ engine?: string;
1016
+ /** How long the camera capture took, ms (→ `captureMs`; informational). */
1017
+ durationMs?: number;
1018
+ }
1019
+ /** Result of a presign — the presigned PUT URL plus the pending object key. */
1020
+ interface PresignUploadResult {
1021
+ key: string;
1022
+ uploadUrl: string;
1023
+ /** Presign TTL in seconds (900 = 15 min). */
1024
+ expiresIn: number;
1025
+ }
1026
+ /** Result of `POST /sessions/:id/media/complete` — the PROMOTED (class-prefixed) key. */
1027
+ interface CompleteMediaResult {
1028
+ ready: true;
1029
+ /** e.g. `clips/{sessionId}/{uuid}` or `doc-images/{sessionId}/{uuid}` — submit THIS key, not the pending one. */
1030
+ key: string;
1031
+ }
1032
+ /**
1033
+ * Liveness attestation bag (attestation-verifier.service.ts). The web SDK sends
1034
+ * `{channel: 'hosted_web', siteKey, turnstileToken}` — `siteKey` echoes the
1035
+ * `HostedSessionView.turnstileSiteKey` the challenge was rendered with, so the
1036
+ * backend picks the matching widget secret deterministically (Turnstile tokens
1037
+ * are single-use: siteverify runs exactly once, never "org then global").
1038
+ * `sandbox: true` and `signature` are honored only on sandbox sessions;
1039
+ * native SDKs will use platform attestation.
1040
+ */
1041
+ interface AttestationInput {
1042
+ sandbox?: boolean;
1043
+ signature?: string;
1044
+ channel?: "hosted_web";
1045
+ turnstileToken?: string;
1046
+ /** The Turnstile site key the token was minted with (selects the verifying secret). */
1047
+ siteKey?: string;
1048
+ }
1049
+ /**
1050
+ * liveness executor input: a promoted `clips/…` key + optional attestation.
1051
+ * `faceMatch` (v2 latency §3.2) carries the already-promoted `doc-images/…`
1052
+ * keyframe so the backend can run face_match IN THE SAME submission when its
1053
+ * bundle flag is on and `face_match` is the next pending step. Backends that
1054
+ * do not bundle ignore it (the input bag is an open record) — the SDK then
1055
+ * submits `face_match` itself with the same key (see liveness-flow.ts).
1056
+ */
1057
+ interface LivenessInput {
1058
+ clipKey: string;
1059
+ attestation?: AttestationInput;
1060
+ /** Declared capture channel — the liveness band is mobile-calibrated, so the
1061
+ * backend clamps non-mobile verdicts to review (uncalibrated_capture_channel).
1062
+ * Client-declared by design (not a security boundary); absent = legacy
1063
+ * clients, treated as mobile server-side during the rollout window. */
1064
+ captureChannel?: 'mobile' | 'desktop';
1065
+ /** Capture metadata (engine, dimensions, clip length, face-capture timing)
1066
+ * — the backend's allowlisted, non-PII `capture` block (session insights). */
1067
+ capture?: LivenessCaptureInput;
1068
+ /** The promoted face_match probe key, for a bundled liveness + face_match run. */
1069
+ faceMatch?: {
1070
+ imageKey: string;
1071
+ };
1072
+ /**
1073
+ * Step 3 one-capture live photo (flow model 2026-10-23): the ICAO still of the
1074
+ * same capture — exactly the `portrait_icao` body. When `portrait_icao` is the
1075
+ * next pending step the server runs it in parallel with liveness.
1076
+ */
1077
+ portraitIcao?: PortraitIcaoInput;
1078
+ }
1079
+ /** `LivenessInput.capture`: what the SDK's `captured` event measured. All ms are whole, ≥ 0. */
1080
+ interface LivenessCaptureInput {
1081
+ engine?: 'v1' | 'v3';
1082
+ width?: number;
1083
+ height?: number;
1084
+ clipMs?: number;
1085
+ cameraReadyMs?: number;
1086
+ faceAcquireMs?: number;
1087
+ captureMs?: number;
1088
+ }
1089
+ /**
1090
+ * population_registry adapter input. `nni` is the 10-digit NNI for an UNBOUND
1091
+ * session; a session minted with a server-side `input.nni` is BOUND to it
1092
+ * (Plan A Task 2) — the browser submits `{}` and the backend reads the sealed
1093
+ * value. Sending a DIFFERENT nni on a bound session answers 409 `nni_locked`.
1094
+ */
1095
+ interface PopulationInput {
1096
+ nni?: string;
1097
+ }
1098
+ /**
1099
+ * face_match executor input: a promoted `doc-images/…` key (the source selfie frame).
1100
+ * The same key may already have ridden along as {@link LivenessInput.faceMatch}.
1101
+ */
1102
+ interface FaceMatchInput {
1103
+ imageKey: string;
1104
+ }
1105
+ /** Document side (docs.types.ts DocSide). */
1106
+ type DocSide = "recto" | "verso" | "single";
1107
+ /** doc_ocr executor input: a promoted `doc-images/…` key + which side it shows. */
1108
+ interface DocOcrInput {
1109
+ imageKey: string;
1110
+ side: DocSide;
1111
+ /**
1112
+ * Web live MRZ (spec 2026-10-21 D6): the device-read HINT of a live-MRZ capture — where the MRZ is in the uploaded
1113
+ * image and that every hard check digit validated on the device. Geometry, a format, counts and a salted digest
1114
+ * only (never MRZ text); the server re-reads the pixels and decides. Omitted when not ready at POST time.
1115
+ */
1116
+ deviceRead?: DocDeviceRead;
1117
+ }
1118
+ /** The `doc_ocr` device-read hint (identity-backend DeviceReadSchema; strict, text-free). */
1119
+ interface DocDeviceRead {
1120
+ engine: string;
1121
+ source: "live_mrz";
1122
+ format: "TD1" | "TD2" | "TD3";
1123
+ lineCount: number;
1124
+ checkDigitsValid: true;
1125
+ stillConfirmed?: boolean;
1126
+ /** [x, y, w, h] normalized to the uploaded image. */
1127
+ band: [number, number, number, number];
1128
+ /** sha256("sekyro-mrz-1\0" + sessionId + "\0" + canonical MRZ lines), 64 hex. */
1129
+ digest: string;
1130
+ framesToValid: number;
1131
+ msToValid: number;
1132
+ }
1133
+ /**
1134
+ * phone_otp executor input (Task 13): a two-phase check. `send` dispatches (or
1135
+ * resends) an OTP — resolving the number from the trusted server-side prefill
1136
+ * or the user-entered `phone` per the step's `numberSource`; `verify` checks
1137
+ * the entered `code` against the parked token. `phone` is IGNORED for a
1138
+ * `prefilled` step (a client number is never trusted as "prefilled").
1139
+ */
1140
+ interface PhoneOtpInput {
1141
+ /** Phase selector: `send` dispatches/resends, `verify` checks the code. Defaults to `send`. */
1142
+ action: "send" | "verify";
1143
+ /** USER-entered E.164 number (`user_entered` / `prefilled_or_entered` sends only). */
1144
+ phone?: string;
1145
+ /** The code the user typed — `verify` only. */
1146
+ code?: string;
1147
+ /**
1148
+ * The channel the person chose for `send`. Must be one of the step's
1149
+ * `otp.channels` when the view lists them; a session without channels
1150
+ * defaults to `sms`.
1151
+ */
1152
+ channel?: "sms" | "whatsapp";
1153
+ /** OTP message locale override for `send`. */
1154
+ locale?: "ar" | "fr" | "en";
1155
+ }
1156
+ /**
1157
+ * email_otp executor input (Plan 8, profile v2 §4.6.5): `send` emails a code to
1158
+ * the trusted prefill or the user-entered `email` (per `emailOtp.emailSource`;
1159
+ * ignored for `prefilled`), `verify` checks the entered `code`.
1160
+ */
1161
+ interface EmailOtpInput {
1162
+ /** Phase selector: `send` dispatches/resends, `verify` checks the code. */
1163
+ action: "send" | "verify";
1164
+ /** USER-entered address (`user_entered` / `prefilled_or_entered` sends only). */
1165
+ email?: string;
1166
+ /** The code the user typed — `verify` only. */
1167
+ code?: string;
1168
+ /** Email copy locale override for `send`. */
1169
+ locale?: "ar" | "fr" | "en";
1170
+ }
1171
+ /**
1172
+ * portrait_icao executor input (SDK spec §4.9): the ICAO gate's frame
1173
+ * (uploaded as media kind `image`). The server re-runs every criterion; only
1174
+ * `gate.baselineEyeOpenness` (0–1, the user's own eye-openness baseline) is used.
1175
+ */
1176
+ interface PortraitIcaoInput {
1177
+ media: {
1178
+ portrait: string;
1179
+ };
1180
+ gate?: {
1181
+ baselineEyeOpenness?: number;
1182
+ /**
1183
+ * The device verdict on the submitted still, with the server's criterion names
1184
+ * (`framing` folds into `resolution`; unmeasured criteria are omitted, and
1185
+ * `background` when the profile does not require it). Compared with the server
1186
+ * report, never trusted. Values are booleans keyed by {@link IcaoCriterion}
1187
+ * (the 0.1 type is kept: the public API only grows).
1188
+ */
1189
+ criteria?: Record<string, unknown>;
1190
+ /** The client background-uniformity measurement (numbers only; telemetry — the server decides). */
1191
+ backgroundCheck?: Record<string, unknown>;
1192
+ };
1193
+ tuningVersion?: string;
1194
+ }
1195
+ /**
1196
+ * eid_auth executor input (SDK spec §3.2): `start` asks the provider for a
1197
+ * code (with the person's `identifier` — the NNI in MR — unless the session is
1198
+ * bound to it), `verify` checks the typed `code`, `resend` asks for a new code.
1199
+ * On a bound session a different `identifier` answers 409 `nni_locked`.
1200
+ */
1201
+ interface EidAuthInput {
1202
+ action: "start" | "verify" | "resend";
1203
+ /** `start` only: the national number, omitted on a bound session. */
1204
+ identifier?: string;
1205
+ /** `verify` only: the code shown in the provider's app. */
1206
+ code?: string;
1207
+ }
1208
+ /** Per-check-type submission input shapes, keyed by the frozen vocabulary. */
1209
+ interface CheckInputByType {
1210
+ liveness: LivenessInput;
1211
+ face_match: FaceMatchInput;
1212
+ doc_ocr: DocOcrInput;
1213
+ population_registry: PopulationInput;
1214
+ phone_otp: PhoneOtpInput;
1215
+ /** M-nfc: read and submitted by the native SDKs only — the web never submits it. */
1216
+ nfc_chip: never;
1217
+ email_otp: EmailOtpInput;
1218
+ /** SDK spec §4.9: the web ICAO gate ships with M1b; the shape is ready for headless use. */
1219
+ portrait_icao: PortraitIcaoInput;
1220
+ /** SDK spec §3.2: the national eID sign-in. */
1221
+ eid_auth: EidAuthInput;
1222
+ }
1223
+ /**
1224
+ * Body of `POST /sessions/:id/checks/:checkType` (SubmitCheckSchema).
1225
+ * `forceOutcome` is SANDBOX-ONLY (plan D-C) — a live session rejects it with
1226
+ * 403 `force_outcome_forbidden`.
1227
+ */
1228
+ interface SubmitCheckBody {
1229
+ input?: Record<string, unknown>;
1230
+ forceOutcome?: "pass" | "fail";
1231
+ }
1232
+
1233
+ /**
1234
+ * Strip MIME parameters: `video/webm;codecs=vp8,opus` → `video/webm`.
1235
+ *
1236
+ * The capture engine (Task 4) emits `clipMimeType` WITH codec parameters, but
1237
+ * the presign allowlist holds BARE types only (['video/webm','video/mp4'] /
1238
+ * ['image/jpeg','image/png']) and the presigned PUT signs the exact declared
1239
+ * Content-Type. Exported so pages can normalize once and stay byte-consistent
1240
+ * between presign and PUT.
1241
+ */
1242
+ declare function stripMimeParams(contentType: string): string;
1243
+ /** Media-bearing check types → the input field the promoted key goes into. */
1244
+ declare const MEDIA_INPUT_FIELD: {
1245
+ readonly liveness: "clipKey";
1246
+ readonly face_match: "imageKey";
1247
+ readonly doc_ocr: "imageKey";
1248
+ };
1249
+ /** Check types that take an uploaded media key (population_registry does not). */
1250
+ type MediaCheckType = keyof typeof MEDIA_INPUT_FIELD;
1251
+ interface SubmitOptions {
1252
+ /**
1253
+ * Sent as `x-idempotency-key` (the header IdempotencyInterceptor reads).
1254
+ * One key per LOGICAL submit — a flaky-network retry with the same key
1255
+ * replays the first response without burning another attempt.
1256
+ */
1257
+ idempotencyKey?: string;
1258
+ /** SANDBOX-ONLY deterministic override (plan D-C); a live session 403s it. */
1259
+ forceOutcome?: "pass" | "fail";
1260
+ }
1261
+ interface UploadMedia {
1262
+ kind: MediaKind;
1263
+ blob: Blob;
1264
+ /** May carry codec params (engine `clipMimeType`) — stripped before presign AND PUT. */
1265
+ contentType: string;
1266
+ }
1267
+ interface HostedClientOptions {
1268
+ /** API origin, e.g. `https://api.sekyro.ai` — paths are `${apiOrigin}/api/v2/...`. */
1269
+ apiOrigin: string;
1270
+ /** The raw session token from the hosted URL (short code; legacy `cst_` during its TTL) — sent as `x-session-token` on every API call. */
1271
+ sessionToken: string;
1272
+ /** Injection seam (tests / non-window contexts). Defaults to `globalThis.fetch`. */
1273
+ fetch?: typeof globalThis.fetch;
1274
+ /** Per-request timeout for API calls (NOT the Spaces PUT). Default 30 000 ms. */
1275
+ requestTimeoutMs?: number;
1276
+ /**
1277
+ * Tuning for the ONLY automatic retry the client performs: 409
1278
+ * `idempotency_in_progress` (a concurrent identical request is mid-flight
1279
+ * server-side; the real response lands once it finishes). `attempts` counts
1280
+ * retries AFTER the first try. Defaults: 5 × 400 ms.
1281
+ */
1282
+ inProgressRetry?: {
1283
+ attempts?: number;
1284
+ delayMs?: number;
1285
+ };
1286
+ }
1287
+ /**
1288
+ * Hand-authored typed client for the five session-token endpoints:
1289
+ *
1290
+ * GET /api/v2/hosted/session → {@link getSession}
1291
+ * POST /api/v2/hosted/consent → {@link recordConsent}
1292
+ * POST /api/v2/hosted/method → {@link selectMethod}
1293
+ * POST /api/v2/hosted/contact → {@link selectContact}
1294
+ * POST /api/v2/hosted/identity/confirm → {@link confirmIdentity}
1295
+ * GET /api/v2/hosted/identity/portrait → {@link identityPortrait}
1296
+ * POST /api/v2/sessions/:id/media → {@link presign}
1297
+ * POST /api/v2/sessions/:id/media/complete → {@link completeMedia}
1298
+ * POST /api/v2/sessions/:id/checks/:checkType → {@link submitCheck}
1299
+ *
1300
+ * plus the bare presigned Spaces PUT ({@link uploadToSpaces}) and the
1301
+ * presign→PUT→complete→submit composition ({@link uploadAndSubmit}).
1302
+ *
1303
+ * Envelopes (verified against sessions-docs.e2e-spec.ts):
1304
+ * - success: `{success: true, data}` — unwrapped here, methods return the payload;
1305
+ * - error: flat `{code, message}` → thrown as {@link HostedApiError}.
1306
+ *
1307
+ * The media/checks routes are `:id`-addressed while the token itself is the
1308
+ * only credential the page starts with — the client learns its sessionId from
1309
+ * `GET /hosted/session` (token-addressed) and lazily bootstraps it when a
1310
+ * `:id` route is called first.
1311
+ *
1312
+ * No automatic retry EXCEPT bounded short-delay retry on 409
1313
+ * `idempotency_in_progress`. Everything else — including 429 with its parsed
1314
+ * `retryAfterSeconds` — is surfaced to the page to handle.
1315
+ */
1316
+ declare class HostedClient {
1317
+ private readonly apiOrigin;
1318
+ private readonly sessionToken;
1319
+ private readonly fetchImpl;
1320
+ private readonly requestTimeoutMs;
1321
+ private readonly retryAttempts;
1322
+ private readonly retryDelayMs;
1323
+ private sessionId;
1324
+ private bootstrap;
1325
+ /** D8: prefetched capture nonces (`armCapture`), each with its local expiry. */
1326
+ private captureNonces;
1327
+ private captureFetch;
1328
+ constructor(opts: HostedClientOptions);
1329
+ /** Token-addressed bootstrap — the hosted page's first call. Caches the sessionId. */
1330
+ getSession(): Promise<HostedSessionView>;
1331
+ /**
1332
+ * Record consent once (201; an idempotent replay answers 200 with
1333
+ * `alreadyRecorded: true`). Until recorded, media presign/complete AND check
1334
+ * submission answer 409 `consent_required`.
1335
+ */
1336
+ recordConsent(locale: ConsentLocale): Promise<RecordConsentResult>;
1337
+ /**
1338
+ * Send the person's identity method (profile v2 step 3) when the view offers
1339
+ * a choice (`methodChoice.available`). Returns the updated hosted view —
1340
+ * continue from it. 409 `method_locked` when the method can no longer
1341
+ * change (a check of the chosen method was already submitted).
1342
+ */
1343
+ selectMethod(method: IdentityMethod): Promise<HostedSessionView>;
1344
+ /**
1345
+ * Choose how the person confirms their contact (flow model 2026-10-23, Step 1)
1346
+ * on a `one_of` session (`otpChoice` in the view): `phone` or `email`. Returns
1347
+ * the updated hosted view. The person may switch until a contact step passed;
1348
+ * then (or on a session without the choice) 409 `contact_locked`.
1349
+ */
1350
+ selectContact(channel: ContactChannel): Promise<HostedSessionView>;
1351
+ /**
1352
+ * Answer the identity summary (`identitySummary` in the view, Step 2b):
1353
+ * `confirm` lifts the live-photo gate, `retake` re-opens the identity checks
1354
+ * that produced the data. Returns the updated hosted view; 409
1355
+ * `identity_confirm_locked` when nothing is pending or no retake is left.
1356
+ */
1357
+ confirmIdentity(decision: IdentityConfirmDecision): Promise<HostedSessionView>;
1358
+ /**
1359
+ * The summary portrait (`identitySummary.portrait.available`) as a JPEG
1360
+ * `Blob`, fetched with the session-token header (`Cache-Control: no-store`).
1361
+ * Show it from memory (`URL.createObjectURL`, revoked when done) — never
1362
+ * cache or store it.
1363
+ */
1364
+ identityPortrait(): Promise<Blob>;
1365
+ /**
1366
+ * Presign a Spaces PUT for a new pending object. `contentType` is stripped of
1367
+ * MIME parameters defensively (the allowlist holds bare types; the engine's
1368
+ * `clipMimeType` carries codec params) — PUT with the SAME bare type, or the
1369
+ * signed-header check rejects the upload.
1370
+ */
1371
+ presign(kind: MediaKind, contentType: string, contentLength: number): Promise<PresignUploadResult>;
1372
+ /**
1373
+ * D8: prefetch live-camera capture nonces when a camera capture STARTS, so the
1374
+ * presigns that follow (`presignCapture`) need no extra round trip. Never
1375
+ * throws; an older backend without the route simply leaves the pool empty.
1376
+ */
1377
+ armCapture(count?: number): void;
1378
+ /**
1379
+ * D8: presign a LIVE-CAMERA image / clip — like {@link presign}, plus the
1380
+ * capture proof: a fresh single-use nonce (pooled by {@link armCapture}, else
1381
+ * fetched now), the engine and the capture duration. When no nonce can be had
1382
+ * (route absent, network) the presign goes out without a proof — the backend
1383
+ * decides (`signal` flags it, `enforce` refuses it). Only the SDK's own capture
1384
+ * flows call this; a picked file goes through {@link presign}.
1385
+ */
1386
+ presignCapture(kind: MediaKind, contentType: string, contentLength: number, context?: CaptureContext): Promise<PresignUploadResult>;
1387
+ /** D8: one pooled (still fresh) nonce, else one fetched now; undefined when none can be had. */
1388
+ private takeCaptureNonce;
1389
+ /** D8: fetch `count` nonces into the pool (one request in flight at a time). Never throws. */
1390
+ private fillCaptureNonces;
1391
+ /**
1392
+ * Bare PUT to the presigned Spaces URL: EXACTLY the signed headers — the
1393
+ * declared Content-Type and the blob body. NO `x-session-token` (the
1394
+ * signature IS the credential; an extra header breaks nothing but leaks the
1395
+ * token to a third-party host) and no JSON envelope.
1396
+ *
1397
+ * `contentType` is param-stripped here too (idempotent for bare types): the
1398
+ * presign strips internally, so a caller composing manually with the engine's
1399
+ * parameterful `clipMimeType` would otherwise sign `video/webm` but PUT
1400
+ * `video/webm;codecs=…` — a signed-header mismatch S3 rejects at runtime.
1401
+ *
1402
+ * Timeout: 15 min (`AbortSignal.timeout(900_000)`, matching the presign TTL)
1403
+ * rather than the API calls' 30 s — a 15 MB clip on a slow link legitimately
1404
+ * exceeds 30 s, but a stalled in-flight socket must still be bounded (the TTL
1405
+ * alone only bounds when a PUT can START).
1406
+ *
1407
+ * Failures are ALWAYS typed: non-2xx throws {@link HostedApiError} with the
1408
+ * client-synthesized `upload_failed` (message carries a truncated S3 body
1409
+ * snippet — e.g. SignatureDoesNotMatch); a mid-transfer drop → `network_error`;
1410
+ * the timeout → `timeout`.
1411
+ */
1412
+ uploadToSpaces(uploadUrl: string, blob: Blob, contentType: string): Promise<void>;
1413
+ /** Finalize an upload: the backend sniffs + promotes; returns the PROMOTED key to submit. */
1414
+ completeMedia(key: string): Promise<CompleteMediaResult>;
1415
+ /**
1416
+ * Submit one check. Sends `{input, forceOutcome?}` (SubmitCheckSchema) and the
1417
+ * optional `x-idempotency-key` header. Returns the whitelisted SessionView.
1418
+ */
1419
+ submitCheck<T extends CheckType>(checkType: T, input: CheckInputByType[T], opts?: SubmitOptions): Promise<SessionView>;
1420
+ /**
1421
+ * Convenience composition: presign → PUT → complete → submit, passing the
1422
+ * PROMOTED key (from complete's response — never the pending presign key)
1423
+ * into the check's media input field (`clipKey` for liveness, `imageKey` for
1424
+ * face_match/doc_ocr). `extraInput` carries the remaining input fields
1425
+ * (liveness `attestation`, doc_ocr `side`). The media content type is
1426
+ * param-stripped ONCE and used for both presign and PUT, so the signed
1427
+ * header always matches.
1428
+ */
1429
+ uploadAndSubmit<T extends MediaCheckType>(checkType: T, media: UploadMedia, extraInput: Omit<CheckInputByType[T], (typeof MEDIA_INPUT_FIELD)[T]>, opts?: SubmitOptions): Promise<SessionView>;
1430
+ /** The token's own sessionId — cached from getSession(), lazily bootstrapped. */
1431
+ private ensureSessionId;
1432
+ /**
1433
+ * One API request: `${apiOrigin}/api/v2${path}`, `x-session-token` always,
1434
+ * AbortSignal timeout (30 s default), `{success, data}` unwrap on 2xx, flat
1435
+ * `{code, message}` → {@link HostedApiError} on non-2xx. EVERY failure is
1436
+ * typed: a rejected fetch → `network_error`, the timeout → `timeout`, an
1437
+ * unparseable 2xx body → `unknown_error` — the page never sees a raw
1438
+ * TypeError/DOMException. The ONLY automatic retry is 409
1439
+ * `idempotency_in_progress` (bounded, short delay).
1440
+ */
1441
+ private api;
1442
+ }
1443
+
1444
+ /** Luma at or above this counts as clipped / specular. */
1445
+ declare const CLIPPED_LUMA = 250;
1446
+ /** Laplacian-variance reference (same as the liveness gate's frame-quality SHARP_REF). */
1447
+ declare const ICAO_SHARP_REF = 180;
1448
+ interface LumaPlane {
1449
+ data: Uint8Array;
1450
+ width: number;
1451
+ height: number;
1452
+ }
1453
+ /** Rec.601 luma of RGBA pixels. */
1454
+ declare function lumaPlane(image: {
1455
+ data: Uint8ClampedArray | Uint8Array;
1456
+ width: number;
1457
+ height: number;
1458
+ }): LumaPlane;
1459
+ interface RegionStats {
1460
+ mean: number;
1461
+ sd: number;
1462
+ /** Fraction of pixels at or above {@link CLIPPED_LUMA}. */
1463
+ clipped: number;
1464
+ count: number;
1465
+ }
1466
+ /** Mean / SD / clipped fraction of a box, optionally excluding a second box. */
1467
+ declare function regionStats(plane: LumaPlane, box: Box, exclude?: Box): RegionStats;
1468
+ interface FaceLighting {
1469
+ faceLuma: number;
1470
+ /** Left-half / right-half mean luma (image halves, split at the nose). */
1471
+ halfRatio: number;
1472
+ clipped: number;
1473
+ }
1474
+ declare function faceLighting(plane: LumaPlane, g: FaceGeometry): FaceLighting | null;
1475
+ /** Specular fraction inside both eye regions (spec §5.4 glasses glare). */
1476
+ declare function eyeSpecular(plane: LumaPlane, g: FaceGeometry): number | null;
1477
+ interface BackgroundStats {
1478
+ luma: number;
1479
+ sd: number;
1480
+ /** Pixels measured — too few means the corners left the frame. */
1481
+ count: number;
1482
+ }
1483
+ /**
1484
+ * Background heuristic: the two top corners of the ICAO window (each 20 % wide,
1485
+ * 25 % tall from 4 % down), minus the head (the face box widened by 40 % and
1486
+ * extended to the top of the window for hair). `null` when nothing is left.
1487
+ */
1488
+ declare function backgroundStats(plane: LumaPlane, g: FaceGeometry): BackgroundStats | null;
1489
+ /** Laplacian variance over the face core on a ≤96×96 grid, normalised 0..1 by {@link ICAO_SHARP_REF}. */
1490
+ declare function faceSharpness(plane: LumaPlane, g: FaceGeometry): number | null;
1491
+ /** Every pixel measure of one frame (`null` fields: not measurable). */
1492
+ interface IcaoPixelStats {
1493
+ lighting: FaceLighting | null;
1494
+ eyeSpecular: number | null;
1495
+ background: BackgroundStats | null;
1496
+ sharpness: number | null;
1497
+ }
1498
+ declare function icaoPixelStats(plane: LumaPlane, g: FaceGeometry): IcaoPixelStats;
1499
+
1500
+ /** The client gate's criteria, in coaching priority order. */
1501
+ declare const ICAO_GATE_CRITERIA: readonly ["face", "framing", "pose", "eyes", "mouth", "glare", "lighting", "background", "resolution", "sharpness"];
1502
+ type IcaoGateCriterion = (typeof ICAO_GATE_CRITERIA)[number];
1503
+ /** Coaching keys (copy `portrait.coach.<key>`). */
1504
+ type IcaoCoachKey = "no_face" | "multiple_faces" | "center" | "too_close" | "pose" | "eyes" | "mouth" | "glare" | "lighting" | "lighting_side" | "background" | "resolution" | "hold_still" | "hold";
1505
+ /** One frame as the gate sees it. Geometry in SOURCE (stream) pixels. */
1506
+ interface IcaoFrameObservation {
1507
+ frameWidth: number;
1508
+ frameHeight: number;
1509
+ /** Faces the landmarker found (the gate wants exactly one). */
1510
+ faces: number;
1511
+ geometry: FaceGeometry | null;
1512
+ /** Facial transformation matrix (4×4, column-major), when produced. */
1513
+ matrix?: readonly number[] | null;
1514
+ /** Blendshape scores by category name (`eyeBlinkLeft`, `jawOpen`, …). */
1515
+ blendshapes?: Readonly<Record<string, number>> | null;
1516
+ /** Pixel heuristics on the analysis frame; `null` when not measured. */
1517
+ stats?: IcaoPixelStats | null;
1518
+ /**
1519
+ * The background-uniformity check (segmenter, or the strip fallback), the
1520
+ * latest measurement — it runs a few times per second, not on every frame.
1521
+ * When present it decides the `background` criterion; `null` = not measurable.
1522
+ */
1523
+ backgroundCheck?: BackgroundUniformity | null;
1524
+ }
1525
+ interface IcaoCriterionState {
1526
+ criterion: IcaoGateCriterion;
1527
+ pass: boolean;
1528
+ /** Passed continuously for `holdMs`. */
1529
+ held: boolean;
1530
+ /** A failing soft criterion the gate stopped waiting for. */
1531
+ waived: boolean;
1532
+ /** The measured value (degrees, ratio, luma, px…) — coaching / telemetry only. */
1533
+ value: number | null;
1534
+ /** Coaching key when failing. */
1535
+ coach: IcaoCoachKey | null;
1536
+ }
1537
+ interface IcaoGateUpdate {
1538
+ criteria: IcaoCriterionState[];
1539
+ /** The one message to show (`hold` when everything passes and the gate is counting down). */
1540
+ coach: IcaoCoachKey;
1541
+ /** The lowest failing criterion (`null` when all pass or are waived). */
1542
+ failing: IcaoGateCriterion | null;
1543
+ /** 0..1 — held criteria, for a progress ring. */
1544
+ progress: number;
1545
+ /** Take the portrait frame now. */
1546
+ ready: boolean;
1547
+ /**
1548
+ * The user's own eye-openness baseline, once known. Unit (D9d, everywhere): openness =
1549
+ * 1 − the Landmarker's `eyeBlink{Left,Right}` blendshape; per frame the mean of the
1550
+ * two eyes; the baseline is the `baseline.percentile` (0.8) quantile over the recent
1551
+ * frontal, single-face frames. kyc-portrait compares the lower eye against it.
1552
+ */
1553
+ baseline: number | null;
1554
+ pose: HeadPose | null;
1555
+ }
1556
+ /** The on-device result submitted with the portrait (`gate`, §4.9): numbers only. */
1557
+ interface IcaoGateSummary {
1558
+ /** In (0, 1]: the gate's eye baseline (1 − blink blendshape; see {@link IcaoGateUpdate.baseline}). */
1559
+ baselineEyeOpenness?: number;
1560
+ criteria: Partial<Record<IcaoGateCriterion, {
1561
+ pass: boolean;
1562
+ value: number | null;
1563
+ waived?: boolean;
1564
+ }>>;
1565
+ /**
1566
+ * The device verdict with the server's criterion names — the submission's
1567
+ * `gate.criteria`: `framing` folds into `resolution`, unmeasured criteria are
1568
+ * omitted (occlusion always; background when the profile does not require it).
1569
+ */
1570
+ verdict?: Partial<Record<IcaoCriterion, boolean>>;
1571
+ /** The background-uniformity measurement behind the `background` criterion (numbers only). */
1572
+ backgroundCheck?: Omit<BackgroundUniformity, "pixels">;
1573
+ }
1574
+ interface Measured {
1575
+ pass: boolean;
1576
+ value: number | null;
1577
+ coach: IcaoCoachKey | null;
1578
+ /** Not measured on this frame (left out of the verdict). */
1579
+ none?: true;
1580
+ }
1581
+ /** The criteria a profile sets limits for (conformance.json `criteria`). */
1582
+ type IcaoJudgedCriterion = "pose" | "eyes" | "mouth" | "lighting" | "background" | "resolution";
1583
+ /**
1584
+ * One frame's measures in the server's units (conformance.json `measures`);
1585
+ * `null` = not measured on this frame.
1586
+ */
1587
+ interface IcaoMeasures {
1588
+ /** Degrees; `pitch` is RAW (before `pitchOffsetDeg`). `null` = no transformation matrix. */
1589
+ pose: HeadPose | null;
1590
+ /** The value shown for a pose without matrix (roll from the eye line). */
1591
+ eyeLineRoll?: number;
1592
+ /** Openness of each eye (1 − the `eyeBlink*` blendshape). */
1593
+ eyes: {
1594
+ left: number;
1595
+ right: number;
1596
+ } | null;
1597
+ /** The person's eye-openness baseline; `null` = the absolute floor only. */
1598
+ baseline: number | null;
1599
+ /** Blendshapes: `jawOpen`, the larger `mouthSmile*`, `mouthPucker`. */
1600
+ mouth: {
1601
+ jawOpen: number;
1602
+ smile: number;
1603
+ pucker: number;
1604
+ } | null;
1605
+ lighting: {
1606
+ faceLuma: number;
1607
+ halfRatio: number;
1608
+ clipped: number;
1609
+ } | null;
1610
+ background: {
1611
+ luma: number;
1612
+ sd: number;
1613
+ } | null;
1614
+ /** The web background-uniformity check; when present (and enabled) it decides the background. */
1615
+ uniformity?: Pick<BackgroundUniformity, "uniform" | "colorStd"> | null;
1616
+ iod: number;
1617
+ frameWidth: number;
1618
+ frameHeight: number;
1619
+ }
1620
+ /**
1621
+ * The per-criterion decision on one frame's measures — pure, the same rule as
1622
+ * kyc-portrait and the native gates (packages/sekyro-contract/icao/conformance.json;
1623
+ * limits are inclusive).
1624
+ */
1625
+ declare function judgeIcao(m: IcaoMeasures, t: ResolvedIcaoTuning): Record<IcaoJudgedCriterion, Measured>;
1626
+ /** Openness of one eye from the Landmarker's blink blendshape (1 − blink). */
1627
+ declare function eyeOpenness(blendshapes: Readonly<Record<string, number>> | null | undefined): {
1628
+ left: number;
1629
+ right: number;
1630
+ blinkLeft: number;
1631
+ blinkRight: number;
1632
+ } | null;
1633
+ /** The `p`-quantile (0..1) of a sample, nearest-rank. */
1634
+ declare function percentile(values: readonly number[], p: number): number | null;
1635
+ /** Score one frame against the limits. Pure; `baseline` is the eye baseline so far. */
1636
+ declare function scoreFrame(frame: IcaoFrameObservation, t: ResolvedIcaoTuning, baseline: number | null): {
1637
+ results: Record<IcaoGateCriterion, Measured>;
1638
+ pose: HeadPose | null;
1639
+ };
1640
+ interface IcaoGate {
1641
+ push(frame: IcaoFrameObservation, nowMs: number): IcaoGateUpdate;
1642
+ /** The last update (`null` before the first frame). */
1643
+ last(): IcaoGateUpdate | null;
1644
+ /** The submission's `gate` block from the last update. */
1645
+ summary(): IcaoGateSummary;
1646
+ /** Start the holds over (a retake). The eye baseline is kept: it describes the person. */
1647
+ reset(): void;
1648
+ }
1649
+ interface IcaoGateOptions {
1650
+ /** `alert`: coaching only — ready `alertGraceMs` after the face was accepted at the latest. Default `reject`. */
1651
+ policy?: "reject" | "alert";
1652
+ }
1653
+ declare function createIcaoGate(t: ResolvedIcaoTuning, options?: IcaoGateOptions): IcaoGate;
1654
+
1655
+ export { type PopulationReason as $, type AcceptedDocumentType as A, type IcaoCoachKey as B, type CheckReason as C, type DecisionReason as D, type EidAfterPass as E, type FaceMatchReason as F, type IcaoCriterion as G, type HeadPose as H, type IcaoCaptureTuning as I, type IcaoCriterionState as J, type IcaoGateCriterion as K, type IcaoGateSummary as L, type IcaoGateUpdate as M, type IcaoReportEntry as N, type IdentityConfirmDecision as O, type IdentityMethod as P, type IdentitySummarySource as Q, type IdentitySummaryView as R, type LivenessReason as S, type MatchReason as T, type MethodChoiceView as U, type NationalEidOption as V, type NfcChipReason as W, type NfcStepView as X, type OtpChoiceView as Y, type PhoneOtpReason as Z, type PolicyFailureReason as _, type CheckState as a, type MediaCheckType as a$, type PortraitIcaoInput as a0, type PortraitIcaoReason as a1, type SessionFailureReason as a2, type SessionOutcome as a3, type SessionStatus as a4, type StepSection as a5, type SessionView as a6, type BackgroundStats as a7, type Box as a8, CLIPPED_LUMA as a9, icaoWindow as aA, judgeIcao as aB, lumaPlane as aC, percentile as aD, portraitCropRegion as aE, poseFromMatrix as aF, regionStats as aG, resolveIcaoTuning as aH, scoreFrame as aI, windowOverflow as aJ, type PersonMask as aK, HostedClient as aL, type AttestationInput as aM, type CaptureContext as aN, type CaptureNoncesResult as aO, type CaptureProof as aP, type CheckInputByType as aQ, type CompleteMediaResult as aR, type ConsentLocale as aS, type DocDeviceRead as aT, type DocOcrInput as aU, type DocSide as aV, type EmailOtpInput as aW, type FaceMatchInput as aX, type HostedClientOptions as aY, type LivenessCaptureInput as aZ, type LivenessInput as a_, type FaceGeometry as aa, type FaceLighting as ab, ICAO_CANVAS as ac, ICAO_FALLBACK as ad, ICAO_GATE_CRITERIA as ae, ICAO_SHARP_REF as af, type IcaoFrameObservation as ag, type IcaoGate as ah, type IcaoGateOptions as ai, type IcaoJudgedCriterion as aj, type IcaoMeasures as ak, type IcaoPixelStats as al, type IcaoProfile as am, type LumaPlane as an, type Point as ao, type RegionStats as ap, type ResolvedIcaoTuning as aq, backgroundStats as ar, createIcaoGate as as, eyeLineRoll as at, eyeOpenness as au, eyeSpecular as av, faceGeometry as aw, faceLighting as ax, faceSharpness as ay, icaoPixelStats as az, type CheckType as b, type MediaKind as b0, type PhoneOtpInput as b1, type PopulationInput as b2, type PresignRequest as b3, type PresignUploadResult as b4, type RecordConsentResult as b5, type SessionStepView as b6, type SubmitCheckBody as b7, type SubmitOptions as b8, type UploadMedia as b9, stripMimeParams as ba, type ContactChannel as c, type DocReason as d, type EidAuthInput as e, type EidReason as f, type EmailOtpReason as g, type HostedAcceptedDocuments as h, type HostedBranding as i, type HostedBrandingColorKey as j, type HostedCaptureTuning as k, type HostedDocumentCaptureTuning as l, type HostedDocumentScanTuning as m, type HostedEidView as n, type HostedEmailOtpView as o, type HostedIcaoPolicy as p, type HostedIntegrity as q, type HostedLivePhotoTuning as r, type HostedNativeCaptureTuning as s, type HostedOcrTuning as t, type HostedOtpView as u, type HostedPortraitView as v, type HostedSessionView as w, type HostedStepView as x, type HostedVoiceLocale as y, type HostedVoiceView as z };