@sekyro/web
Framework-free browser SDK: capture engine, hosted client and popup embed.
Browser SDK for Sekyro AI identity verification: liveness capture
(v3 engine), the typed hosted-verification client, and the partner popup embed.
Framework-free; pair it with @sekyro/react for a ready-made <LivenessCapture>.
import { createVerification } from "@sekyro/web";
const v = createVerification({ clientToken, locale: "ar" }); // token from your server (@sekyro/node)
v.on("result", ({ status, reason }) => {
// approved | rejected | review | retry | unavailable — for display;
// the verdict that counts is read server-side (sessions.get + webhooks).
});
const cap = v.liveness({ video }); // your <video playsinline muted>
cap.on("gate", ({ progress, hint }) => {/* ring 0..1, hint code */});
await v.consent();
void v.startRegistry();
await cap.start();Read the Web SDK guide for the full integration:
server-bound NNI, results, webhooks, registered origins, CSP, model assets
(assetBaseUrl), reason codes, billing. Hosted-flow embed only? The
quickstart covers openHosted.
Requirements: a secure context (HTTPS or localhost), self-hosted detector assets
under /models-v3 (or assetBaseUrl), and your page origin registered with Sekyro AI.
Status:
0.1.0-rc.0, stillprivate— the public npm release is wired in a later task (Changesets + Trusted Publishing). Sekyro AI's own hosted page consumes it via afile:link today. Formerly@ibtikar/identity-web-sdk(internal).
Preload & camera probes
Warm the face detector before the user taps Start (on the step before the capture, or at mount), and read the camera permission for your "get ready" UI:
import { createLivenessCapture, getLivenessAssetsStatus, hasMultipleCameras,
preloadLivenessAssets, subscribeCameraPermission, subscribeLivenessAssets } from "@sekyro/web";
void preloadLivenessAssets({ assetBaseUrl: "/models-v3", idle: true }); // never rejects
const gateStart = (s: string) => (startBtn.disabled = s === "loading");
gateStart(getLivenessAssetsStatus("/models-v3")); // subscribe does not emit the current status
subscribeLivenessAssets(gateStart, "/models-v3");
subscribeCameraPermission((p) => showCameraRow(p)); // granted|prompt|denied|unknown
const showFacingChoice = await hasMultipleCameras();
const cap = createLivenessCapture({ video, assetBaseUrl: "/models-v3" });
await cap.start({ facing: "user" }); // reuses the warm detector automaticallypreloadLivenessAssetsis idempotent perassetBaseUrland resolves the settled status:'ready','failed'(call it again to retry), or'idle'— nothing was preloaded (SSR, no camera API, Data-Saver withrespectSaveData); just start normally.idle: truedefers the load to browser idle time; a capture that starts meanwhile begins it at once.- The preload warms the blaze (MediaPipe) tier with a generous deadline. Every
capture created afterwards with the same
assetBaseUrluses it without any extra wiring, and destroying a capture never frees it; callreleaseLivenessAssets(assetBaseUrl)when the user leaves the flow (never while a capture is mounted). Without a preload the capture loads the detector itself, as before. getLivenessAssetsStatus/subscribeLivenessAssetsfollow theuseSyncExternalStorecontract (no emit on subscribe).subscribeCameraPermissionemits the current state first, then every change;queryCameraPermissionis the one-shot read. Both report'unknown'where the Permissions API cannot tell.start({ facing })picks the camera for this attempt and its retries;switchCamera()still flips it mid-capture.keepScreenAwake(capture option, defaulttrue) holds a Screen Wake Lock while the camera is live (starting/gating/recording), released oncaptured,erroranddestroy(), re-taken onretry()and when the page is visible again. Best-effort — unsupported or refused requests never affect the capture. In an iframe, allow it:<iframe allow="camera; screen-wake-lock">. Passfalseto never request it.
Entry points
| Export | Contents | Public API |
|---|---|---|
@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 |
@sekyro/web/core | Framework-free capture: CaptureSessionV3, createDetector, FaceGateV3, frame quality, device profile, media/keyframe/messages/ClipRecorder (calibration-pinned v1 port), camera constraints | Yes |
@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 |
@sekyro/web/client | Typed client for the hosted session API: HostedClient, error codes, check input/view types | Yes |
@sekyro/web/popup | Partner-facing popup/redirect embed (open, SekyroEvent) | Yes |
@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 AI's hosted page / @sekyro/react only. No semver guarantees; may change in any release | No |
Every entry ships ESM + CJS + .d.ts (tsup). ./core deliberately does not
re-export the document scanner (that lives only under ./internal) nor the worker
entry.
Calibration pin — do not "improve" src/core/
src/core/media.ts, keyframe.ts, and messages.ts are byte-identical ports of
v1's kyc-ibtikar/lib/faceCaptureV2/*. The only allowed delta is the 3-line port
header block (two comment lines + one blank separator line) prepended to each file.
src/core/clip-recorder.ts is the de-hooked useClipRecorder with identical
constants and behavior (CLIP_MS=2500, TIMESLICE_MS=1000, STOP_GRACE_MS=1500,
VIDEO_BPS=2_500_000, MAX_CLIP_BYTES=8MiB, mime ladder, guarded double construction,
incremental cap enforcement, stop-grace finalize, single-settle, stop-all-tracks).
The FAS-CLIP liveness model was calibrated on clips produced by exactly this v1
pipeline (spec §6 / risk 1). Any drift silently invalidates the calibration.
test/port-parity.test.ts enforces this in two layers: a sha256 checksum pin against
the committed test/port-manifest.json that runs everywhere, CI included, and a
byte-level comparison against the real v1 sources that runs on every dev machine with
the kyc-ibtikar/ checkout alongside (that layer self-skips in CI, where the
gitignored v1 checkout is absent — and it also verifies the manifest itself matches
header + v1 bytes, so the manifest cannot drift). Do NOT edit src/core/ without a
calibration re-validation; if a re-validated change ever lands, regenerate the
manifest hashes alongside it.
The parity test resolves the v1 checkout as ../../kyc-ibtikar/lib/faceCaptureV2
from the package root, so this package must stay two levels below the repo root
(packages/sekyro-web).
Note: v1's upload.ts is intentionally NOT ported — uploading is the typed client's
job in v2 (HostedClient.uploadAndSubmit).
Develop
npm ci
npm run build # tsup → dist/ (ESM + CJS + d.ts per entry)
npm test # vitest (jsdom)
npm run typecheck # tsc --noEmit
npm run api:check # api-extractor: fails when the ROOT public surface drifts from etc/sekyro-web.api.md
npm run api:report # regenerate etc/sekyro-web.api.md after an intentional, reviewed API changeThe api-extractor report covers the root . entry only (the partner contract); the
subpath entries (./core, ./client, ./popup) are not reported. npm run build must
precede api:check (it reads dist/index.d.ts).
Standalone package: own lockfile, no workspace wiring. identity-hosted consumes it
via "@sekyro/web": "file:../packages/sekyro-web" and needs dist/ built first.