@sekyro/web 0.1.0-rc.1 → 0.1.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/{capture-session-v3-CAoWM8iT.d.cts → capture-session-v3-COIO_F2w.d.cts} +7 -6
- package/dist/{capture-session-v3-BJ8ciaWl.d.ts → capture-session-v3-CvIfcwST.d.ts} +7 -6
- package/dist/{chunk-LOTAJDRE.js → chunk-7TC6TOZ4.js} +8 -7
- package/dist/chunk-7TC6TOZ4.js.map +1 -0
- package/dist/{chunk-NU4YOLCN.js → chunk-FIXX2AXC.js} +144 -76
- package/dist/chunk-FIXX2AXC.js.map +1 -0
- package/dist/core/index.cjs +7 -6
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/{create-verification--ldwakXV.d.cts → create-verification-CP18eq1Q.d.cts} +16 -9
- package/dist/{create-verification-BlAPWeW5.d.ts → create-verification-CswF2As-.d.ts} +16 -9
- package/dist/index.cjs +150 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/internal/index.cjs +149 -80
- package/dist/internal/index.cjs.map +1 -1
- package/dist/internal/index.d.cts +3 -3
- package/dist/internal/index.d.ts +3 -3
- package/dist/internal/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-LOTAJDRE.js.map +0 -1
- package/dist/chunk-NU4YOLCN.js.map +0 -1
package/dist/core/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CameraErrorKind } from '../capture-session-v3-
|
|
2
|
-
export { a as CLIP_MS, b as CaptureArtifacts, c as CaptureErrorKind, d as CaptureSessionEvents, e as CaptureSessionV3, f as CaptureSessionV3Events, g as CaptureSessionV3Options, h as CaptureState, i as ClipError, j as ClipErrorKind, k as ClipRecorder, l as ClipResult, D as DEFAULT_CLIP_MIME, m as DEVICE_PROFILES, n as DeviceClass, o as DeviceProbe, p as DeviceProfile, q as DeviceProfiler, E as EscalationClipArtifact, r as EscalationHook, F as FaceGateV3, G as GateChecks, K as KEYFRAME_CANDIDATES, s as KEYFRAME_WINDOW_MS, M as MAX_CLIP_BYTES, t as MIME_CANDIDATES, S as STOP_GRACE_MS, T as TIMESLICE_MS, V as V3ChecksPayload, u as V3GateFactor, v as V3GateQuality, w as V3GateThresholds, x as V3GateTick, y as V3GateTickInput, z as V3TelemetryEvent, A as V3_GATE_DEFAULTS, B as VIDEO_BPS, H as buildLivenessV2FormData, I as cameraPreflight, J as classifyDevice, L as extForMime, N as isClipTooLarge, O as isMediaRecorderSupported, P as mapGetUserMediaError, Q as pickMimeType, R as probeDevice } from '../capture-session-v3-
|
|
1
|
+
import { C as CameraErrorKind } from '../capture-session-v3-COIO_F2w.cjs';
|
|
2
|
+
export { a as CLIP_MS, b as CaptureArtifacts, c as CaptureErrorKind, d as CaptureSessionEvents, e as CaptureSessionV3, f as CaptureSessionV3Events, g as CaptureSessionV3Options, h as CaptureState, i as ClipError, j as ClipErrorKind, k as ClipRecorder, l as ClipResult, D as DEFAULT_CLIP_MIME, m as DEVICE_PROFILES, n as DeviceClass, o as DeviceProbe, p as DeviceProfile, q as DeviceProfiler, E as EscalationClipArtifact, r as EscalationHook, F as FaceGateV3, G as GateChecks, K as KEYFRAME_CANDIDATES, s as KEYFRAME_WINDOW_MS, M as MAX_CLIP_BYTES, t as MIME_CANDIDATES, S as STOP_GRACE_MS, T as TIMESLICE_MS, V as V3ChecksPayload, u as V3GateFactor, v as V3GateQuality, w as V3GateThresholds, x as V3GateTick, y as V3GateTickInput, z as V3TelemetryEvent, A as V3_GATE_DEFAULTS, B as VIDEO_BPS, H as buildLivenessV2FormData, I as cameraPreflight, J as classifyDevice, L as extForMime, N as isClipTooLarge, O as isMediaRecorderSupported, P as mapGetUserMediaError, Q as pickMimeType, R as probeDevice } from '../capture-session-v3-COIO_F2w.cjs';
|
|
3
3
|
export { C as CENTERED_FACTOR, a as CIRCLE_RADIUS_FACTOR, I as IN_CIRCLE_FACTOR, b as buildCaptureConstraints, d as detectIsMobile } from '../capture-constraints-BujkZcfb.cjs';
|
|
4
4
|
export { C as CreateDetectorOptions, D as DEFAULT_TIER_ORDER, a as DEFAULT_V3_ASSET_BASE_URL, b as Detector, c as DetectorLadderError, F as FaceObservation, d as createDetector, i as intrinsicSize, t as toStreamPixels } from '../detect-worker-CKXHlXx9.cjs';
|
|
5
5
|
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CameraErrorKind } from '../capture-session-v3-
|
|
2
|
-
export { a as CLIP_MS, b as CaptureArtifacts, c as CaptureErrorKind, d as CaptureSessionEvents, e as CaptureSessionV3, f as CaptureSessionV3Events, g as CaptureSessionV3Options, h as CaptureState, i as ClipError, j as ClipErrorKind, k as ClipRecorder, l as ClipResult, D as DEFAULT_CLIP_MIME, m as DEVICE_PROFILES, n as DeviceClass, o as DeviceProbe, p as DeviceProfile, q as DeviceProfiler, E as EscalationClipArtifact, r as EscalationHook, F as FaceGateV3, G as GateChecks, K as KEYFRAME_CANDIDATES, s as KEYFRAME_WINDOW_MS, M as MAX_CLIP_BYTES, t as MIME_CANDIDATES, S as STOP_GRACE_MS, T as TIMESLICE_MS, V as V3ChecksPayload, u as V3GateFactor, v as V3GateQuality, w as V3GateThresholds, x as V3GateTick, y as V3GateTickInput, z as V3TelemetryEvent, A as V3_GATE_DEFAULTS, B as VIDEO_BPS, H as buildLivenessV2FormData, I as cameraPreflight, J as classifyDevice, L as extForMime, N as isClipTooLarge, O as isMediaRecorderSupported, P as mapGetUserMediaError, Q as pickMimeType, R as probeDevice } from '../capture-session-v3-
|
|
1
|
+
import { C as CameraErrorKind } from '../capture-session-v3-CvIfcwST.js';
|
|
2
|
+
export { a as CLIP_MS, b as CaptureArtifacts, c as CaptureErrorKind, d as CaptureSessionEvents, e as CaptureSessionV3, f as CaptureSessionV3Events, g as CaptureSessionV3Options, h as CaptureState, i as ClipError, j as ClipErrorKind, k as ClipRecorder, l as ClipResult, D as DEFAULT_CLIP_MIME, m as DEVICE_PROFILES, n as DeviceClass, o as DeviceProbe, p as DeviceProfile, q as DeviceProfiler, E as EscalationClipArtifact, r as EscalationHook, F as FaceGateV3, G as GateChecks, K as KEYFRAME_CANDIDATES, s as KEYFRAME_WINDOW_MS, M as MAX_CLIP_BYTES, t as MIME_CANDIDATES, S as STOP_GRACE_MS, T as TIMESLICE_MS, V as V3ChecksPayload, u as V3GateFactor, v as V3GateQuality, w as V3GateThresholds, x as V3GateTick, y as V3GateTickInput, z as V3TelemetryEvent, A as V3_GATE_DEFAULTS, B as VIDEO_BPS, H as buildLivenessV2FormData, I as cameraPreflight, J as classifyDevice, L as extForMime, N as isClipTooLarge, O as isMediaRecorderSupported, P as mapGetUserMediaError, Q as pickMimeType, R as probeDevice } from '../capture-session-v3-CvIfcwST.js';
|
|
3
3
|
export { C as CENTERED_FACTOR, a as CIRCLE_RADIUS_FACTOR, I as IN_CIRCLE_FACTOR, b as buildCaptureConstraints, d as detectIsMobile } from '../capture-constraints-BujkZcfb.js';
|
|
4
4
|
export { C as CreateDetectorOptions, D as DEFAULT_TIER_ORDER, a as DEFAULT_V3_ASSET_BASE_URL, b as Detector, c as DetectorLadderError, F as FaceObservation, d as createDetector, i as intrinsicSize, t as toStreamPixels } from '../detect-worker-CKXHlXx9.js';
|
|
5
5
|
|
package/dist/core/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as CaptureState$1, f as CaptureSessionV3Events, g as CaptureSessionV3Options, c as CaptureErrorKind$1 } from './capture-session-v3-
|
|
1
|
+
import { h as CaptureState$1, f as CaptureSessionV3Events, g as CaptureSessionV3Options, c as CaptureErrorKind$1 } from './capture-session-v3-COIO_F2w.cjs';
|
|
2
2
|
import { C as CheckReason, c as HostedSessionView, g as HostedClient, A as AttestationInput, h as SessionView, b as CheckType } from './hosted-client-BpvBqu9f.cjs';
|
|
3
3
|
|
|
4
4
|
/** Gate guidance code. `'hold'` is emitted while the clip records. */
|
|
@@ -274,11 +274,13 @@ interface LivenessFlowDeps extends SubmitRuntime {
|
|
|
274
274
|
getAttestation(): Promise<AttestationInput>;
|
|
275
275
|
/** Latest hosted view (frontier decisions). */
|
|
276
276
|
getView(): HostedSessionView;
|
|
277
|
+
/** Re-GET the view from the server (after a RESYNC code) and fold it in. */
|
|
278
|
+
refreshView(): Promise<HostedSessionView>;
|
|
277
279
|
/** Fold a submit response into the view. */
|
|
278
280
|
applySubmit(view: SessionView, submitted: CheckType): void;
|
|
279
281
|
/** Flow-state hook for the capture handle. */
|
|
280
282
|
onPhase(phase: "uploading" | "checking"): void;
|
|
281
|
-
/**
|
|
283
|
+
/** The verification was destroyed (the only thing that cancels a chain). */
|
|
282
284
|
isCancelled(): boolean;
|
|
283
285
|
}
|
|
284
286
|
type LivenessFlowOutcome = {
|
|
@@ -292,13 +294,18 @@ type LivenessFlowOutcome = {
|
|
|
292
294
|
kind: "cancelled";
|
|
293
295
|
};
|
|
294
296
|
/**
|
|
295
|
-
* Run the biometric submissions
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
297
|
+
* Run the biometric submissions the CURRENT frontier allows, re-evaluating
|
|
298
|
+
* after every submit and after every RESYNC re-GET:
|
|
299
|
+
* - frontier `liveness` → clip upload + attestation + `liveness` (once per capture);
|
|
300
|
+
* - frontier `face_match` → keyframe upload + `face_match` (once per capture),
|
|
301
|
+
* allowed when liveness was NOT part of this capture (it had already
|
|
302
|
+
* settled — any state, incl. `failed`: the selfie-fallback case, where this
|
|
303
|
+
* capture only feeds face_match) or settled chainable from this capture
|
|
304
|
+
* (passed / review / processing / skipped — or absent);
|
|
305
|
+
* - anything else (terminal view, a non-biometric frontier, a check this
|
|
306
|
+
* capture already submitted) → `settled` for the caller to map the view.
|
|
307
|
+
* A RESYNC code re-GETs the view and re-enters (at most {@link RESYNC_REENTRY_MAX}
|
|
308
|
+
* times); the final RESYNC surfaces as `resync` for the caller to map.
|
|
302
309
|
*/
|
|
303
310
|
declare function runLivenessFlow(deps: LivenessFlowDeps): Promise<LivenessFlowOutcome>;
|
|
304
311
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as CaptureState$1, f as CaptureSessionV3Events, g as CaptureSessionV3Options, c as CaptureErrorKind$1 } from './capture-session-v3-
|
|
1
|
+
import { h as CaptureState$1, f as CaptureSessionV3Events, g as CaptureSessionV3Options, c as CaptureErrorKind$1 } from './capture-session-v3-CvIfcwST.js';
|
|
2
2
|
import { C as CheckReason, c as HostedSessionView, g as HostedClient, A as AttestationInput, h as SessionView, b as CheckType } from './hosted-client-BpvBqu9f.js';
|
|
3
3
|
|
|
4
4
|
/** Gate guidance code. `'hold'` is emitted while the clip records. */
|
|
@@ -274,11 +274,13 @@ interface LivenessFlowDeps extends SubmitRuntime {
|
|
|
274
274
|
getAttestation(): Promise<AttestationInput>;
|
|
275
275
|
/** Latest hosted view (frontier decisions). */
|
|
276
276
|
getView(): HostedSessionView;
|
|
277
|
+
/** Re-GET the view from the server (after a RESYNC code) and fold it in. */
|
|
278
|
+
refreshView(): Promise<HostedSessionView>;
|
|
277
279
|
/** Fold a submit response into the view. */
|
|
278
280
|
applySubmit(view: SessionView, submitted: CheckType): void;
|
|
279
281
|
/** Flow-state hook for the capture handle. */
|
|
280
282
|
onPhase(phase: "uploading" | "checking"): void;
|
|
281
|
-
/**
|
|
283
|
+
/** The verification was destroyed (the only thing that cancels a chain). */
|
|
282
284
|
isCancelled(): boolean;
|
|
283
285
|
}
|
|
284
286
|
type LivenessFlowOutcome = {
|
|
@@ -292,13 +294,18 @@ type LivenessFlowOutcome = {
|
|
|
292
294
|
kind: "cancelled";
|
|
293
295
|
};
|
|
294
296
|
/**
|
|
295
|
-
* Run the biometric submissions
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
297
|
+
* Run the biometric submissions the CURRENT frontier allows, re-evaluating
|
|
298
|
+
* after every submit and after every RESYNC re-GET:
|
|
299
|
+
* - frontier `liveness` → clip upload + attestation + `liveness` (once per capture);
|
|
300
|
+
* - frontier `face_match` → keyframe upload + `face_match` (once per capture),
|
|
301
|
+
* allowed when liveness was NOT part of this capture (it had already
|
|
302
|
+
* settled — any state, incl. `failed`: the selfie-fallback case, where this
|
|
303
|
+
* capture only feeds face_match) or settled chainable from this capture
|
|
304
|
+
* (passed / review / processing / skipped — or absent);
|
|
305
|
+
* - anything else (terminal view, a non-biometric frontier, a check this
|
|
306
|
+
* capture already submitted) → `settled` for the caller to map the view.
|
|
307
|
+
* A RESYNC code re-GETs the view and re-enters (at most {@link RESYNC_REENTRY_MAX}
|
|
308
|
+
* times); the final RESYNC surfaces as `resync` for the caller to map.
|
|
302
309
|
*/
|
|
303
310
|
declare function runLivenessFlow(deps: LivenessFlowDeps): Promise<LivenessFlowOutcome>;
|
|
304
311
|
|
package/dist/index.cjs
CHANGED
|
@@ -862,10 +862,12 @@ var V3_GATE_DEFAULTS = Object.freeze({
|
|
|
862
862
|
MOTION_TAU_S: 0.4,
|
|
863
863
|
MOTION_DEADBAND_IOD_PER_S: 0.1,
|
|
864
864
|
READY_WINDOW_MS: 500,
|
|
865
|
-
// 0.
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
|
|
865
|
+
// (READY_MEAN_Q, the former 0.75 window MEAN, is gone — 2026-10-17 field
|
|
866
|
+
// bug: a steady q anywhere in [Q_GOOD, 0.75) saturated p at 1, passed every
|
|
867
|
+
// guide ("hold still") and could never reach the mean, so the ring sat full
|
|
868
|
+
// and green with nothing recording. Readiness now checks the window against
|
|
869
|
+
// Q_GOOD, which keeps the one-flaky-tick forgiveness and the second-bad-tick
|
|
870
|
+
// block without a dead band above the rise threshold.)
|
|
869
871
|
GUIDE_SIZE: 0.6,
|
|
870
872
|
GUIDE_CENTERING: 0.6,
|
|
871
873
|
GUIDE_POSE: 0.6,
|
|
@@ -943,8 +945,7 @@ var FaceGateV3 = class {
|
|
|
943
945
|
const worstIndex = windowSamples.indexOf(worst);
|
|
944
946
|
windowSamples = windowSamples.filter((_, index) => index !== worstIndex);
|
|
945
947
|
}
|
|
946
|
-
const
|
|
947
|
-
const ready = this.p >= 1 && windowMean >= this.t.READY_MEAN_Q;
|
|
948
|
+
const ready = this.p >= 1 && windowSamples.every((sample) => sample >= this.t.Q_GOOD);
|
|
948
949
|
return {
|
|
949
950
|
at: timestampMs,
|
|
950
951
|
q,
|
|
@@ -1562,6 +1563,14 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
1562
1563
|
let animations = "full";
|
|
1563
1564
|
let recordingStartedAt = null;
|
|
1564
1565
|
let frozenAt = null;
|
|
1566
|
+
let frameWidth = 0;
|
|
1567
|
+
let frameHeight = 0;
|
|
1568
|
+
const latchFrameSize = () => {
|
|
1569
|
+
if (options.video.videoWidth > 0 && options.video.videoHeight > 0) {
|
|
1570
|
+
frameWidth = options.video.videoWidth;
|
|
1571
|
+
frameHeight = options.video.videoHeight;
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1565
1574
|
const setState = (next) => {
|
|
1566
1575
|
state = next;
|
|
1567
1576
|
emitter.emit("state", next);
|
|
@@ -1582,10 +1591,13 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
1582
1591
|
faceDetected = false;
|
|
1583
1592
|
recordingStartedAt = null;
|
|
1584
1593
|
frozenAt = null;
|
|
1594
|
+
frameWidth = 0;
|
|
1595
|
+
frameHeight = 0;
|
|
1585
1596
|
}
|
|
1586
1597
|
setState(next);
|
|
1587
1598
|
syncProfile();
|
|
1588
1599
|
if (next === "recording") {
|
|
1600
|
+
latchFrameSize();
|
|
1589
1601
|
recordingStartedAt = now();
|
|
1590
1602
|
hint = "hold";
|
|
1591
1603
|
progress = 1;
|
|
@@ -1593,6 +1605,7 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
1593
1605
|
}
|
|
1594
1606
|
}),
|
|
1595
1607
|
engine.on("checks", (payload) => {
|
|
1608
|
+
latchFrameSize();
|
|
1596
1609
|
progress = payload.progress;
|
|
1597
1610
|
faceDetected = payload.factors.presence > 0;
|
|
1598
1611
|
emitGate();
|
|
@@ -1612,8 +1625,8 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
1612
1625
|
keyframe: artifacts.keyframe,
|
|
1613
1626
|
...artifacts.clip !== void 0 ? { clip: artifacts.clip } : {},
|
|
1614
1627
|
...artifacts.clipMimeType !== void 0 ? { clipMime: artifacts.clipMimeType } : {},
|
|
1615
|
-
width: options.video.videoWidth,
|
|
1616
|
-
height: options.video.videoHeight,
|
|
1628
|
+
width: options.video.videoWidth || frameWidth,
|
|
1629
|
+
height: options.video.videoHeight || frameHeight,
|
|
1617
1630
|
...clipMs !== void 0 ? { clipMs } : {},
|
|
1618
1631
|
captureChannel: detectIsMobile() ? "mobile" : "desktop",
|
|
1619
1632
|
...degraded ? { degraded: "NO_RECORDER" } : {}
|
|
@@ -2064,6 +2077,7 @@ var KEYFRAME_MIME = "image/jpeg";
|
|
|
2064
2077
|
var TRANSPORT_REPLAY_ATTEMPTS = 2;
|
|
2065
2078
|
var TRANSPORT_REPLAY_DELAY_MS = 1e3;
|
|
2066
2079
|
var MAX_RETRY_AFTER_MS = 1e4;
|
|
2080
|
+
var RESYNC_REENTRY_MAX = 3;
|
|
2067
2081
|
var TRANSPORT_CODES = /* @__PURE__ */ new Set([
|
|
2068
2082
|
"network_error",
|
|
2069
2083
|
"timeout",
|
|
@@ -2124,6 +2138,7 @@ async function runSubmission(rt, executor, hooks = {}) {
|
|
|
2124
2138
|
}
|
|
2125
2139
|
}
|
|
2126
2140
|
}
|
|
2141
|
+
var LIVENESS_CHAINABLE = /* @__PURE__ */ new Set(["passed", "review", "processing", "skipped"]);
|
|
2127
2142
|
async function runLivenessFlow(deps) {
|
|
2128
2143
|
const { artifacts } = deps;
|
|
2129
2144
|
const clip = artifacts.clip;
|
|
@@ -2131,67 +2146,87 @@ async function runLivenessFlow(deps) {
|
|
|
2131
2146
|
if (artifacts.degraded === "NO_RECORDER" || clip === void 0 || !CLIP_MIME_ALLOWLIST.includes(bareMime)) {
|
|
2132
2147
|
return { kind: "unavailable", reason: "no_recorder" };
|
|
2133
2148
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
if (clipKey === null) {
|
|
2143
|
-
deps.onPhase("uploading");
|
|
2144
|
-
const presigned = await client.presign("clip", bareMime, clip.size);
|
|
2145
|
-
await client.uploadToSpaces(presigned.uploadUrl, clip, bareMime);
|
|
2146
|
-
clipKey = (await client.completeMedia(presigned.key)).key;
|
|
2147
|
-
}
|
|
2148
|
-
if (attestation === null) {
|
|
2149
|
-
try {
|
|
2150
|
-
attestation = await deps.getAttestation();
|
|
2151
|
-
} catch (err) {
|
|
2152
|
-
throw new AttestationError(err instanceof Error ? err.message : String(err));
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
deps.onPhase("checking");
|
|
2156
|
-
return client.submitCheck(
|
|
2157
|
-
"liveness",
|
|
2158
|
-
{ clipKey, attestation, captureChannel: artifacts.captureChannel },
|
|
2159
|
-
{ idempotencyKey }
|
|
2160
|
-
);
|
|
2161
|
-
},
|
|
2162
|
-
{
|
|
2163
|
-
onAttestationInvalid: () => {
|
|
2164
|
-
attestation = null;
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
);
|
|
2168
|
-
if (deps.isCancelled()) return { kind: "cancelled" };
|
|
2169
|
-
deps.applySubmit(response, "liveness");
|
|
2170
|
-
}
|
|
2149
|
+
const submitLiveness = buildLivenessSubmit(deps, clip, bareMime);
|
|
2150
|
+
const submitFaceMatch = buildFaceMatchSubmit(deps, artifacts.keyframe);
|
|
2151
|
+
let livenessSubmitted = false;
|
|
2152
|
+
let faceMatchSubmitted = false;
|
|
2153
|
+
let livenessChainable = true;
|
|
2154
|
+
let resyncs = 0;
|
|
2155
|
+
for (; ; ) {
|
|
2156
|
+
if (deps.isCancelled()) return { kind: "cancelled" };
|
|
2171
2157
|
const view = deps.getView();
|
|
2172
|
-
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2158
|
+
if (isTerminalView(view)) return { kind: "settled" };
|
|
2159
|
+
const frontier = frontierStep(view)?.checkType;
|
|
2160
|
+
const next = frontier === "liveness" && !livenessSubmitted ? "liveness" : frontier === "face_match" && !faceMatchSubmitted && livenessChainable ? "face_match" : null;
|
|
2161
|
+
if (next === null) return { kind: "settled" };
|
|
2162
|
+
try {
|
|
2163
|
+
if (next === "liveness") {
|
|
2164
|
+
livenessSubmitted = true;
|
|
2165
|
+
const response = await submitLiveness();
|
|
2166
|
+
if (deps.isCancelled()) return { kind: "cancelled" };
|
|
2167
|
+
deps.applySubmit(response, "liveness");
|
|
2168
|
+
const state = stepOf(deps.getView(), "liveness")?.state;
|
|
2169
|
+
livenessChainable = state === void 0 || LIVENESS_CHAINABLE.has(state);
|
|
2170
|
+
} else {
|
|
2171
|
+
faceMatchSubmitted = true;
|
|
2172
|
+
const response = await submitFaceMatch();
|
|
2173
|
+
if (deps.isCancelled()) return { kind: "cancelled" };
|
|
2174
|
+
deps.applySubmit(response, "face_match");
|
|
2175
|
+
}
|
|
2176
|
+
} catch (err) {
|
|
2187
2177
|
if (deps.isCancelled()) return { kind: "cancelled" };
|
|
2188
|
-
|
|
2178
|
+
const outcome = classifyFailure(err);
|
|
2179
|
+
if (outcome.kind !== "resync" || resyncs >= RESYNC_REENTRY_MAX) return outcome;
|
|
2180
|
+
resyncs += 1;
|
|
2181
|
+
await deps.refreshView();
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function buildLivenessSubmit(deps, clip, bareMime) {
|
|
2186
|
+
let clipKey = null;
|
|
2187
|
+
let attestation = null;
|
|
2188
|
+
return () => runSubmission(
|
|
2189
|
+
deps,
|
|
2190
|
+
async (client, idempotencyKey) => {
|
|
2191
|
+
if (clipKey === null) {
|
|
2192
|
+
deps.onPhase("uploading");
|
|
2193
|
+
const presigned = await client.presign("clip", bareMime, clip.size);
|
|
2194
|
+
await client.uploadToSpaces(presigned.uploadUrl, clip, bareMime);
|
|
2195
|
+
clipKey = (await client.completeMedia(presigned.key)).key;
|
|
2196
|
+
}
|
|
2197
|
+
if (attestation === null) {
|
|
2198
|
+
try {
|
|
2199
|
+
attestation = await deps.getAttestation();
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
throw new AttestationError(err instanceof Error ? err.message : String(err));
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
deps.onPhase("checking");
|
|
2205
|
+
return client.submitCheck(
|
|
2206
|
+
"liveness",
|
|
2207
|
+
{ clipKey, attestation, captureChannel: deps.artifacts.captureChannel },
|
|
2208
|
+
{ idempotencyKey }
|
|
2209
|
+
);
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
onAttestationInvalid: () => {
|
|
2213
|
+
attestation = null;
|
|
2214
|
+
}
|
|
2189
2215
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
function buildFaceMatchSubmit(deps, keyframe) {
|
|
2219
|
+
let imageKey = null;
|
|
2220
|
+
return () => runSubmission(deps, async (client, idempotencyKey) => {
|
|
2221
|
+
if (imageKey === null) {
|
|
2222
|
+
deps.onPhase("uploading");
|
|
2223
|
+
const presigned = await client.presign("image", KEYFRAME_MIME, keyframe.size);
|
|
2224
|
+
await client.uploadToSpaces(presigned.uploadUrl, keyframe, KEYFRAME_MIME);
|
|
2225
|
+
imageKey = (await client.completeMedia(presigned.key)).key;
|
|
2226
|
+
}
|
|
2227
|
+
deps.onPhase("checking");
|
|
2228
|
+
return client.submitCheck("face_match", { imageKey }, { idempotencyKey });
|
|
2229
|
+
});
|
|
2195
2230
|
}
|
|
2196
2231
|
function classifyFailure(err) {
|
|
2197
2232
|
if (err instanceof AttestationError) return { kind: "unavailable", reason: "attestation" };
|
|
@@ -2427,7 +2462,8 @@ function createVerificationInternal(options, internals) {
|
|
|
2427
2462
|
};
|
|
2428
2463
|
};
|
|
2429
2464
|
let active = null;
|
|
2430
|
-
let
|
|
2465
|
+
let chain = Promise.resolve();
|
|
2466
|
+
let chainDepth = 0;
|
|
2431
2467
|
const settleProcessing = async () => {
|
|
2432
2468
|
let current = await ensureView();
|
|
2433
2469
|
for (let i = 0; i < PROCESSING_POLL_MAX && !destroyed && !isTerminalView(current) && frontierStep(current) === void 0 && current.steps.some((step) => step.state === "processing"); i += 1) {
|
|
@@ -2437,11 +2473,17 @@ function createVerificationInternal(options, internals) {
|
|
|
2437
2473
|
}
|
|
2438
2474
|
return current;
|
|
2439
2475
|
};
|
|
2440
|
-
const runFlow = async (
|
|
2441
|
-
const
|
|
2476
|
+
const runFlow = async (bound, artifacts, capturedAttempt) => {
|
|
2477
|
+
const { capture } = bound;
|
|
2478
|
+
const isCancelled = () => destroyed;
|
|
2479
|
+
const ownsHandle = () => bound.alive && bound.attempt === capturedAttempt;
|
|
2480
|
+
const setPhase = (phase) => {
|
|
2481
|
+
if (ownsHandle()) capture.setFlowState(phase);
|
|
2482
|
+
};
|
|
2442
2483
|
const finish = (result) => {
|
|
2443
2484
|
if (isCancelled()) return;
|
|
2444
2485
|
emitResult(result);
|
|
2486
|
+
if (!ownsHandle()) return;
|
|
2445
2487
|
if (result.status === "retry" && rearm === "auto") {
|
|
2446
2488
|
capture.handle.retry();
|
|
2447
2489
|
return;
|
|
@@ -2453,7 +2495,7 @@ function createVerificationInternal(options, internals) {
|
|
|
2453
2495
|
finish(unavailable(sessionId, "no_recorder"));
|
|
2454
2496
|
return;
|
|
2455
2497
|
}
|
|
2456
|
-
|
|
2498
|
+
setPhase("uploading");
|
|
2457
2499
|
try {
|
|
2458
2500
|
await startRegistry();
|
|
2459
2501
|
if (isCancelled()) return;
|
|
@@ -2475,10 +2517,9 @@ function createVerificationInternal(options, internals) {
|
|
|
2475
2517
|
artifacts,
|
|
2476
2518
|
getAttestation,
|
|
2477
2519
|
getView: () => view ?? current,
|
|
2520
|
+
refreshView,
|
|
2478
2521
|
applySubmit,
|
|
2479
|
-
onPhase:
|
|
2480
|
-
if (!isCancelled()) capture.setFlowState(phase);
|
|
2481
|
-
},
|
|
2522
|
+
onPhase: setPhase,
|
|
2482
2523
|
isCancelled
|
|
2483
2524
|
});
|
|
2484
2525
|
if (isCancelled()) return;
|
|
@@ -2495,18 +2536,43 @@ function createVerificationInternal(options, internals) {
|
|
|
2495
2536
|
finish(unavailable(view?.sessionId ?? "", "network"));
|
|
2496
2537
|
}
|
|
2497
2538
|
};
|
|
2539
|
+
const enqueueFlow = (bound, artifacts) => {
|
|
2540
|
+
const capturedAttempt = bound.attempt;
|
|
2541
|
+
if (chainDepth > 0 && bound.alive) {
|
|
2542
|
+
bound.capture.setFlowState("uploading");
|
|
2543
|
+
}
|
|
2544
|
+
chainDepth += 1;
|
|
2545
|
+
chain = chain.then(() => runFlow(bound, artifacts, capturedAttempt)).finally(() => {
|
|
2546
|
+
chainDepth -= 1;
|
|
2547
|
+
}).catch(() => void 0);
|
|
2548
|
+
};
|
|
2498
2549
|
const liveness = (captureOptions) => {
|
|
2499
|
-
active?.handle.destroy();
|
|
2550
|
+
active?.capture.handle.destroy();
|
|
2551
|
+
if (active !== null) active.alive = false;
|
|
2500
2552
|
const capture = createLivenessCaptureInternal(captureOptions, internals.capture);
|
|
2501
|
-
|
|
2553
|
+
const bound = { capture, alive: true, attempt: 0 };
|
|
2554
|
+
active = bound;
|
|
2502
2555
|
capture.handle.on("state", (state) => {
|
|
2503
|
-
if (state === "starting")
|
|
2556
|
+
if (state === "starting") bound.attempt += 1;
|
|
2504
2557
|
});
|
|
2505
2558
|
capture.handle.on("captured", (artifacts) => {
|
|
2506
|
-
|
|
2507
|
-
void Promise.resolve().then(() => runFlow(capture, artifacts, token));
|
|
2559
|
+
void Promise.resolve().then(() => enqueueFlow(bound, artifacts));
|
|
2508
2560
|
});
|
|
2509
|
-
|
|
2561
|
+
const inner = capture.handle;
|
|
2562
|
+
return {
|
|
2563
|
+
get state() {
|
|
2564
|
+
return inner.state;
|
|
2565
|
+
},
|
|
2566
|
+
on: (event, handler) => inner.on(event, handler),
|
|
2567
|
+
start: () => inner.start(),
|
|
2568
|
+
retry: () => inner.retry(),
|
|
2569
|
+
switchCamera: () => inner.switchCamera(),
|
|
2570
|
+
destroy: () => {
|
|
2571
|
+
bound.alive = false;
|
|
2572
|
+
if (active === bound) active = null;
|
|
2573
|
+
inner.destroy();
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2510
2576
|
};
|
|
2511
2577
|
return {
|
|
2512
2578
|
consent: () => ensureConsent(),
|
|
@@ -2516,8 +2582,11 @@ function createVerificationInternal(options, internals) {
|
|
|
2516
2582
|
destroy: () => {
|
|
2517
2583
|
destroyed = true;
|
|
2518
2584
|
lifetime.abort();
|
|
2519
|
-
active
|
|
2520
|
-
|
|
2585
|
+
if (active !== null) {
|
|
2586
|
+
active.alive = false;
|
|
2587
|
+
active.capture.handle.destroy();
|
|
2588
|
+
active = null;
|
|
2589
|
+
}
|
|
2521
2590
|
emitter.clear();
|
|
2522
2591
|
}
|
|
2523
2592
|
};
|