@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.
- package/CHANGELOG.md +17 -0
- package/LICENSE +20 -0
- package/README.md +92 -2
- package/dist/capture-constraints-BujkZcfb.d.cts +21 -0
- package/dist/capture-constraints-BujkZcfb.d.ts +21 -0
- package/dist/capture-session-v3-BJ8ciaWl.d.ts +457 -0
- package/dist/capture-session-v3-CAoWM8iT.d.cts +457 -0
- package/dist/chunk-BPYVZUJA.js +852 -0
- package/dist/chunk-BPYVZUJA.js.map +1 -0
- package/dist/chunk-LOTAJDRE.js +1094 -0
- package/dist/chunk-LOTAJDRE.js.map +1 -0
- package/dist/chunk-MWNFSNMP.js +52 -0
- package/dist/chunk-MWNFSNMP.js.map +1 -0
- package/dist/chunk-NILNKLK6.js +50 -0
- package/dist/chunk-NILNKLK6.js.map +1 -0
- package/dist/chunk-NWF46H7K.js +359 -0
- package/dist/chunk-NWF46H7K.js.map +1 -0
- package/dist/chunk-WY6U7AIK.js +193 -0
- package/dist/chunk-WY6U7AIK.js.map +1 -0
- package/dist/chunk-YVFF4Q4G.js +327 -0
- package/dist/chunk-YVFF4Q4G.js.map +1 -0
- package/dist/client/index.cjs +357 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +39 -0
- package/dist/client/index.d.ts +39 -0
- package/dist/client/index.js +15 -0
- package/dist/client/index.js.map +1 -0
- package/dist/core/index.cjs +1605 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +87 -0
- package/dist/core/index.d.ts +87 -0
- package/dist/core/index.js +139 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/v3/detect-worker.cjs +392 -0
- package/dist/core/v3/detect-worker.cjs.map +1 -0
- package/dist/core/v3/detect-worker.d.cts +1 -0
- package/dist/core/v3/detect-worker.d.ts +1 -0
- package/dist/core/v3/detect-worker.js +13 -0
- package/dist/core/v3/detect-worker.js.map +1 -0
- package/dist/create-verification-CjLnJ0w6.d.ts +348 -0
- package/dist/create-verification-EnZ13uY4.d.cts +348 -0
- package/dist/detect-worker-CKXHlXx9.d.cts +105 -0
- package/dist/detect-worker-CKXHlXx9.d.ts +105 -0
- package/dist/detector-scanic-4C6IEW2K.js +787 -0
- package/dist/detector-scanic-4C6IEW2K.js.map +1 -0
- package/dist/hosted-client-BpvBqu9f.d.cts +408 -0
- package/dist/hosted-client-BpvBqu9f.d.ts +408 -0
- package/dist/index.cjs +2636 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/index.cjs +4166 -0
- package/dist/internal/index.cjs.map +1 -0
- package/dist/internal/index.d.cts +431 -0
- package/dist/internal/index.d.ts +431 -0
- package/dist/internal/index.js +647 -0
- package/dist/internal/index.js.map +1 -0
- package/dist/popup/index.cjs +76 -0
- package/dist/popup/index.cjs.map +1 -0
- package/dist/popup/index.d.cts +66 -0
- package/dist/popup/index.d.ts +66 -0
- package/dist/popup/index.js +11 -0
- package/dist/popup/index.js.map +1 -0
- package/dist/scanic-ml-stub-S2OZ6HEF.js +10 -0
- package/dist/scanic-ml-stub-S2OZ6HEF.js.map +1 -0
- package/package.json +118 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @sekyro/web
|
|
2
|
+
|
|
3
|
+
## 0.1.0-rc.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ca2cd0e: Verification flow: a capture's submission chain now survives a `<LivenessCapture>` remount, and the flow resumes from the server's frontier after a `409`.
|
|
8
|
+
|
|
9
|
+
- Submission chains are **serialized per verification**: creating a new capture handle via `verification.liveness()` (a React remount, a partner re-rendering the stage) no longer cancels the previous capture's in-flight liveness → face_match chain. The new capture's artifacts queue behind it and are then evaluated against the current hosted view. Only `verification.destroy()` cancels. Post-capture states (`uploading` / `checking` / `done`) and the auto re-arm reach a handle only while it is alive and still on the attempt that produced the artifacts.
|
|
10
|
+
- The flow is **frontier-driven**: when `liveness` is the frontier it uploads the clip and submits liveness, then face_match from the same keyframe; when liveness already settled and `face_match` is the frontier it submits the keyframe only; a frontier the SDK cannot drive finishes with the mapped result. After a RESYNC code (`check_already_finalized`, `check_out_of_order`, `session_not_active`, `check_not_found`) the view is re-fetched and the frontier logic re-entered (bounded, `RESYNC_REENTRY_MAX`) instead of finishing on a stale view — a liveness that passed under an earlier run still gets its face_match.
|
|
11
|
+
- Idempotency / same-key replay semantics are unchanged; `result` is still emitted once per terminal verdict (and once per non-terminal finish of a run).
|
|
12
|
+
|
|
13
|
+
## 0.1.0-rc.1
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 466d3e1: Initial release line of the Sekyro SDKs: `@sekyro/web` (browser liveness capture, hosted-verification client and popup embed), `@sekyro/react` (the themeable, accessible `<LivenessCapture />` component over `@sekyro/web`, ar/fr/en, RTL-safe) and `@sekyro/node` (server-side sessions client and signed-webhook verification, zero runtime dependencies). Pre-releases ship as `0.1.0-rc.N` under the `next` dist-tag from `develop`; the first stable `0.1.0` ships from `main`. All three packages are proprietary (`UNLICENSED` — see each package's LICENSE).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Sekyro SDK — Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ibtikar Technologies. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software (the "SDK") is proprietary to Ibtikar Technologies. Use is
|
|
6
|
+
permitted only under a Sekyro customer agreement with Ibtikar Technologies, and
|
|
7
|
+
only to integrate the customer's own applications with the Sekyro
|
|
8
|
+
identity-verification service. For that purpose you may install the SDK
|
|
9
|
+
from the public npm registry and run it unmodified.
|
|
10
|
+
|
|
11
|
+
No other right or license, express or implied, is granted. You may not
|
|
12
|
+
modify, reverse-engineer, redistribute, sublicense, resell or create
|
|
13
|
+
derivative works of the SDK, nor remove or alter this notice. The source
|
|
14
|
+
code is published for transparency (build provenance, security review); its
|
|
15
|
+
publication grants no additional rights.
|
|
16
|
+
|
|
17
|
+
THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED. TO THE EXTENT PERMITTED BY LAW, IBTIKAR TECHNOLOGIES SHALL NOT BE
|
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE
|
|
20
|
+
SDK, EXCEPT AS PROVIDED IN THE APPLICABLE CUSTOMER AGREEMENT.
|
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sekyro/web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Browser SDK for [Sekyro](https://sekyro.ai) identity verification: liveness capture
|
|
4
|
+
(v3 engine), the typed hosted-verification client, and the partner popup embed.
|
|
5
|
+
Framework-free; pair it with `@sekyro/react` for a ready-made `<LivenessCapture>`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createVerification } from "@sekyro/web";
|
|
9
|
+
|
|
10
|
+
const v = createVerification({ clientToken, locale: "ar" }); // token from your server (@sekyro/node)
|
|
11
|
+
v.on("result", ({ status, reason }) => {
|
|
12
|
+
// approved | rejected | review | retry | unavailable — for display;
|
|
13
|
+
// the verdict that counts is read server-side (sessions.get + webhooks).
|
|
14
|
+
});
|
|
15
|
+
const cap = v.liveness({ video }); // your <video playsinline muted>
|
|
16
|
+
cap.on("gate", ({ progress, hint }) => {/* ring 0..1, hint code */});
|
|
17
|
+
await v.consent();
|
|
18
|
+
void v.startRegistry();
|
|
19
|
+
await cap.start();
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Read the [Web SDK guide](../../docs/partner/web-sdk.md)** for the full integration:
|
|
23
|
+
server-bound NNI, results, webhooks, registered origins, CSP, model assets
|
|
24
|
+
(`assetBaseUrl`), reason codes, billing. Hosted-flow embed only? The
|
|
25
|
+
[quickstart](../../docs/partner/quickstart.md) covers `openHosted`.
|
|
26
|
+
|
|
27
|
+
Requirements: a secure context (HTTPS or `localhost`), self-hosted detector assets
|
|
28
|
+
under `/models-v3` (or `assetBaseUrl`), and your page origin registered with Sekyro.
|
|
29
|
+
|
|
30
|
+
> Status: `0.1.0-rc.0`, still `private` — the public npm release is wired in a later
|
|
31
|
+
> task (Changesets + Trusted Publishing). Sekyro's own hosted page consumes it via a
|
|
32
|
+
> `file:` link today. Formerly `@ibtikar/identity-web-sdk` (internal).
|
|
33
|
+
|
|
34
|
+
## Entry points
|
|
35
|
+
|
|
36
|
+
| Export | Contents | Public API |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `@sekyro/web` | **The partner API** (spec §3.1): `createVerification` (consent → early registry → liveness capture → upload → Turnstile attestation → `liveness` + `face_match` → `result`), `createLivenessCapture` (standalone capture handle), `openHosted` (popup/redirect embed) + the contract types. Surface pinned by `etc/sekyro-web.api.md` (api-extractor) | Yes |
|
|
39
|
+
| `@sekyro/web/core` | Framework-free capture: `CaptureSessionV3`, `createDetector`, `FaceGateV3`, frame quality, device profile, `media`/`keyframe`/`messages`/`ClipRecorder` (calibration-pinned v1 port), camera constraints | Yes |
|
|
40
|
+
| `@sekyro/web/core/v3/detect-worker` | Standalone detection-worker entry — construct with `new Worker(new URL('@sekyro/web/core/v3/detect-worker', import.meta.url))` so its worker-scope auto-install never rides a main-thread bundle | Yes |
|
|
41
|
+
| `@sekyro/web/client` | Typed client for the hosted session API: `HostedClient`, error codes, check input/view types | Yes |
|
|
42
|
+
| `@sekyro/web/popup` | Partner-facing popup/redirect embed (`open`, `SekyroEvent`) | Yes |
|
|
43
|
+
| `@sekyro/web/internal` | **Not public.** Document scanner (doc catalog, `DocGateV1`, MRZ band, frame analysis, feedback aggregator, `CaptureSessionDoc`) + gate-debug geometry constants + the verification internals (`createVerificationInternal` / `createLivenessCaptureInternal` seams, `runLivenessFlow`, `runSubmission`, result mapping, Turnstile loader), consumed by Sekyro's hosted page / `@sekyro/react` only. No semver guarantees; may change in any release | No |
|
|
44
|
+
|
|
45
|
+
Every entry ships ESM + CJS + `.d.ts` (`tsup`). `./core` deliberately does **not**
|
|
46
|
+
re-export the document scanner (that lives only under `./internal`) nor the worker
|
|
47
|
+
entry.
|
|
48
|
+
|
|
49
|
+
## Calibration pin — do not "improve" `src/core/`
|
|
50
|
+
|
|
51
|
+
`src/core/media.ts`, `keyframe.ts`, and `messages.ts` are **byte-identical ports** of
|
|
52
|
+
v1's `kyc-ibtikar/lib/faceCaptureV2/*`. The only allowed delta is the 3-line port
|
|
53
|
+
header block (two comment lines + one blank separator line) prepended to each file.
|
|
54
|
+
`src/core/clip-recorder.ts` is the de-hooked `useClipRecorder` with identical
|
|
55
|
+
constants and behavior (`CLIP_MS=2500`, `TIMESLICE_MS=1000`, `STOP_GRACE_MS=1500`,
|
|
56
|
+
`VIDEO_BPS=2_500_000`, `MAX_CLIP_BYTES=8MiB`, mime ladder, guarded double construction,
|
|
57
|
+
incremental cap enforcement, stop-grace finalize, single-settle, stop-all-tracks).
|
|
58
|
+
|
|
59
|
+
The FAS-CLIP liveness model was calibrated on clips produced by exactly this v1
|
|
60
|
+
pipeline (spec §6 / risk 1). Any drift silently invalidates the calibration.
|
|
61
|
+
`test/port-parity.test.ts` enforces this in two layers: a sha256 checksum pin against
|
|
62
|
+
the committed `test/port-manifest.json` that runs **everywhere, CI included**, and a
|
|
63
|
+
byte-level comparison against the real v1 sources that runs on every dev machine with
|
|
64
|
+
the `kyc-ibtikar/` checkout alongside (that layer self-skips in CI, where the
|
|
65
|
+
gitignored v1 checkout is absent — and it also verifies the manifest itself matches
|
|
66
|
+
header + v1 bytes, so the manifest cannot drift). Do NOT edit `src/core/` without a
|
|
67
|
+
calibration re-validation; if a re-validated change ever lands, regenerate the
|
|
68
|
+
manifest hashes alongside it.
|
|
69
|
+
|
|
70
|
+
The parity test resolves the v1 checkout as `../../kyc-ibtikar/lib/faceCaptureV2`
|
|
71
|
+
from the package root, so this package must stay **two levels below the repo root**
|
|
72
|
+
(`packages/sekyro-web`).
|
|
73
|
+
|
|
74
|
+
Note: v1's `upload.ts` is intentionally NOT ported — uploading is the typed client's
|
|
75
|
+
job in v2 (`HostedClient.uploadAndSubmit`).
|
|
76
|
+
|
|
77
|
+
## Develop
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm ci
|
|
81
|
+
npm run build # tsup → dist/ (ESM + CJS + d.ts per entry)
|
|
82
|
+
npm test # vitest (jsdom)
|
|
83
|
+
npm run typecheck # tsc --noEmit
|
|
84
|
+
npm run api:check # api-extractor: fails when the ROOT public surface drifts from etc/sekyro-web.api.md
|
|
85
|
+
npm run api:report # regenerate etc/sekyro-web.api.md after an intentional, reviewed API change
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The api-extractor report covers the root `.` entry only (the partner contract); the
|
|
89
|
+
subpath entries (`./core`, `./client`, `./popup`) are not reported. `npm run build` must
|
|
90
|
+
precede `api:check` (it reads `dist/index.d.ts`).
|
|
91
|
+
|
|
92
|
+
Standalone package: own lockfile, no workspace wiring. `identity-hosted` consumes it
|
|
93
|
+
via `"@sekyro/web": "file:../packages/sekyro-web"` and needs `dist/` built first.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** getUserMedia constraint tier — 720p-class on every device since 2026-07-06.
|
|
2
|
+
* The v2 FAS-CLIP liveness head loses its texture cues below ~720p: the SAME
|
|
3
|
+
* genuine clip scores heads 0.0001 at 720x1280 vs 0.355 at the old 480x360
|
|
4
|
+
* mobile tier, which pushed real users into review (first observed live on
|
|
5
|
+
* staging 2026-07-06; falsified on corpus clips the same day). Dimensions are
|
|
6
|
+
* sensor-relative (landscape); mobile browsers deliver portrait via rotation
|
|
7
|
+
* metadata, which the liveness service's decoder applies (cv2
|
|
8
|
+
* ORIENTATION_AUTO). `isMobile` is kept for call-site compat and future
|
|
9
|
+
* divergence — both tiers are 720p today. Bitrate pairs with VIDEO_BPS in
|
|
10
|
+
* clip-recorder.ts (2.5 Mbps). */
|
|
11
|
+
declare function buildCaptureConstraints(isMobile: boolean, facingMode: "user" | "environment"): MediaStreamConstraints;
|
|
12
|
+
/** v1 mobile heuristic (FaceCaptureV2.tsx L216-224). */
|
|
13
|
+
declare function detectIsMobile(): boolean;
|
|
14
|
+
/** Gate radius as a fraction of the guidance circle (v1 L501: circleSize × 0.45). */
|
|
15
|
+
declare const CIRCLE_RADIUS_FACTOR = 0.45;
|
|
16
|
+
/** `inCircle` tolerance as a fraction of the gate radius (v1 L508: r × 0.9). */
|
|
17
|
+
declare const IN_CIRCLE_FACTOR = 0.9;
|
|
18
|
+
/** `centered` tolerance as a fraction of the gate radius (v1 L509: r × 0.4). */
|
|
19
|
+
declare const CENTERED_FACTOR = 0.4;
|
|
20
|
+
|
|
21
|
+
export { CENTERED_FACTOR as C, IN_CIRCLE_FACTOR as I, CIRCLE_RADIUS_FACTOR as a, buildCaptureConstraints as b, detectIsMobile as d };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** getUserMedia constraint tier — 720p-class on every device since 2026-07-06.
|
|
2
|
+
* The v2 FAS-CLIP liveness head loses its texture cues below ~720p: the SAME
|
|
3
|
+
* genuine clip scores heads 0.0001 at 720x1280 vs 0.355 at the old 480x360
|
|
4
|
+
* mobile tier, which pushed real users into review (first observed live on
|
|
5
|
+
* staging 2026-07-06; falsified on corpus clips the same day). Dimensions are
|
|
6
|
+
* sensor-relative (landscape); mobile browsers deliver portrait via rotation
|
|
7
|
+
* metadata, which the liveness service's decoder applies (cv2
|
|
8
|
+
* ORIENTATION_AUTO). `isMobile` is kept for call-site compat and future
|
|
9
|
+
* divergence — both tiers are 720p today. Bitrate pairs with VIDEO_BPS in
|
|
10
|
+
* clip-recorder.ts (2.5 Mbps). */
|
|
11
|
+
declare function buildCaptureConstraints(isMobile: boolean, facingMode: "user" | "environment"): MediaStreamConstraints;
|
|
12
|
+
/** v1 mobile heuristic (FaceCaptureV2.tsx L216-224). */
|
|
13
|
+
declare function detectIsMobile(): boolean;
|
|
14
|
+
/** Gate radius as a fraction of the guidance circle (v1 L501: circleSize × 0.45). */
|
|
15
|
+
declare const CIRCLE_RADIUS_FACTOR = 0.45;
|
|
16
|
+
/** `inCircle` tolerance as a fraction of the gate radius (v1 L508: r × 0.9). */
|
|
17
|
+
declare const IN_CIRCLE_FACTOR = 0.9;
|
|
18
|
+
/** `centered` tolerance as a fraction of the gate radius (v1 L509: r × 0.4). */
|
|
19
|
+
declare const CENTERED_FACTOR = 0.4;
|
|
20
|
+
|
|
21
|
+
export { CENTERED_FACTOR as C, IN_CIRCLE_FACTOR as I, CIRCLE_RADIUS_FACTOR as a, buildCaptureConstraints as b, detectIsMobile as d };
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { F as FaceObservation, b as Detector, W as WorkerLike } from './detect-worker-CKXHlXx9.js';
|
|
2
|
+
|
|
3
|
+
/** MP4-first ladder: one container (H.264 MP4) covers iOS/macOS Safari + Chrome 130+.
|
|
4
|
+
* WebM/VP9→VP8 only as the Firefox/older-Chromium fallback. */
|
|
5
|
+
declare const MIME_CANDIDATES: readonly ["video/mp4;codecs=avc1.42E01E,mp4a.40.2", "video/mp4;codecs=avc1.42E01E", "video/mp4", "video/webm;codecs=vp9,opus", "video/webm;codecs=vp9", "video/webm;codecs=vp8,opus", "video/webm;codecs=vp8", "video/webm"];
|
|
6
|
+
type CameraErrorKind = "permission" | "notfound" | "inuse" | "overconstrained" | "insecure" | "unsupported" | "generic";
|
|
7
|
+
/** First supported candidate, or "" to let the browser pick its own default. */
|
|
8
|
+
declare function pickMimeType(): string;
|
|
9
|
+
/** Default clip container when pickMimeType() === "" — NEVER .bin/octet-stream (REVIEW FIX 5).
|
|
10
|
+
* Prefer the first webm in the ladder the browser most likely defaults to. */
|
|
11
|
+
declare const DEFAULT_CLIP_MIME = "video/webm";
|
|
12
|
+
/** Branch on err.name (never the message). TypeError ⇒ navigator.mediaDevices was undefined
|
|
13
|
+
* (insecure/unsupported context) — surfaced as "insecure". */
|
|
14
|
+
declare function mapGetUserMediaError(err: unknown): CameraErrorKind;
|
|
15
|
+
declare function cameraPreflight(): "INSECURE_CONTEXT" | "UNSUPPORTED" | "OK";
|
|
16
|
+
/** iOS Safari (and any browser) may lack MediaRecorder entirely — detect BEFORE prompting
|
|
17
|
+
* for the camera so we can fall back to the v1 single-photo path (REVIEW FIX 1). */
|
|
18
|
+
declare function isMediaRecorderSupported(): boolean;
|
|
19
|
+
declare const MAX_CLIP_BYTES: number;
|
|
20
|
+
declare function isClipTooLarge(blob: Blob): boolean;
|
|
21
|
+
declare function extForMime(mime: string): string;
|
|
22
|
+
/** Build the exact multipart body /api/liveness/v2 expects: fields `token`, files `clip` + `keyframe`.
|
|
23
|
+
* Caller must NOT set Content-Type — the browser adds the boundary. */
|
|
24
|
+
declare function buildLivenessV2FormData(args: {
|
|
25
|
+
token: string;
|
|
26
|
+
clip: Blob;
|
|
27
|
+
keyframe: Blob;
|
|
28
|
+
ts?: number;
|
|
29
|
+
}): FormData;
|
|
30
|
+
|
|
31
|
+
declare const CLIP_MS = 2500;
|
|
32
|
+
declare const TIMESLICE_MS = 1000;
|
|
33
|
+
declare const STOP_GRACE_MS = 1500;
|
|
34
|
+
declare const VIDEO_BPS = 2500000;
|
|
35
|
+
type ClipErrorKind = CameraErrorKind | "cliptoolarge";
|
|
36
|
+
declare class ClipError extends Error {
|
|
37
|
+
kind: ClipErrorKind;
|
|
38
|
+
constructor(kind: ClipErrorKind, msg?: string);
|
|
39
|
+
}
|
|
40
|
+
interface ClipResult {
|
|
41
|
+
blob: Blob;
|
|
42
|
+
/** Container mime the clip was finalized with (recorder.mimeType || picked || DEFAULT_CLIP_MIME). */
|
|
43
|
+
mimeType: string;
|
|
44
|
+
}
|
|
45
|
+
declare class ClipRecorder {
|
|
46
|
+
private recorder;
|
|
47
|
+
private stream;
|
|
48
|
+
private chunks;
|
|
49
|
+
private bytes;
|
|
50
|
+
private hardStopTimer;
|
|
51
|
+
private graceTimer;
|
|
52
|
+
private settled;
|
|
53
|
+
private clearTimers;
|
|
54
|
+
/** Records CLIP_MS of the stream. Resolves with the clip blob + its container mime;
|
|
55
|
+
* rejects with ClipError. Behavior contract is the v1 hook's `start`, verbatim
|
|
56
|
+
* (iOS workarounds included).
|
|
57
|
+
*
|
|
58
|
+
* ONE RECORDING AT A TIME: chunk/byte/timer state is shared per instance, so a
|
|
59
|
+
* second record() while one is in flight rejects ClipError("generic") instead of
|
|
60
|
+
* corrupting the in-flight clip. (v2 guard — v1's hook was gated by its owning
|
|
61
|
+
* component; a class API invites reuse. Zero impact on clip bytes.)
|
|
62
|
+
*
|
|
63
|
+
* `onStopped` (optional) fires ONCE the moment the CLIP_MS recording window
|
|
64
|
+
* elapses (the hard-stop), BEFORE finalize — which on iOS can lag up to
|
|
65
|
+
* STOP_GRACE_MS while onstop flushes the last chunk. It lets the host freeze
|
|
66
|
+
* the display on the already-captured keyframe the instant recording ends,
|
|
67
|
+
* instead of waiting for the blob. PURE NOTIFICATION — it does not touch
|
|
68
|
+
* timers, chunks, or the finalize path, so it is calibration-neutral (no
|
|
69
|
+
* effect on clip bytes). Never fires on a rejected capture (error /
|
|
70
|
+
* cliptoolarge settle before the hard-stop and clear the timer). */
|
|
71
|
+
record(stream: MediaStream, onStopped?: () => void): Promise<ClipResult>;
|
|
72
|
+
/** Idempotent teardown: stop recorder + ALL tracks (v1 `stopAndCleanup` + the unmount
|
|
73
|
+
* effect). Optional `stream` override mirrors v1's `stopAndCleanup(stream?)`.
|
|
74
|
+
*
|
|
75
|
+
* MID-RECORDING SEMANTICS (verbatim v1): calling stop() while a record() is in
|
|
76
|
+
* flight may LATER resolve the pending promise with a TRUNCATED clip (recorder.stop()
|
|
77
|
+
* fires onstop → finalize from whatever chunks arrived so far), or — because this
|
|
78
|
+
* method clears the hard-stop/grace timers — never settle it at all if onstop never
|
|
79
|
+
* fires (the iOS grace path has been torn down). Callers MUST NOT treat a resolution
|
|
80
|
+
* arriving after stop() as a full CLIP_MS clip, and must not await a record()
|
|
81
|
+
* promise past stop() without their own timeout. (Task 4's CaptureSession owns
|
|
82
|
+
* sequencing on top of this contract.) */
|
|
83
|
+
stop(stream?: MediaStream | null): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** CaptureSession state machine (transition map in capture-session.ts):
|
|
87
|
+
* idle → preflight → acquiring → positioning → recording → captured
|
|
88
|
+
* (photo mode skips `recording`: positioning → captured); any → error. */
|
|
89
|
+
type CaptureState = "idle" | "preflight" | "acquiring" | "positioning" | "recording" | "captured" | "error";
|
|
90
|
+
/** Camera kinds (ported media.ts) + engine kinds:
|
|
91
|
+
* - 'models' detector init failed (the v3 native→blaze ladder was
|
|
92
|
+
* exhausted — e.g. BlazeFace wasm never initialized). Fail
|
|
93
|
+
* loud; there is no degraded mode (a mock capture-anyway mode
|
|
94
|
+
* is a liveness hole, and the face-api floor was dropped).
|
|
95
|
+
* - 'cliptoolarge' the 8 MB clip cap (shared with the liveness API contract)
|
|
96
|
+
* - 'recorder' MediaRecorder failed mid-clip. ClipRecorder's v1-verbatim kind for
|
|
97
|
+
* this is 'generic'; CaptureSession REMAPS it to 'recorder' so hosts
|
|
98
|
+
* can distinguish recorder failure from camera 'generic'. */
|
|
99
|
+
type CaptureErrorKind = CameraErrorKind | "models" | "cliptoolarge" | "recorder";
|
|
100
|
+
/** The five v1 gate checks (hysteresis-smoothed statuses). */
|
|
101
|
+
interface GateChecks {
|
|
102
|
+
faceDetected: boolean;
|
|
103
|
+
inCircle: boolean;
|
|
104
|
+
centered: boolean;
|
|
105
|
+
goodQuality: boolean;
|
|
106
|
+
stable: boolean;
|
|
107
|
+
}
|
|
108
|
+
/** Capture output. Upload is NOT the engine's job — hosts submit these themselves. */
|
|
109
|
+
interface CaptureArtifacts {
|
|
110
|
+
/** Absent in photo mode. */
|
|
111
|
+
clip?: Blob;
|
|
112
|
+
/** Container mime the clip finalized with. Absent in photo mode. */
|
|
113
|
+
clipMimeType?: string;
|
|
114
|
+
/** Mirrored JPEG keyframe (captureMirroredKeyframe). */
|
|
115
|
+
keyframe: Blob;
|
|
116
|
+
/** JPEG data URL of the keyframe canvas (v1 preview, quality 0.92). */
|
|
117
|
+
previewDataUrl: string;
|
|
118
|
+
}
|
|
119
|
+
interface CaptureSessionEvents {
|
|
120
|
+
statechange: {
|
|
121
|
+
state: CaptureState;
|
|
122
|
+
};
|
|
123
|
+
checks: GateChecks & {
|
|
124
|
+
counters: Record<keyof GateChecks, number>;
|
|
125
|
+
};
|
|
126
|
+
/** fcv2.* message KEY (messages.ts / guidanceCopy) — hosts translate; never prose. */
|
|
127
|
+
guidance: {
|
|
128
|
+
key: string;
|
|
129
|
+
};
|
|
130
|
+
artifacts: CaptureArtifacts;
|
|
131
|
+
error: {
|
|
132
|
+
kind: CaptureErrorKind;
|
|
133
|
+
retryable: boolean;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface V3GateThresholds {
|
|
138
|
+
/** q at/above which progress rises; below it progress falls. */
|
|
139
|
+
Q_GOOD: number;
|
|
140
|
+
/** Integrator slopes, per second. */
|
|
141
|
+
RISE_PER_S: number;
|
|
142
|
+
FALL_PER_S: number;
|
|
143
|
+
/** Centering falloff radius = this × IOD (scale-invariant). The plan's
|
|
144
|
+
* formula is 0.45 × IOD × 3 — the 0.45 echoes v1's CIRCLE_RADIUS_FACTOR
|
|
145
|
+
* heritage — folded here to 1.35 × IOD. */
|
|
146
|
+
CENTER_FALLOFF_IOD_MULTIPLE: number;
|
|
147
|
+
/** Box-only tiers: gate point = bbox center shifted UP by this × height
|
|
148
|
+
* (the documented bbox-vs-eyes offset heuristic). */
|
|
149
|
+
BOX_EYE_OFFSET_FACTOR: number;
|
|
150
|
+
/** Box-only IOD proxy = this × box width. */
|
|
151
|
+
IOD_BOX_PROXY_FACTOR: number;
|
|
152
|
+
/** IOD / frame short side: ideal band + hard limits. */
|
|
153
|
+
SIZE_IDEAL_MIN: number;
|
|
154
|
+
SIZE_IDEAL_MAX: number;
|
|
155
|
+
SIZE_HARD_MIN: number;
|
|
156
|
+
SIZE_HARD_MAX: number;
|
|
157
|
+
/** Eye-line roll (deg) that zeroes the pose factor. */
|
|
158
|
+
ROLL_MAX_DEG: number;
|
|
159
|
+
/** Center velocity that zeroes the motion factor, in IOD-per-second. */
|
|
160
|
+
MOTION_VELOCITY_IOD_PER_S: number;
|
|
161
|
+
/** Velocity-EMA time constant (seconds) — the EMA weight is derived per
|
|
162
|
+
* tick as 1−exp(−dt/τ), so the smoothing HORIZON is cadence-invariant:
|
|
163
|
+
* a LOW device detecting at 5 Hz gets the same effective smoothing as a
|
|
164
|
+
* HIGH device at 10 Hz (a fixed per-sample alpha under-smoothed exactly
|
|
165
|
+
* the jittery low-cadence ticks that needed it most). */
|
|
166
|
+
MOTION_TAU_S: number;
|
|
167
|
+
/** Velocities at/below this (IOD/s) count as stillness — absorbs detector
|
|
168
|
+
* box jitter (largest on LOW's 128px input) before it reaches the EMA. */
|
|
169
|
+
MOTION_DEADBAND_IOD_PER_S: number;
|
|
170
|
+
/** ready = p ≥ 1 AND mean q over this window ≥ READY_MEAN_Q. The mean is
|
|
171
|
+
* TRIMMED: with ≥3 samples in the window the single worst is dropped, so
|
|
172
|
+
* one flaky tick (5 Hz LOW cadence = only 2–3 samples per window) cannot
|
|
173
|
+
* veto an otherwise-ready user. */
|
|
174
|
+
READY_WINDOW_MS: number;
|
|
175
|
+
READY_MEAN_Q: number;
|
|
176
|
+
/** Per-factor guidance trigger levels (lowest failing factor speaks). */
|
|
177
|
+
GUIDE_SIZE: number;
|
|
178
|
+
GUIDE_CENTERING: number;
|
|
179
|
+
GUIDE_POSE: number;
|
|
180
|
+
GUIDE_EXPOSURE: number;
|
|
181
|
+
GUIDE_SHARPNESS: number;
|
|
182
|
+
GUIDE_MOTION: number;
|
|
183
|
+
}
|
|
184
|
+
/** Defaults — retuned 2026-07-12 (operator ruling superseding the 2026-07
|
|
185
|
+
* freeze; the capture Task-15 refit set must be split at this boundary):
|
|
186
|
+
* Q_GOOD 0.55→0.6 so it aligns with the 0.6 guidance thresholds under
|
|
187
|
+
* min-q ("no complaint" ⇔ "progress rises"); SIZE_IDEAL_MIN 0.16→0.12 —
|
|
188
|
+
* wide-FOV front cameras (common low-end) put an arm's-length face below
|
|
189
|
+
* 0.16, silently forcing lean-in; the 720p clip floor keeps a 0.12-IOD
|
|
190
|
+
* face fully resolvable server-side. */
|
|
191
|
+
declare const V3_GATE_DEFAULTS: Readonly<V3GateThresholds>;
|
|
192
|
+
interface V3GateQuality {
|
|
193
|
+
/** From frame-quality.sharpness on the face crop. */
|
|
194
|
+
sharpness: number;
|
|
195
|
+
/** From frame-quality.exposure on the face crop. */
|
|
196
|
+
exposure: number;
|
|
197
|
+
/** Mean crop luma — picks more_light vs too_bright guidance. */
|
|
198
|
+
meanLuma: number;
|
|
199
|
+
}
|
|
200
|
+
interface V3GateTickInput {
|
|
201
|
+
timestampMs: number;
|
|
202
|
+
observation: FaceObservation | null;
|
|
203
|
+
quality: V3GateQuality;
|
|
204
|
+
frameWidth: number;
|
|
205
|
+
frameHeight: number;
|
|
206
|
+
}
|
|
207
|
+
type V3GateFactor = "presence" | "centering" | "size" | "pose" | "sharpness" | "exposure" | "motion";
|
|
208
|
+
/** Per-tick output — the telemetry + chrome contract. */
|
|
209
|
+
interface V3GateTick {
|
|
210
|
+
at: number;
|
|
211
|
+
/** Instantaneous quality 0..1 (MIN over the factors — the worst factor). */
|
|
212
|
+
q: number;
|
|
213
|
+
/** Integrated progress 0..1. */
|
|
214
|
+
p: number;
|
|
215
|
+
factors: Record<V3GateFactor, number>;
|
|
216
|
+
/** fcv3.* message key for the lowest failing factor (host translates). */
|
|
217
|
+
guidanceKey: string;
|
|
218
|
+
/** Fire signal: p ≥ 1 AND the READY_WINDOW mean q ≥ READY_MEAN_Q. */
|
|
219
|
+
ready: boolean;
|
|
220
|
+
}
|
|
221
|
+
declare class FaceGateV3 {
|
|
222
|
+
private readonly t;
|
|
223
|
+
private p;
|
|
224
|
+
private lastTickAt;
|
|
225
|
+
private lastCenter;
|
|
226
|
+
private velocityEma;
|
|
227
|
+
private qWindow;
|
|
228
|
+
constructor(overrides?: Partial<V3GateThresholds>);
|
|
229
|
+
reset(): void;
|
|
230
|
+
tick(input: V3GateTickInput): V3GateTick;
|
|
231
|
+
private geometryOf;
|
|
232
|
+
private motionOf;
|
|
233
|
+
/** Lowest failing factor speaks — fixed priority (diagnostic, incl. light):
|
|
234
|
+
* presence → size → centering → pose → exposure → sharpness → hold.
|
|
235
|
+
*
|
|
236
|
+
* HONESTY FALLBACK (operator ruling 2026-07-12): when every factor clears
|
|
237
|
+
* its guide threshold but q (= min factor) is still below Q_GOOD — i.e.
|
|
238
|
+
* progress is DRAINING — "hold still" would be a lie ("Perfect" while
|
|
239
|
+
* nothing advances; the field symptom was users discovering by accident
|
|
240
|
+
* that only leaning in fires the gate). In that state the WORST factor
|
|
241
|
+
* speaks even though it passed its guide level, so the user always hears
|
|
242
|
+
* the change that actually unblocks recording. */
|
|
243
|
+
private guidanceOf;
|
|
244
|
+
/** Direction from the size band set THIS tick (never inverted at the hard
|
|
245
|
+
* limits: past SIZE_HARD_MAX the direction is 'close' → move_back). */
|
|
246
|
+
private sizeGuidance;
|
|
247
|
+
/** Band midpoint — direction only (tracks EXPOSURE_BAND automatically). */
|
|
248
|
+
private exposureGuidance;
|
|
249
|
+
/** Which side of the ideal band the last size measurement fell on. */
|
|
250
|
+
private lastSizeDirection;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type DeviceClass = "LOW" | "MID" | "HIGH";
|
|
254
|
+
interface DeviceProfile {
|
|
255
|
+
/** Detection cadence — the scheduler's pace. */
|
|
256
|
+
cadenceHz: number;
|
|
257
|
+
/** Downscaled snapshot width handed to the detector. */
|
|
258
|
+
inputWidth: number;
|
|
259
|
+
/** Chrome hint: 'reduced' tones down ticks/breathe on LOW even without
|
|
260
|
+
* prefers-reduced-motion. */
|
|
261
|
+
animations: "full" | "reduced";
|
|
262
|
+
}
|
|
263
|
+
declare const DEVICE_PROFILES: Readonly<Record<DeviceClass, Readonly<DeviceProfile>>>;
|
|
264
|
+
interface DeviceProbe {
|
|
265
|
+
cores: number;
|
|
266
|
+
/** null where the UA hides it — iOS Safari NEVER exposes deviceMemory
|
|
267
|
+
* (field lesson: a 2GB default classified every iPhone LOW). Unknown is
|
|
268
|
+
* treated as NEUTRAL (4GB): never demotes to LOW on its own, never
|
|
269
|
+
* promotes to HIGH; the dynamic profiler corrects truly slow devices. */
|
|
270
|
+
memGB: number | null;
|
|
271
|
+
/** WebAssembly SIMD available (informs wasm-tier expectations/telemetry). */
|
|
272
|
+
simd: boolean;
|
|
273
|
+
}
|
|
274
|
+
declare function probeDevice(): DeviceProbe;
|
|
275
|
+
/** Static class: LOW (<4 cores or <3GB), HIGH (≥8 cores AND ≥6GB), else MID.
|
|
276
|
+
* Unknown memory counts as a neutral 4GB (see DeviceProbe.memGB). */
|
|
277
|
+
declare function classifyDevice(probe: DeviceProbe): DeviceClass;
|
|
278
|
+
/** Dynamic refinement: after the FIRST 10 measured detect ticks, a mean over
|
|
279
|
+
* 70ms demotes straight to LOW; under 25ms promotes ONE class — at most one
|
|
280
|
+
* adjustment ever (no oscillation). */
|
|
281
|
+
declare class DeviceProfiler {
|
|
282
|
+
private current;
|
|
283
|
+
private readonly samples;
|
|
284
|
+
private adjusted;
|
|
285
|
+
constructor(initial: DeviceClass);
|
|
286
|
+
get deviceClass(): DeviceClass;
|
|
287
|
+
get profile(): DeviceProfile;
|
|
288
|
+
record(detectMs: number): DeviceClass;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Best-of-N keyframe window: up to 4 candidates over the first 600ms. */
|
|
292
|
+
declare const KEYFRAME_CANDIDATES = 4;
|
|
293
|
+
declare const KEYFRAME_WINDOW_MS = 600;
|
|
294
|
+
interface V3ChecksPayload {
|
|
295
|
+
engine: "v3";
|
|
296
|
+
/** Gate integrator 0..1 — the ring drives off this directly. */
|
|
297
|
+
progress: number;
|
|
298
|
+
/** Instantaneous quality 0..1. */
|
|
299
|
+
q: number;
|
|
300
|
+
factors: Record<V3GateFactor, number>;
|
|
301
|
+
/** Attempt ordinal (bumps on retry/switchCamera). */
|
|
302
|
+
attempt: number;
|
|
303
|
+
}
|
|
304
|
+
interface CaptureSessionV3Events {
|
|
305
|
+
statechange: {
|
|
306
|
+
state: CaptureState;
|
|
307
|
+
};
|
|
308
|
+
checks: V3ChecksPayload;
|
|
309
|
+
/** fcv3.* message KEY — hosts translate; never prose. Deduped. */
|
|
310
|
+
guidance: {
|
|
311
|
+
key: string;
|
|
312
|
+
};
|
|
313
|
+
artifacts: CaptureArtifacts;
|
|
314
|
+
error: {
|
|
315
|
+
kind: CaptureErrorKind;
|
|
316
|
+
retryable: boolean;
|
|
317
|
+
};
|
|
318
|
+
/** v3-only, CHROME-only (no data flows here): fires the instant the clip
|
|
319
|
+
* recording window ends — BEFORE the blob finalizes (iOS onstop can lag up
|
|
320
|
+
* to STOP_GRACE_MS). Carries the already-captured keyframe's JPEG data URL so
|
|
321
|
+
* the host can freeze the display on the still immediately instead of leaving
|
|
322
|
+
* the live feed running while the clip flushes. Clip mode only (photo mode
|
|
323
|
+
* goes straight to `artifacts`). A host that ignores it is unaffected. */
|
|
324
|
+
frozen: {
|
|
325
|
+
previewDataUrl: string;
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
type V3TelemetryEvent = {
|
|
329
|
+
type: "detector";
|
|
330
|
+
tier: Detector["tier"];
|
|
331
|
+
fallbackReason?: string;
|
|
332
|
+
} | {
|
|
333
|
+
type: "profile";
|
|
334
|
+
staticClass: DeviceClass;
|
|
335
|
+
activeClass: DeviceClass;
|
|
336
|
+
simd: boolean;
|
|
337
|
+
} | {
|
|
338
|
+
type: "fired";
|
|
339
|
+
timeToGateMs: number;
|
|
340
|
+
degradeCount: number;
|
|
341
|
+
meanDetectMs: number;
|
|
342
|
+
} | {
|
|
343
|
+
type: "error";
|
|
344
|
+
kind: CaptureErrorKind;
|
|
345
|
+
fallbackReason?: string;
|
|
346
|
+
};
|
|
347
|
+
interface KeyframeCandidate {
|
|
348
|
+
blob: Blob;
|
|
349
|
+
previewDataUrl: string;
|
|
350
|
+
sharpness: number;
|
|
351
|
+
}
|
|
352
|
+
/** Challenge-clip artifact for the Phase 3 escalation path. */
|
|
353
|
+
interface EscalationClipArtifact {
|
|
354
|
+
clip: Blob;
|
|
355
|
+
clipMimeType: string;
|
|
356
|
+
}
|
|
357
|
+
/** INERT Phase 3 seam (docs/superpowers/specs/2026-07-07-active-liveness-
|
|
358
|
+
* escalation-spec.md): when the server settles a liveness verdict in the
|
|
359
|
+
* REVIEW band it may mint ONE gesture challenge; the host then asks the
|
|
360
|
+
* engine for a challenge clip through this hook. Typed and documented NOW so
|
|
361
|
+
* capture-v3+ starts from a design — NEVER invoked anywhere in the current
|
|
362
|
+
* flow (pinned by a test). */
|
|
363
|
+
interface EscalationHook {
|
|
364
|
+
onBorderline(challenge: "blink" | "turn"): Promise<EscalationClipArtifact>;
|
|
365
|
+
}
|
|
366
|
+
interface CaptureSessionV3Options {
|
|
367
|
+
/** Host-owned <video playsInline muted> — same iOS contract as v1. */
|
|
368
|
+
video: HTMLVideoElement;
|
|
369
|
+
/** v3 assets base (tasks-vision wasm + tflite); default /models-v3. */
|
|
370
|
+
v3AssetBaseUrl?: string;
|
|
371
|
+
mode?: "clip" | "photo";
|
|
372
|
+
/** FaceGateV3 threshold overrides (Phase 2 tuning). */
|
|
373
|
+
thresholds?: Partial<V3GateThresholds>;
|
|
374
|
+
telemetry?: (event: V3TelemetryEvent) => void;
|
|
375
|
+
facingMode?: "user" | "environment";
|
|
376
|
+
/** Static class override (tests / host pinning); default = boot probe. */
|
|
377
|
+
deviceClass?: DeviceClass;
|
|
378
|
+
/** (detector ladder) */
|
|
379
|
+
createDetector?: () => Promise<Detector>;
|
|
380
|
+
/** (fresh ClipRecorder per attempt) */
|
|
381
|
+
createRecorder?: () => Pick<ClipRecorder, "record" | "stop">;
|
|
382
|
+
/** (navigator.mediaDevices.getUserMedia) */
|
|
383
|
+
getUserMedia?: (constraints: MediaStreamConstraints) => Promise<MediaStream>;
|
|
384
|
+
/** (mirrored canvas keyframe + downscaled Laplacian sharpness) */
|
|
385
|
+
captureKeyframe?: (video: HTMLVideoElement) => Promise<KeyframeCandidate>;
|
|
386
|
+
/** (downscaled face-crop sharpness/exposure via canvas) */
|
|
387
|
+
measureQuality?: (video: HTMLVideoElement, box: {
|
|
388
|
+
x: number;
|
|
389
|
+
y: number;
|
|
390
|
+
width: number;
|
|
391
|
+
height: number;
|
|
392
|
+
}) => V3GateQuality;
|
|
393
|
+
/** (reused canvas at profile.inputWidth; falls back to the raw video where
|
|
394
|
+
* canvas is unavailable) */
|
|
395
|
+
makeSnapshot?: (video: HTMLVideoElement, targetWidth: number) => CanvasImageSource;
|
|
396
|
+
/** Worker offload opt-in (Task 9): when provided AND the platform supports
|
|
397
|
+
* it (OffscreenCanvas + createImageBitmap + Worker), detection runs
|
|
398
|
+
* worker-side through WorkerDetectorProxy. Hosts own Worker construction
|
|
399
|
+
* (bundler-specific `new Worker(new URL(...))`). */
|
|
400
|
+
createWorker?: () => WorkerLike;
|
|
401
|
+
/** INERT (Phase 3 escalation seam — see EscalationHook). Accepted and
|
|
402
|
+
* retained, never called in Phases 1–2. */
|
|
403
|
+
escalation?: EscalationHook;
|
|
404
|
+
/** (performance.now) */
|
|
405
|
+
now?: () => number;
|
|
406
|
+
}
|
|
407
|
+
type Handler<K extends keyof CaptureSessionV3Events> = (payload: CaptureSessionV3Events[K]) => void;
|
|
408
|
+
declare class CaptureSessionV3 {
|
|
409
|
+
private readonly options;
|
|
410
|
+
private readonly handlers;
|
|
411
|
+
private stateValue;
|
|
412
|
+
private attempt;
|
|
413
|
+
private facing;
|
|
414
|
+
private stream;
|
|
415
|
+
private detector;
|
|
416
|
+
private scheduler;
|
|
417
|
+
private gate;
|
|
418
|
+
private profiler;
|
|
419
|
+
private lastGuidanceKey;
|
|
420
|
+
private positioningStartedAt;
|
|
421
|
+
private fired;
|
|
422
|
+
private carriedDegrade;
|
|
423
|
+
private carriedMeans;
|
|
424
|
+
constructor(options: CaptureSessionV3Options);
|
|
425
|
+
get state(): CaptureState;
|
|
426
|
+
on<K extends keyof CaptureSessionV3Events>(event: K, handler: Handler<K>): () => void;
|
|
427
|
+
private emit;
|
|
428
|
+
start(): Promise<void>;
|
|
429
|
+
/** Full reset + camera restart — the documented recovery path (v1 parity). */
|
|
430
|
+
retry(): void;
|
|
431
|
+
switchCamera(): void;
|
|
432
|
+
destroy(): void;
|
|
433
|
+
private run;
|
|
434
|
+
/** Chrome hint (Task 11): 'reduced' on LOW even without the media query. */
|
|
435
|
+
get animations(): "full" | "reduced";
|
|
436
|
+
private startScheduler;
|
|
437
|
+
private detectTick;
|
|
438
|
+
private fire;
|
|
439
|
+
/** Up to KEYFRAME_CANDIDATES mirrored frames over KEYFRAME_WINDOW_MS —
|
|
440
|
+
* sharpest wins (reuses frame-quality.sharpness via the capture seam). */
|
|
441
|
+
private collectKeyframes;
|
|
442
|
+
private finishCapture;
|
|
443
|
+
private defaultKeyframe;
|
|
444
|
+
/** Reused downscale canvas — falls back to the raw video where canvas 2D
|
|
445
|
+
* is unavailable (jsdom) so tests and exotic hosts keep detecting. */
|
|
446
|
+
private snapshotCanvas;
|
|
447
|
+
private defaultSnapshot;
|
|
448
|
+
private defaultQuality;
|
|
449
|
+
private centerSharpness;
|
|
450
|
+
private teardownAttempt;
|
|
451
|
+
private stopTracks;
|
|
452
|
+
private fail;
|
|
453
|
+
private setState;
|
|
454
|
+
private now;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export { V3_GATE_DEFAULTS as A, VIDEO_BPS as B, type CameraErrorKind as C, DEFAULT_CLIP_MIME as D, type EscalationClipArtifact as E, FaceGateV3 as F, type GateChecks as G, buildLivenessV2FormData as H, cameraPreflight as I, classifyDevice as J, KEYFRAME_CANDIDATES as K, extForMime as L, MAX_CLIP_BYTES as M, isClipTooLarge as N, isMediaRecorderSupported as O, mapGetUserMediaError as P, pickMimeType as Q, probeDevice as R, STOP_GRACE_MS as S, TIMESLICE_MS as T, type V3ChecksPayload as V, CLIP_MS as a, type CaptureArtifacts as b, type CaptureErrorKind as c, type CaptureSessionEvents as d, CaptureSessionV3 as e, type CaptureSessionV3Events as f, type CaptureSessionV3Options as g, type CaptureState as h, ClipError as i, type ClipErrorKind as j, ClipRecorder as k, type ClipResult as l, DEVICE_PROFILES as m, type DeviceClass as n, type DeviceProbe as o, type DeviceProfile as p, DeviceProfiler as q, type EscalationHook as r, KEYFRAME_WINDOW_MS as s, MIME_CANDIDATES as t, type V3GateFactor as u, type V3GateQuality as v, type V3GateThresholds as w, type V3GateTick as x, type V3GateTickInput as y, type V3TelemetryEvent as z };
|