Sekyro AIDocs

@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, still private — the public npm release is wired in a later task (Changesets + Trusted Publishing). Sekyro AI's own hosted page consumes it via a file: 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 automatically
  • preloadLivenessAssets is idempotent per assetBaseUrl and resolves the settled status: 'ready', 'failed' (call it again to retry), or 'idle' — nothing was preloaded (SSR, no camera API, Data-Saver with respectSaveData); just start normally. idle: true defers 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 assetBaseUrl uses it without any extra wiring, and destroying a capture never frees it; call releaseLivenessAssets(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 / subscribeLivenessAssets follow the useSyncExternalStore contract (no emit on subscribe). subscribeCameraPermission emits the current state first, then every change; queryCameraPermission is 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, default true) holds a Screen Wake Lock while the camera is live (starting / gating / recording), released on captured, error and destroy(), re-taken on retry() 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">. Pass false to never request it.

Entry points

ExportContentsPublic API
@sekyro/webThe 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/coreFramework-free capture: CaptureSessionV3, createDetector, FaceGateV3, frame quality, device profile, media/keyframe/messages/ClipRecorder (calibration-pinned v1 port), camera constraintsYes
@sekyro/web/core/v3/detect-workerStandalone 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 bundleYes
@sekyro/web/clientTyped client for the hosted session API: HostedClient, error codes, check input/view typesYes
@sekyro/web/popupPartner-facing popup/redirect embed (open, SekyroEvent)Yes
@sekyro/web/internalNot 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 releaseNo

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 change

The 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.

On this page