@sekyro/web 0.0.0-stage → 0.1.0-rc.2

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 (68) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +20 -0
  3. package/README.md +92 -2
  4. package/dist/capture-constraints-BujkZcfb.d.cts +21 -0
  5. package/dist/capture-constraints-BujkZcfb.d.ts +21 -0
  6. package/dist/capture-session-v3-BJ8ciaWl.d.ts +457 -0
  7. package/dist/capture-session-v3-CAoWM8iT.d.cts +457 -0
  8. package/dist/chunk-BPYVZUJA.js +852 -0
  9. package/dist/chunk-BPYVZUJA.js.map +1 -0
  10. package/dist/chunk-LOTAJDRE.js +1094 -0
  11. package/dist/chunk-LOTAJDRE.js.map +1 -0
  12. package/dist/chunk-MWNFSNMP.js +52 -0
  13. package/dist/chunk-MWNFSNMP.js.map +1 -0
  14. package/dist/chunk-NILNKLK6.js +50 -0
  15. package/dist/chunk-NILNKLK6.js.map +1 -0
  16. package/dist/chunk-NWF46H7K.js +359 -0
  17. package/dist/chunk-NWF46H7K.js.map +1 -0
  18. package/dist/chunk-WY6U7AIK.js +193 -0
  19. package/dist/chunk-WY6U7AIK.js.map +1 -0
  20. package/dist/chunk-YVFF4Q4G.js +327 -0
  21. package/dist/chunk-YVFF4Q4G.js.map +1 -0
  22. package/dist/client/index.cjs +357 -0
  23. package/dist/client/index.cjs.map +1 -0
  24. package/dist/client/index.d.cts +39 -0
  25. package/dist/client/index.d.ts +39 -0
  26. package/dist/client/index.js +15 -0
  27. package/dist/client/index.js.map +1 -0
  28. package/dist/core/index.cjs +1605 -0
  29. package/dist/core/index.cjs.map +1 -0
  30. package/dist/core/index.d.cts +87 -0
  31. package/dist/core/index.d.ts +87 -0
  32. package/dist/core/index.js +139 -0
  33. package/dist/core/index.js.map +1 -0
  34. package/dist/core/v3/detect-worker.cjs +392 -0
  35. package/dist/core/v3/detect-worker.cjs.map +1 -0
  36. package/dist/core/v3/detect-worker.d.cts +1 -0
  37. package/dist/core/v3/detect-worker.d.ts +1 -0
  38. package/dist/core/v3/detect-worker.js +13 -0
  39. package/dist/core/v3/detect-worker.js.map +1 -0
  40. package/dist/create-verification-CjLnJ0w6.d.ts +348 -0
  41. package/dist/create-verification-EnZ13uY4.d.cts +348 -0
  42. package/dist/detect-worker-CKXHlXx9.d.cts +105 -0
  43. package/dist/detect-worker-CKXHlXx9.d.ts +105 -0
  44. package/dist/detector-scanic-4C6IEW2K.js +787 -0
  45. package/dist/detector-scanic-4C6IEW2K.js.map +1 -0
  46. package/dist/hosted-client-BpvBqu9f.d.cts +408 -0
  47. package/dist/hosted-client-BpvBqu9f.d.ts +408 -0
  48. package/dist/index.cjs +2636 -0
  49. package/dist/index.cjs.map +1 -0
  50. package/dist/index.d.cts +6 -0
  51. package/dist/index.d.ts +6 -0
  52. package/dist/index.js +28 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/internal/index.cjs +4166 -0
  55. package/dist/internal/index.cjs.map +1 -0
  56. package/dist/internal/index.d.cts +431 -0
  57. package/dist/internal/index.d.ts +431 -0
  58. package/dist/internal/index.js +647 -0
  59. package/dist/internal/index.js.map +1 -0
  60. package/dist/popup/index.cjs +76 -0
  61. package/dist/popup/index.cjs.map +1 -0
  62. package/dist/popup/index.d.cts +66 -0
  63. package/dist/popup/index.d.ts +66 -0
  64. package/dist/popup/index.js +11 -0
  65. package/dist/popup/index.js.map +1 -0
  66. package/dist/scanic-ml-stub-S2OZ6HEF.js +10 -0
  67. package/dist/scanic-ml-stub-S2OZ6HEF.js.map +1 -0
  68. package/package.json +118 -4
@@ -0,0 +1,647 @@
1
+ import {
2
+ AttestationError,
3
+ CLIP_MIME_ALLOWLIST,
4
+ PROCESSING_POLL_DELAY_MS,
5
+ PROCESSING_POLL_MAX,
6
+ REGISTRY_RETRY_DELAY_MS,
7
+ RETRYABLE_REASONS,
8
+ TRANSPORT_REPLAY_ATTEMPTS,
9
+ TRANSPORT_REPLAY_DELAY_MS,
10
+ TURNSTILE_EXECUTE_TIMEOUT_MS,
11
+ TURNSTILE_SCRIPT_SRC,
12
+ createLivenessCaptureInternal,
13
+ createVerificationInternal,
14
+ executeTurnstile,
15
+ frontierStep,
16
+ isExecutable,
17
+ isTerminalView,
18
+ loadTurnstile,
19
+ mapEngineState,
20
+ mapErrorKind,
21
+ mapGuidanceHint,
22
+ mapSessionResult,
23
+ mergeSessionView,
24
+ randomUuid,
25
+ resetTurnstileForTests,
26
+ runLivenessFlow,
27
+ runSubmission,
28
+ stepOf
29
+ } from "../chunk-BPYVZUJA.js";
30
+ import {
31
+ CENTERED_FACTOR,
32
+ CIRCLE_RADIUS_FACTOR,
33
+ IN_CIRCLE_FACTOR,
34
+ classifyDevice,
35
+ mapGetUserMediaError,
36
+ probeDevice
37
+ } from "../chunk-LOTAJDRE.js";
38
+ import "../chunk-YVFF4Q4G.js";
39
+ import "../chunk-NWF46H7K.js";
40
+ import {
41
+ ASPECT_BANDS,
42
+ DOC_DETECT_WIDTH,
43
+ DOC_GATE_V1_DEFAULTS,
44
+ DocGateV1,
45
+ MR_DOC_CATALOG,
46
+ aspectBand,
47
+ quadAreaFrac,
48
+ quadBounds,
49
+ quadIou
50
+ } from "../chunk-WY6U7AIK.js";
51
+ import {
52
+ EXPOSURE_BAND,
53
+ exposure,
54
+ meanLuma,
55
+ sharpness
56
+ } from "../chunk-MWNFSNMP.js";
57
+
58
+ // src/core/doc/mrz-band.ts
59
+ var MRZ_BAND_DEFAULTS = Object.freeze({
60
+ EDGE_LUMA_DELTA: 40,
61
+ MIN_ROW_DENSITY: 0.08,
62
+ MIN_COLUMN_COVERAGE: 0.6,
63
+ MIN_WIDTH_FRAC: 0.55,
64
+ MIN_HEIGHT_FRAC: 0.02,
65
+ MAX_HEIGHT_FRAC: 0.16,
66
+ ROW_MERGE_GAP: 18
67
+ });
68
+ var COLUMN_BUCKETS = 12;
69
+ function detectMrzBand(frame, overrides = {}) {
70
+ const t = { ...MRZ_BAND_DEFAULTS, ...overrides };
71
+ const { data, width, height } = frame;
72
+ if (width < 32 || height < 32) return null;
73
+ const qualifies = new Array(height).fill(false);
74
+ const density = new Float32Array(height);
75
+ const minX = new Int32Array(height).fill(width);
76
+ const maxX = new Int32Array(height).fill(-1);
77
+ const bucketWidth = width / COLUMN_BUCKETS;
78
+ const buckets = new Array(COLUMN_BUCKETS);
79
+ for (let y = 0; y < height; y++) {
80
+ let edges = 0;
81
+ buckets.fill(false);
82
+ const rowOffset = y * width * 4;
83
+ let previousLuma = 0.299 * (data[rowOffset] ?? 0) + 0.587 * (data[rowOffset + 1] ?? 0) + 0.114 * (data[rowOffset + 2] ?? 0);
84
+ for (let x = 1; x < width; x++) {
85
+ const i = rowOffset + x * 4;
86
+ const luma = 0.299 * (data[i] ?? 0) + 0.587 * (data[i + 1] ?? 0) + 0.114 * (data[i + 2] ?? 0);
87
+ if (Math.abs(luma - previousLuma) >= t.EDGE_LUMA_DELTA) {
88
+ edges += 1;
89
+ buckets[Math.min(COLUMN_BUCKETS - 1, Math.floor(x / bucketWidth))] = true;
90
+ if (x < minX[y]) minX[y] = x;
91
+ if (x > maxX[y]) maxX[y] = x;
92
+ }
93
+ previousLuma = luma;
94
+ }
95
+ density[y] = edges / width;
96
+ const coverage = buckets.filter(Boolean).length / COLUMN_BUCKETS;
97
+ qualifies[y] = density[y] >= t.MIN_ROW_DENSITY && coverage >= t.MIN_COLUMN_COVERAGE && maxX[y] - minX[y] >= t.MIN_WIDTH_FRAC * width;
98
+ }
99
+ let best = null;
100
+ let runTop = -1;
101
+ let lastQualifying = -1;
102
+ let runRows = 0;
103
+ const flush = (bottom) => {
104
+ if (runTop < 0) return;
105
+ const bandHeight = bottom - runTop + 1;
106
+ if (bandHeight >= t.MIN_HEIGHT_FRAC * height && bandHeight <= t.MAX_HEIGHT_FRAC * height && runRows >= 4 && // at least a few real stroke rows, not a lone edge
107
+ (best === null || runRows > best.rows)) {
108
+ best = { top: runTop, bottom, rows: runRows };
109
+ }
110
+ runTop = -1;
111
+ runRows = 0;
112
+ };
113
+ for (let y = 0; y < height; y++) {
114
+ if (qualifies[y]) {
115
+ if (runTop < 0) runTop = y;
116
+ else if (y - lastQualifying > t.ROW_MERGE_GAP) {
117
+ flush(lastQualifying);
118
+ runTop = y;
119
+ }
120
+ lastQualifying = y;
121
+ runRows += 1;
122
+ }
123
+ }
124
+ flush(lastQualifying);
125
+ if (best === null) return null;
126
+ const band = best;
127
+ let x1 = width;
128
+ let x2 = -1;
129
+ let densitySum = 0;
130
+ let densityRows = 0;
131
+ for (let y = band.top; y <= band.bottom; y++) {
132
+ if (!qualifies[y]) continue;
133
+ if (minX[y] < x1) x1 = minX[y];
134
+ if (maxX[y] > x2) x2 = maxX[y];
135
+ densitySum += density[y];
136
+ densityRows += 1;
137
+ }
138
+ if (x2 - x1 < t.MIN_WIDTH_FRAC * width) return null;
139
+ return {
140
+ x: x1,
141
+ y: band.top,
142
+ width: x2 - x1,
143
+ height: band.bottom - band.top + 1,
144
+ density: densityRows > 0 ? densitySum / densityRows : 0
145
+ };
146
+ }
147
+
148
+ // src/core/doc/frame-analysis.ts
149
+ var DOC_ANALYSIS_DEFAULTS = Object.freeze({
150
+ /** Doc area fraction above which the camera is too close (capture would crop). */
151
+ MAX_AREA_FRAC: 0.92,
152
+ /** Opposite-side length ratio beyond which the view is too oblique. */
153
+ TILT_MAX_SIDE_RATIO: 1.3,
154
+ /** Specular-pixel fraction at/above which the frame reads as glare. */
155
+ GLARE_MAX_SPECULAR_FRAC: 0.025
156
+ });
157
+ function specularFraction(crop) {
158
+ const { data, width, height } = crop;
159
+ const pixels = width * height;
160
+ if (pixels === 0) return 0;
161
+ let hot = 0;
162
+ for (let i = 0; i < pixels; i++) {
163
+ const o = i * 4;
164
+ const luma = 0.299 * (data[o] ?? 0) + 0.587 * (data[o + 1] ?? 0) + 0.114 * (data[o + 2] ?? 0);
165
+ if (luma >= 252) hot++;
166
+ }
167
+ return hot / pixels;
168
+ }
169
+ function quadTiltRatio(quad) {
170
+ const len = (a, b) => Math.hypot(b.x - a.x, b.y - a.y);
171
+ const top = len(quad.topLeft, quad.topRight);
172
+ const bottom = len(quad.bottomLeft, quad.bottomRight);
173
+ const left = len(quad.topLeft, quad.bottomLeft);
174
+ const right = len(quad.topRight, quad.bottomRight);
175
+ const h = Math.max(top, bottom) / Math.max(1e-6, Math.min(top, bottom));
176
+ const v = Math.max(left, right) / Math.max(1e-6, Math.min(left, right));
177
+ return Math.max(h, v);
178
+ }
179
+ function analyzeDocFrame(input) {
180
+ const t = { ...DOC_GATE_V1_DEFAULTS, ...input.thresholds };
181
+ const a = DOC_ANALYSIS_DEFAULTS;
182
+ const { quad, frameWidth, frameHeight, measures } = input;
183
+ const bandMode = input.format === "ID-3";
184
+ let framing = "ok";
185
+ if (quad === null) {
186
+ framing = "no_document";
187
+ } else if (bandMode) {
188
+ const bounds = quadBounds(quad);
189
+ const widthFrac = frameWidth > 0 ? bounds.width / frameWidth : 0;
190
+ const aspect = bounds.height > 0 ? bounds.width / bounds.height : 0;
191
+ if (widthFrac < t.ID3_BAND_MIN_WIDTH_FRAC) framing = "too_far";
192
+ else if (edgeClipped(quad, frameWidth, frameHeight, t.EDGE_CLIP_MARGIN_PX)) {
193
+ framing = "edge_clipped";
194
+ } else if (aspect < t.ID3_BAND_ASPECT_MIN || aspect > t.ID3_BAND_ASPECT_MAX) {
195
+ framing = "wrong_shape";
196
+ }
197
+ } else {
198
+ const areaFrac = quadAreaFrac(quad, frameWidth, frameHeight);
199
+ const band = aspectBand(input.format);
200
+ const bounds = quadBounds(quad);
201
+ const aspect = Math.max(bounds.width, bounds.height) / Math.max(1e-6, Math.min(bounds.width, bounds.height));
202
+ if (areaFrac < t.MIN_AREA_FRAC) framing = "too_far";
203
+ else if (areaFrac > a.MAX_AREA_FRAC) framing = "too_close";
204
+ else if (edgeClipped(quad, frameWidth, frameHeight, t.EDGE_CLIP_MARGIN_PX)) {
205
+ framing = "edge_clipped";
206
+ } else if (Math.abs(aspect - band.center) > band.tolerance) framing = "wrong_shape";
207
+ else if (quadTiltRatio(quad) > a.TILT_MAX_SIDE_RATIO) framing = "tilted";
208
+ }
209
+ const lighting = measures.exposure >= t.GUIDE_EXPOSURE ? "ok" : measures.meanLuma < (EXPOSURE_BAND[0] + EXPOSURE_BAND[1]) / 2 ? "too_dark" : "too_bright";
210
+ const blur = measures.sharpness >= t.GUIDE_SHARPNESS ? "ok" : "blurry";
211
+ const glare = measures.specularFraction >= a.GLARE_MAX_SPECULAR_FRAC ? "glare" : "ok";
212
+ return {
213
+ framing,
214
+ lighting,
215
+ blur,
216
+ glare,
217
+ acceptable: framing === "ok" && lighting === "ok" && blur === "ok" && glare === "ok"
218
+ };
219
+ }
220
+ function edgeClipped(quad, frameWidth, frameHeight, margin) {
221
+ const pts = [quad.topLeft, quad.topRight, quad.bottomRight, quad.bottomLeft];
222
+ return pts.some(
223
+ (p) => p.x <= margin || p.y <= margin || p.x >= frameWidth - margin || p.y >= frameHeight - margin
224
+ );
225
+ }
226
+
227
+ // src/core/doc/feedback-aggregator.ts
228
+ var DOC_FEEDBACK_STATES = Object.freeze({
229
+ sensing: { key: "sensing", reticle: "searching", minDurationMs: 900 },
230
+ too_far: { key: "too_far", reticle: "coaching", minDurationMs: 1400 },
231
+ too_close: { key: "too_close", reticle: "coaching", minDurationMs: 1400 },
232
+ edge_clipped: { key: "edge_clipped", reticle: "coaching", minDurationMs: 1400 },
233
+ wrong_shape: { key: "wrong_shape", reticle: "coaching", minDurationMs: 1400 },
234
+ tilted: { key: "tilted", reticle: "coaching", minDurationMs: 1400 },
235
+ too_dark: { key: "too_dark", reticle: "coaching", minDurationMs: 1400 },
236
+ too_bright: { key: "too_bright", reticle: "coaching", minDurationMs: 1400 },
237
+ blurry: { key: "blurry", reticle: "coaching", minDurationMs: 1400 },
238
+ glare: { key: "glare", reticle: "coaching", minDurationMs: 1400 },
239
+ hold_still: { key: "hold_still", reticle: "locking", minDurationMs: 900 },
240
+ processing: { key: "processing", reticle: "processing", minDurationMs: 800 },
241
+ side_captured: { key: "side_captured", reticle: "flip", minDurationMs: 2600 },
242
+ captured: { key: "captured", reticle: "done", minDurationMs: 800 }
243
+ });
244
+ function feedbackVoteOf(analysis) {
245
+ if (analysis.framing === "no_document") return "sensing";
246
+ if (analysis.framing === "too_far") return "too_far";
247
+ if (analysis.framing === "too_close") return "too_close";
248
+ if (analysis.framing === "edge_clipped") return "edge_clipped";
249
+ if (analysis.framing === "wrong_shape") return "wrong_shape";
250
+ if (analysis.framing === "tilted") return "tilted";
251
+ if (analysis.lighting === "too_dark") return "too_dark";
252
+ if (analysis.lighting === "too_bright") return "too_bright";
253
+ if (analysis.glare === "glare") return "glare";
254
+ if (analysis.blur === "blurry") return "blurry";
255
+ return "hold_still";
256
+ }
257
+ var DocFeedbackAggregator = class {
258
+ constructor(windowMs = 2600, decayPerSecond = 0.35) {
259
+ this.windowMs = windowMs;
260
+ this.decayPerSecond = decayPerSecond;
261
+ this.votes = [];
262
+ this.current = DOC_FEEDBACK_STATES.sensing;
263
+ this.currentSinceMs = null;
264
+ }
265
+ reset() {
266
+ this.votes = [];
267
+ this.current = DOC_FEEDBACK_STATES.sensing;
268
+ this.currentSinceMs = null;
269
+ }
270
+ /** Force a terminal/choreography state (capture fired, side flip, upload). */
271
+ force(key, nowMs) {
272
+ this.votes = [];
273
+ this.current = DOC_FEEDBACK_STATES[key];
274
+ this.currentSinceMs = nowMs;
275
+ return this.current;
276
+ }
277
+ /** Cast this frame's vote and return the (possibly held) winning state. */
278
+ tick(vote, nowMs) {
279
+ if (this.currentSinceMs === null) this.currentSinceMs = nowMs;
280
+ this.votes.push({ key: vote, atMs: nowMs });
281
+ this.votes = this.votes.filter((v) => nowMs - v.atMs <= this.windowMs);
282
+ if (nowMs - this.currentSinceMs < this.current.minDurationMs) return this.current;
283
+ const scores = /* @__PURE__ */ new Map();
284
+ for (const v of this.votes) {
285
+ const ageSeconds = (nowMs - v.atMs) / 1e3;
286
+ const weight = this.decayPerSecond ** ageSeconds;
287
+ scores.set(v.key, (scores.get(v.key) ?? 0) + weight);
288
+ }
289
+ let winner = this.current.key;
290
+ let best = -1;
291
+ for (const [key, score] of scores) {
292
+ if (score > best) {
293
+ best = score;
294
+ winner = key;
295
+ }
296
+ }
297
+ if (winner !== this.current.key) {
298
+ this.current = DOC_FEEDBACK_STATES[winner];
299
+ this.currentSinceMs = nowMs;
300
+ }
301
+ return this.current;
302
+ }
303
+ };
304
+
305
+ // src/core/doc/capture-session-doc.ts
306
+ var scanicDetect = null;
307
+ async function lazyScanicDetect(image) {
308
+ if (scanicDetect === null) {
309
+ const module_ = await import("../detector-scanic-4C6IEW2K.js");
310
+ scanicDetect = module_.detectQuad;
311
+ }
312
+ return scanicDetect(image);
313
+ }
314
+ async function mrzBandDetect(image) {
315
+ const band = detectMrzBand(image);
316
+ if (band === null) return null;
317
+ return {
318
+ topLeft: { x: band.x, y: band.y },
319
+ topRight: { x: band.x + band.width, y: band.y },
320
+ bottomRight: { x: band.x + band.width, y: band.y + band.height },
321
+ bottomLeft: { x: band.x, y: band.y + band.height }
322
+ };
323
+ }
324
+ function warmDocDetector() {
325
+ if (scanicDetect !== null) return;
326
+ void import("../detector-scanic-4C6IEW2K.js").then((module_) => {
327
+ scanicDetect = module_.detectQuad;
328
+ }).catch(() => void 0);
329
+ }
330
+ var DOC_CADENCE_HZ = Object.freeze({
331
+ LOW: 3,
332
+ MID: 5,
333
+ HIGH: 6
334
+ });
335
+ var CaptureSessionDoc = class {
336
+ constructor(opts) {
337
+ this.handlers = /* @__PURE__ */ new Map();
338
+ this.stream = null;
339
+ this.timer = null;
340
+ this.state = "idle";
341
+ this.lastGuidance = null;
342
+ this.attempt = 0;
343
+ this.capturing = false;
344
+ this.feedback = new DocFeedbackAggregator();
345
+ this.lastFeedbackKey = null;
346
+ this.opts = opts;
347
+ this.gate = new DocGateV1(opts.format ?? "ID-1", opts.thresholds);
348
+ const deviceClass = opts.deviceClass ?? classifyDevice(probeDevice());
349
+ this.cadenceMs = 1e3 / DOC_CADENCE_HZ[deviceClass];
350
+ }
351
+ on(event, handler) {
352
+ let set = this.handlers.get(event);
353
+ if (!set) {
354
+ set = /* @__PURE__ */ new Set();
355
+ this.handlers.set(event, set);
356
+ }
357
+ set.add(handler);
358
+ return () => set?.delete(handler);
359
+ }
360
+ emit(event, payload) {
361
+ for (const handler of [...this.handlers.get(event) ?? []]) {
362
+ handler(payload);
363
+ }
364
+ }
365
+ async start() {
366
+ const attempt = ++this.attempt;
367
+ this.gate.reset();
368
+ this.feedback.reset();
369
+ this.lastFeedbackKey = null;
370
+ this.lastGuidance = null;
371
+ this.capturing = false;
372
+ this.setState("preflight");
373
+ try {
374
+ const getUserMedia = this.opts.getUserMedia ?? ((constraints) => navigator.mediaDevices.getUserMedia(constraints));
375
+ this.setState("acquiring");
376
+ this.stream = await getUserMedia({
377
+ audio: false,
378
+ video: {
379
+ facingMode: { ideal: "environment" },
380
+ width: { ideal: 1920 },
381
+ height: { ideal: 1080 }
382
+ }
383
+ });
384
+ if (attempt !== this.attempt) {
385
+ for (const track of this.stream.getTracks()) track.stop();
386
+ return;
387
+ }
388
+ this.opts.video.srcObject = this.stream;
389
+ await this.opts.video.play().catch(() => void 0);
390
+ this.setState("positioning");
391
+ this.scheduleTick(attempt);
392
+ } catch (err) {
393
+ const kind = mapGetUserMediaError(err);
394
+ this.setState("error");
395
+ this.emit("error", { kind, retryable: true });
396
+ }
397
+ }
398
+ /** Restart the gate for another side / a retake (same stream). */
399
+ rearm(side) {
400
+ if (side !== void 0) this.opts.side = side;
401
+ this.gate.reset();
402
+ this.feedback.reset();
403
+ this.lastFeedbackKey = null;
404
+ this.lastGuidance = null;
405
+ this.capturing = false;
406
+ if (this.state === "captured") {
407
+ this.setState("positioning");
408
+ this.scheduleTick(this.attempt);
409
+ }
410
+ }
411
+ destroy() {
412
+ this.attempt += 1;
413
+ if (this.timer !== null) (this.opts.clearTimer ?? clearTimeout)(this.timer);
414
+ this.timer = null;
415
+ if (this.stream) for (const track of this.stream.getTracks()) track.stop();
416
+ this.stream = null;
417
+ this.opts.video.srcObject = null;
418
+ }
419
+ setState(state) {
420
+ this.state = state;
421
+ this.emit("statechange", { state });
422
+ }
423
+ scheduleTick(attempt) {
424
+ const setTimer = this.opts.setTimer ?? setTimeout;
425
+ this.timer = setTimer(() => void this.tick(attempt), this.cadenceMs);
426
+ }
427
+ async tick(attempt) {
428
+ if (attempt !== this.attempt || this.state !== "positioning" || this.capturing) return;
429
+ const now = this.opts.now ?? (() => performance.now());
430
+ const grab = this.opts.grabDetectFrame ?? defaultGrabDetectFrame;
431
+ const detect = this.opts.detect ?? ((this.opts.format ?? "ID-1") === "ID-3" ? mrzBandDetect : lazyScanicDetect);
432
+ const frame = grab(this.opts.video, DOC_DETECT_WIDTH);
433
+ if (frame === null) {
434
+ this.scheduleTick(attempt);
435
+ return;
436
+ }
437
+ let quad = null;
438
+ try {
439
+ quad = await detect(frame);
440
+ } catch {
441
+ quad = null;
442
+ }
443
+ if (attempt !== this.attempt) return;
444
+ const crop = quad !== null ? cropQuadRegion(frame, quad) : frame;
445
+ const atMs = now();
446
+ const measures = {
447
+ sharpness: sharpness(crop),
448
+ exposure: exposure(crop),
449
+ meanLuma: meanLuma(crop),
450
+ specularFraction: specularFraction(crop)
451
+ };
452
+ const analysis = analyzeDocFrame({
453
+ format: this.opts.format ?? "ID-1",
454
+ quad,
455
+ frameWidth: frame.width,
456
+ frameHeight: frame.height,
457
+ measures,
458
+ ...this.opts.thresholds !== void 0 ? { thresholds: this.opts.thresholds } : {}
459
+ });
460
+ const tick = this.gate.tick({
461
+ timestampMs: atMs,
462
+ quad,
463
+ quality: measures,
464
+ frameWidth: frame.width,
465
+ frameHeight: frame.height
466
+ });
467
+ this.emit("checks", {
468
+ engine: "docv1",
469
+ p: tick.p,
470
+ plausible: tick.plausible,
471
+ stableTicks: tick.stableTicks,
472
+ quad: tick.quad,
473
+ frameWidth: frame.width,
474
+ frameHeight: frame.height,
475
+ analysis
476
+ });
477
+ const feedbackState = this.feedback.tick(feedbackVoteOf(analysis), atMs);
478
+ if (feedbackState.key !== this.lastFeedbackKey) {
479
+ this.lastFeedbackKey = feedbackState.key;
480
+ this.emit("feedback", feedbackState);
481
+ }
482
+ if (tick.guidanceKey !== this.lastGuidance) {
483
+ this.lastGuidance = tick.guidanceKey;
484
+ this.emit("guidance", { key: tick.guidanceKey });
485
+ }
486
+ if (tick.ready && quad !== null) {
487
+ const processing = this.feedback.force("processing", atMs);
488
+ this.lastFeedbackKey = processing.key;
489
+ this.emit("feedback", processing);
490
+ await this.fire(attempt, quad, {
491
+ width: frame.width,
492
+ height: frame.height,
493
+ tickSharpness: measures.sharpness
494
+ });
495
+ return;
496
+ }
497
+ this.scheduleTick(attempt);
498
+ }
499
+ /**
500
+ * BEST-FRAME capture window (capture-v2): the first ready tick captures a
501
+ * candidate immediately, then up to CAPTURE_EXTRA_CANDIDATES more stills are
502
+ * taken at CAPTURE_CANDIDATE_SPACING_MS while the user holds — each scored
503
+ * by the live detect-tick sharpness at its capture moment — and the SHARPEST
504
+ * wins. Beats first-acceptable-frame capture without decoding stills.
505
+ */
506
+ async fire(attempt, quad, detectSize) {
507
+ this.capturing = true;
508
+ const capture = this.opts.captureStill ?? defaultCaptureStill;
509
+ const grab = this.opts.grabDetectFrame ?? defaultGrabDetectFrame;
510
+ const setTimer = this.opts.setTimer ?? setTimeout;
511
+ const size = { width: detectSize.width, height: detectSize.height };
512
+ try {
513
+ let best = {
514
+ still: await capture(this.opts.video, quad, size),
515
+ sharpness: detectSize.tickSharpness
516
+ };
517
+ for (let i = 0; i < CAPTURE_EXTRA_CANDIDATES; i++) {
518
+ await new Promise((resolve) => {
519
+ setTimer(() => resolve(), CAPTURE_CANDIDATE_SPACING_MS);
520
+ });
521
+ if (attempt !== this.attempt) return;
522
+ const frame = grab(this.opts.video, DOC_DETECT_WIDTH);
523
+ if (frame === null) continue;
524
+ const candidateSharpness = sharpness(cropQuadRegion(frame, quad));
525
+ if (candidateSharpness > best.sharpness * CAPTURE_IMPROVEMENT_MIN_RATIO) {
526
+ const still = await capture(this.opts.video, quad, size);
527
+ if (attempt !== this.attempt) return;
528
+ best = { still, sharpness: candidateSharpness };
529
+ }
530
+ }
531
+ if (attempt !== this.attempt) return;
532
+ this.setState("captured");
533
+ this.emit("artifacts", {
534
+ keyframe: best.still.blob,
535
+ previewDataUrl: best.still.previewDataUrl,
536
+ ...this.opts.side !== void 0 ? { side: this.opts.side } : {}
537
+ });
538
+ } catch {
539
+ if (attempt !== this.attempt) return;
540
+ this.capturing = false;
541
+ this.gate.reset();
542
+ this.scheduleTick(attempt);
543
+ }
544
+ }
545
+ };
546
+ var CAPTURE_EXTRA_CANDIDATES = 2;
547
+ var CAPTURE_CANDIDATE_SPACING_MS = 260;
548
+ var CAPTURE_IMPROVEMENT_MIN_RATIO = 1.08;
549
+ function defaultGrabDetectFrame(video, targetWidth) {
550
+ const vw = video.videoWidth;
551
+ const vh = video.videoHeight;
552
+ if (vw === 0 || vh === 0) return null;
553
+ const scale = Math.min(1, targetWidth / vw);
554
+ const width = Math.round(vw * scale);
555
+ const height = Math.round(vh * scale);
556
+ const canvas = document.createElement("canvas");
557
+ canvas.width = width;
558
+ canvas.height = height;
559
+ const context = canvas.getContext("2d", { willReadFrequently: true });
560
+ if (context === null) return null;
561
+ context.drawImage(video, 0, 0, width, height);
562
+ return context.getImageData(0, 0, width, height);
563
+ }
564
+ async function defaultCaptureStill(video, _quad, _detectSize) {
565
+ const canvas = document.createElement("canvas");
566
+ canvas.width = Math.max(1, video.videoWidth);
567
+ canvas.height = Math.max(1, video.videoHeight);
568
+ const context = canvas.getContext("2d");
569
+ if (context === null) throw new Error("2d context unavailable");
570
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
571
+ const blob = await new Promise((resolve, reject) => {
572
+ canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), "image/jpeg", 0.92);
573
+ });
574
+ return { blob, previewDataUrl: canvas.toDataURL("image/jpeg", 0.85) };
575
+ }
576
+ function cropQuadRegion(frame, quad) {
577
+ const xs = [quad.topLeft.x, quad.topRight.x, quad.bottomRight.x, quad.bottomLeft.x];
578
+ const ys = [quad.topLeft.y, quad.topRight.y, quad.bottomRight.y, quad.bottomLeft.y];
579
+ const x1 = Math.max(0, Math.floor(Math.min(...xs)));
580
+ const y1 = Math.max(0, Math.floor(Math.min(...ys)));
581
+ const x2 = Math.min(frame.width, Math.ceil(Math.max(...xs)));
582
+ const y2 = Math.min(frame.height, Math.ceil(Math.max(...ys)));
583
+ const width = Math.max(1, x2 - x1);
584
+ const height = Math.max(1, y2 - y1);
585
+ const data = new Uint8ClampedArray(width * height * 4);
586
+ for (let row = 0; row < height; row++) {
587
+ const src = ((y1 + row) * frame.width + x1) * 4;
588
+ data.set(frame.data.subarray(src, src + width * 4), row * width * 4);
589
+ }
590
+ return { data, width, height, colorSpace: frame.colorSpace };
591
+ }
592
+ export {
593
+ ASPECT_BANDS,
594
+ AttestationError,
595
+ CAPTURE_CANDIDATE_SPACING_MS,
596
+ CAPTURE_EXTRA_CANDIDATES,
597
+ CENTERED_FACTOR,
598
+ CIRCLE_RADIUS_FACTOR,
599
+ CLIP_MIME_ALLOWLIST,
600
+ CaptureSessionDoc,
601
+ DOC_ANALYSIS_DEFAULTS,
602
+ DOC_CADENCE_HZ,
603
+ DOC_DETECT_WIDTH,
604
+ DOC_FEEDBACK_STATES,
605
+ DOC_GATE_V1_DEFAULTS,
606
+ DocFeedbackAggregator,
607
+ DocGateV1,
608
+ IN_CIRCLE_FACTOR,
609
+ MRZ_BAND_DEFAULTS,
610
+ MR_DOC_CATALOG,
611
+ PROCESSING_POLL_DELAY_MS,
612
+ PROCESSING_POLL_MAX,
613
+ REGISTRY_RETRY_DELAY_MS,
614
+ RETRYABLE_REASONS,
615
+ TRANSPORT_REPLAY_ATTEMPTS,
616
+ TRANSPORT_REPLAY_DELAY_MS,
617
+ TURNSTILE_EXECUTE_TIMEOUT_MS,
618
+ TURNSTILE_SCRIPT_SRC,
619
+ analyzeDocFrame,
620
+ aspectBand,
621
+ createLivenessCaptureInternal,
622
+ createVerificationInternal,
623
+ detectMrzBand,
624
+ executeTurnstile,
625
+ feedbackVoteOf,
626
+ frontierStep,
627
+ isExecutable,
628
+ isTerminalView,
629
+ loadTurnstile,
630
+ mapEngineState,
631
+ mapErrorKind,
632
+ mapGuidanceHint,
633
+ mapSessionResult,
634
+ mergeSessionView,
635
+ quadAreaFrac,
636
+ quadBounds,
637
+ quadIou,
638
+ quadTiltRatio,
639
+ randomUuid,
640
+ resetTurnstileForTests,
641
+ runLivenessFlow,
642
+ runSubmission,
643
+ specularFraction,
644
+ stepOf,
645
+ warmDocDetector
646
+ };
647
+ //# sourceMappingURL=index.js.map