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

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 +7 -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-LOTAJDRE.js +1094 -0
  9. package/dist/chunk-LOTAJDRE.js.map +1 -0
  10. package/dist/chunk-MWNFSNMP.js +52 -0
  11. package/dist/chunk-MWNFSNMP.js.map +1 -0
  12. package/dist/chunk-NILNKLK6.js +50 -0
  13. package/dist/chunk-NILNKLK6.js.map +1 -0
  14. package/dist/chunk-NU4YOLCN.js +796 -0
  15. package/dist/chunk-NU4YOLCN.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--ldwakXV.d.cts +341 -0
  41. package/dist/create-verification-BlAPWeW5.d.ts +341 -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 +2580 -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 +4110 -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 @@
1
+ {"version":3,"sources":["../src/verification/emitter.ts","../src/capture/create-liveness-capture.ts","../src/verification/result.ts","../src/verification/liveness-flow.ts","../src/verification/turnstile.ts","../src/verification/create-verification.ts"],"sourcesContent":["// Tiny typed emitter (zero deps). Handlers are snapshotted before dispatch so an\n// unsubscribe from inside a handler never skips a sibling.\n\nexport interface Emitter<Events extends object> {\n on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): () => void;\n emit<K extends keyof Events>(event: K, payload: Events[K]): void;\n clear(): void;\n}\n\nexport function createEmitter<Events extends object>(): Emitter<Events> {\n const handlers = new Map<keyof Events, Set<(payload: never) => void>>();\n return {\n on(event, handler) {\n let set = handlers.get(event);\n if (set === undefined) {\n set = new Set();\n handlers.set(event, set);\n }\n const h = handler as (payload: never) => void;\n set.add(h);\n return () => {\n set?.delete(h);\n };\n },\n emit(event, payload) {\n const set = handlers.get(event);\n if (set === undefined) return;\n for (const handler of [...set]) {\n (handler as (p: Events[typeof event]) => void)(payload);\n }\n },\n clear() {\n handlers.clear();\n },\n };\n}\n","// createLivenessCapture — the public, framework-free capture handle (spec\n// §3.1) over the calibration-pinned CaptureSessionV3 engine. It MAPS the\n// engine's event surface (statechange / checks / guidance / artifacts / error /\n// frozen) onto the partner contract (state / gate / captured / error) and owns\n// the NO_RECORDER degrade (photo mode → keyframe-only `captured`). Pinned\n// capture constants stay inside the engine — nothing here is configurable.\nimport { detectIsMobile } from \"../core/capture-constraints\";\nimport { isMediaRecorderSupported } from \"../core/media\";\nimport type { CaptureErrorKind as EngineErrorKind, CaptureState as EngineState } from \"../core/types\";\nimport {\n CaptureSessionV3,\n type CaptureSessionV3Events,\n type CaptureSessionV3Options,\n} from \"../core/v3/capture-session-v3\";\nimport { createEmitter } from \"../verification/emitter\";\nimport type {\n CaptureErrorKind,\n CaptureState,\n GateHint,\n LivenessCaptureEvents,\n LivenessCaptureHandle,\n LivenessCaptureOptions,\n} from \"../verification/types\";\n\n/** The engine surface the handle binds — CaptureSessionV3 or a test double. */\nexport interface CaptureEngine {\n readonly state: EngineState;\n on<K extends keyof CaptureSessionV3Events>(\n event: K,\n handler: (payload: CaptureSessionV3Events[K]) => void,\n ): () => void;\n start(): Promise<void>;\n retry(): void;\n switchCamera(): void;\n destroy(): void;\n /** Device-profile chrome hint (LOW class → `'reduced'`); absent on doubles. */\n readonly animations?: \"full\" | \"reduced\";\n}\n\n/** Internal construction seams (tests; Sekyro's own hosted page). */\nexport interface LivenessCaptureInternals {\n /** Replace the engine constructor (default: `new CaptureSessionV3(options)`). */\n createEngine?: (options: CaptureSessionV3Options) => CaptureEngine;\n /** Extra engine options merged in (worker offload, warmed detector, telemetry). */\n engineOptions?: Partial<\n Pick<CaptureSessionV3Options, \"createDetector\" | \"createWorker\" | \"telemetry\" | \"deviceClass\">\n >;\n /** Clock (default `performance.now`). */\n now?: () => number;\n}\n\n/** fcv3.* guidance key → public gate hint code. */\nexport function mapGuidanceHint(key: string): GateHint | null {\n switch (key) {\n case \"fcv3.no_face\":\n case \"fcv3.center_face\":\n case \"fcv3.straighten\":\n return \"center\";\n case \"fcv3.come_closer\":\n return \"closer\";\n case \"fcv3.move_back\":\n return \"farther\";\n case \"fcv3.more_light\":\n case \"fcv3.too_bright\":\n return \"light\";\n case \"fcv3.hold_still\":\n case \"fcv3.steady_camera\":\n return \"still\";\n default:\n return null;\n }\n}\n\n/** Engine state → public state (`preflight|acquiring → starting`, `positioning → gating`). */\nexport function mapEngineState(state: EngineState): CaptureState {\n switch (state) {\n case \"preflight\":\n case \"acquiring\":\n return \"starting\";\n case \"positioning\":\n return \"gating\";\n case \"idle\":\n case \"recording\":\n case \"captured\":\n case \"error\":\n return state;\n default: {\n const unhandled: never = state;\n throw new Error(`mapEngineState: unhandled state ${unhandled as string}`);\n }\n }\n}\n\n/** Engine error kinds → the public six (`models|cliptoolarge|recorder|overconstrained → generic`). */\nexport function mapErrorKind(kind: EngineErrorKind): CaptureErrorKind {\n switch (kind) {\n case \"permission\":\n case \"notfound\":\n case \"inuse\":\n case \"insecure\":\n case \"unsupported\":\n return kind;\n case \"overconstrained\":\n case \"generic\":\n case \"models\":\n case \"cliptoolarge\":\n case \"recorder\":\n return \"generic\";\n default: {\n const unhandled: never = kind;\n throw new Error(`mapErrorKind: unhandled kind ${unhandled as string}`);\n }\n }\n}\n\n/** Handle + the hooks the verification layer uses to drive the flow states. */\nexport interface LivenessCaptureInternalHandle {\n handle: LivenessCaptureHandle;\n /** Push a post-capture state (`uploading | checking | done`). */\n setFlowState(state: Extract<CaptureState, \"uploading\" | \"checking\" | \"done\">): void;\n}\n\nexport function createLivenessCaptureInternal(\n options: LivenessCaptureOptions,\n internals: LivenessCaptureInternals = {},\n): LivenessCaptureInternalHandle {\n const now = internals.now ?? (() => performance.now());\n const emitter = createEmitter<LivenessCaptureEvents>();\n // NO_RECORDER degrade: no MediaRecorder → photo mode (keyframe only). Decided\n // ONCE at construction; the artifacts carry the flag.\n const degraded = !isMediaRecorderSupported();\n const engineOptions: CaptureSessionV3Options = {\n video: options.video,\n mode: degraded ? \"photo\" : \"clip\",\n ...(options.facing !== undefined ? { facingMode: options.facing } : {}),\n ...(options.assetBaseUrl !== undefined ? { v3AssetBaseUrl: options.assetBaseUrl } : {}),\n ...(internals.engineOptions ?? {}),\n };\n const engine: CaptureEngine =\n internals.createEngine?.(engineOptions) ?? new CaptureSessionV3(engineOptions);\n\n let state: CaptureState = \"idle\";\n let hint: GateHint | null = null;\n let progress = 0;\n let faceDetected = false;\n let animations: \"full\" | \"reduced\" = \"full\";\n let recordingStartedAt: number | null = null;\n let frozenAt: number | null = null;\n\n const setState = (next: CaptureState): void => {\n state = next;\n emitter.emit(\"state\", next);\n };\n const emitGate = (): void => emitter.emit(\"gate\", { progress, hint, faceDetected });\n // The LOW-profile hint settles once the engine probes (statechange) and may\n // flip mid-positioning on a dynamic demotion (per checks tick) — emit on change.\n const syncProfile = (): void => {\n const next = engine.animations ?? \"full\";\n if (next === animations) return;\n animations = next;\n emitter.emit(\"profile\", { animations });\n };\n\n const unsubscribes = [\n engine.on(\"statechange\", ({ state: engineState }) => {\n const next = mapEngineState(engineState);\n if (next === \"starting\" && state !== \"starting\") {\n // Fresh attempt: gate chrome resets.\n hint = null;\n progress = 0;\n faceDetected = false;\n recordingStartedAt = null;\n frozenAt = null;\n }\n setState(next);\n syncProfile();\n if (next === \"recording\") {\n recordingStartedAt = now();\n hint = \"hold\";\n progress = 1;\n emitGate();\n }\n }),\n engine.on(\"checks\", (payload) => {\n progress = payload.progress;\n faceDetected = payload.factors.presence > 0;\n emitGate();\n syncProfile();\n }),\n engine.on(\"guidance\", ({ key }) => {\n hint = mapGuidanceHint(key);\n emitGate();\n }),\n engine.on(\"frozen\", ({ previewDataUrl }) => {\n frozenAt = now();\n emitter.emit(\"frozen\", { previewDataUrl });\n }),\n engine.on(\"artifacts\", (artifacts) => {\n const clipMs =\n recordingStartedAt !== null && frozenAt !== null\n ? Math.max(0, Math.round(frozenAt - recordingStartedAt))\n : undefined;\n emitter.emit(\"captured\", {\n keyframe: artifacts.keyframe,\n ...(artifacts.clip !== undefined ? { clip: artifacts.clip } : {}),\n ...(artifacts.clipMimeType !== undefined ? { clipMime: artifacts.clipMimeType } : {}),\n width: options.video.videoWidth,\n height: options.video.videoHeight,\n ...(clipMs !== undefined ? { clipMs } : {}),\n captureChannel: detectIsMobile() ? \"mobile\" : \"desktop\",\n ...(degraded ? { degraded: \"NO_RECORDER\" as const } : {}),\n });\n }),\n engine.on(\"error\", ({ kind, retryable }) => {\n emitter.emit(\"error\", { kind: mapErrorKind(kind), retryable });\n }),\n ];\n\n const handle: LivenessCaptureHandle = {\n get state() {\n return state;\n },\n on: (event, handler) => emitter.on(event, handler),\n start: () => engine.start(),\n retry: () => engine.retry(),\n switchCamera: () => engine.switchCamera(),\n destroy: () => {\n for (const unsubscribe of unsubscribes) unsubscribe();\n engine.destroy();\n emitter.clear();\n },\n };\n\n return {\n handle,\n setFlowState: (next) => {\n if (state !== next) setState(next);\n },\n };\n}\n\n/**\n * Create a standalone liveness capture (no upload — pair it with your own\n * submission, or use {@link Verification.liveness} for the full flow).\n */\nexport function createLivenessCapture(options: LivenessCaptureOptions): LivenessCaptureHandle {\n return createLivenessCaptureInternal(options).handle;\n}\n","// Result mapping: server view → the public VerificationResult (spec §3.1) plus\n// the view helpers the flow needs (frontier, terminal, view merge).\nimport type {\n CheckReason,\n CheckType,\n HostedSessionView,\n HostedStepView,\n SessionView,\n} from \"../client/types\";\nimport type { ReasonCode, VerificationResult } from \"./types\";\n\n/**\n * Reasons the backend parks at `input_required` while attempts remain\n * (identity-backend/src/checks/check-reasons.ts isRetryable tables — the single\n * source of truth; identity-hosted/lib/reason-copy.ts mirrors the same rows).\n */\nexport const RETRYABLE_REASONS: ReadonlySet<CheckReason> = new Set<CheckReason>([\n \"no_face\",\n \"media_unreadable\",\n \"clip_too_short\",\n \"no_face_source\",\n \"side_missing\",\n \"unreadable\",\n \"registry_unavailable\",\n \"otp_mismatch\",\n \"otp_expired\",\n \"otp_send_failed\",\n]);\n\n/** The two checks one biometric capture feeds. */\nconst BIOMETRIC_CHECKS: ReadonlySet<CheckType> = new Set<CheckType>([\"liveness\", \"face_match\"]);\n\n/** A check the browser may still submit. */\nexport function isExecutable(step: HostedStepView | undefined): boolean {\n return step !== undefined && (step.state === \"pending\" || step.state === \"input_required\");\n}\n\n/** The session accepts no further submissions. */\nexport function isTerminalView(view: Pick<HostedSessionView, \"status\">): boolean {\n return view.status !== \"created\" && view.status !== \"in_progress\";\n}\n\n/** First step in `steps[]` order with state `pending | input_required` (the backend's frontier). */\nexport function frontierStep(view: HostedSessionView): HostedStepView | undefined {\n return view.steps.find((step) => isExecutable(step));\n}\n\nexport function stepOf(view: HostedSessionView, checkType: CheckType): HostedStepView | undefined {\n return view.steps.find((step) => step.checkType === checkType);\n}\n\n/**\n * Fold a submit response (partner `SessionView`: no attemptsRemaining) into the\n * hosted view: status/outcome/step states+reasons come from the response; the\n * submitted check's `attemptsRemaining` is decremented locally (every accepted\n * submit consumes exactly one attempt server-side — `max(0, cap - attempts)`).\n * This keeps the happy path free of extra `GET /hosted/session` round-trips.\n */\nexport function mergeSessionView(\n prev: HostedSessionView,\n next: SessionView,\n submitted: CheckType,\n): HostedSessionView {\n const byType = new Map(prev.steps.map((step) => [step.checkType, step] as const));\n const steps: HostedStepView[] = next.steps.map((step) => {\n const before = byType.get(step.checkType);\n const attemptsBefore = before?.attemptsRemaining ?? 0;\n const merged: HostedStepView = {\n ...(before ?? {}),\n checkType: step.checkType,\n state: step.state,\n attemptsRemaining:\n step.checkType === submitted ? Math.max(0, attemptsBefore - 1) : attemptsBefore,\n };\n if (step.reason !== undefined) merged.reason = step.reason;\n else delete merged.reason;\n return merged;\n });\n return { ...prev, status: next.status, outcome: next.outcome, steps, expiresAt: next.expiresAt };\n}\n\nexport function unavailable(sessionId: string, reason?: ReasonCode): VerificationResult {\n return reason === undefined\n ? { sessionId, status: \"unavailable\" }\n : { sessionId, status: \"unavailable\", reason };\n}\n\nfunction withReason(\n sessionId: string,\n status: VerificationResult[\"status\"],\n reason: CheckReason | undefined,\n): VerificationResult {\n return reason === undefined ? { sessionId, status } : { sessionId, status, reason };\n}\n\n/**\n * Map the session view to the public result:\n * - `completed/approved → approved`, `completed/review` or `review_pending → review`,\n * `failed` or `completed/rejected → rejected` (reason = the deciding step's);\n * - `expired → unavailable` (no reason);\n * - in flight: a biometric step parked at `input_required` with a retryable\n * reason → `retry` when `autoRetry` and attempts remain, else `rejected`\n * (the partner opted out of retries — this capture was refused); a step in\n * `review` with nothing executable left → `review`; otherwise `unavailable`\n * (a non-biometric frontier the SDK cannot drive, or still processing).\n */\nexport function mapSessionResult(\n view: HostedSessionView,\n opts: { autoRetry: boolean },\n): VerificationResult {\n const { sessionId } = view;\n switch (view.status) {\n case \"completed\":\n if (view.outcome === \"approved\") return { sessionId, status: \"approved\" };\n if (view.outcome === \"review\") return withReason(sessionId, \"review\", reasonIn(view, \"review\"));\n return withReason(sessionId, \"rejected\", reasonIn(view, \"failed\"));\n case \"failed\":\n return withReason(sessionId, \"rejected\", reasonIn(view, \"failed\"));\n case \"review_pending\":\n return withReason(sessionId, \"review\", reasonIn(view, \"review\"));\n case \"expired\":\n return unavailable(sessionId);\n case \"created\":\n case \"in_progress\":\n break;\n default: {\n const unhandled: never = view.status;\n throw new Error(`mapSessionResult: unhandled status ${unhandled as string}`);\n }\n }\n\n const parked = view.steps.find(\n (step) =>\n BIOMETRIC_CHECKS.has(step.checkType) &&\n step.state === \"input_required\" &&\n step.reason !== undefined,\n );\n if (parked?.reason !== undefined) {\n const canRetry =\n opts.autoRetry && RETRYABLE_REASONS.has(parked.reason) && parked.attemptsRemaining > 0;\n return {\n sessionId,\n status: canRetry ? \"retry\" : \"rejected\",\n reason: parked.reason,\n attemptsRemaining: parked.attemptsRemaining,\n };\n }\n if (frontierStep(view) === undefined && view.steps.some((step) => step.state === \"review\")) {\n return withReason(sessionId, \"review\", reasonIn(view, \"review\"));\n }\n return unavailable(sessionId);\n}\n\n/** Reason of the first step in the given state (the deciding check). */\nfunction reasonIn(view: HostedSessionView, state: HostedStepView[\"state\"]): CheckReason | undefined {\n return view.steps.find((step) => step.state === state && step.reason !== undefined)?.reason;\n}\n","// The post-capture flow: upload the clip → attestation → submit `liveness` →\n// upload the keyframe → submit `face_match`. Logic ported (not the React) from\n// identity-hosted/lib/use-submit.ts (idempotency-key lifecycle, same-key\n// transport replay, one-shot consent_required recovery) and\n// lib/use-capture-submission.ts + components/steps/liveness-step.tsx (the\n// `buildSubmit` executor closures and the face_match chain).\n//\n// Per-submission caches (promoted media key, attestation) are CLOSURE LOCALS of\n// each executor: a same-key replay re-runs the executor with the caches intact\n// (byte-identical body — a re-presign or a fresh token under the same key would\n// be an idempotency_key_collision); fresh artifacts build fresh closures.\nimport { HostedApiError } from \"../client/errors\";\nimport { type HostedClient, stripMimeParams } from \"../client/hosted-client\";\nimport type {\n AttestationInput,\n CheckType,\n HostedSessionView,\n SessionView,\n} from \"../client/types\";\nimport { isExecutable, isTerminalView, stepOf } from \"./result\";\nimport type { CapturedArtifacts, SdkFailureReason } from \"./types\";\n\n/** The client surface the flow needs — `HostedClient` or a test fake. */\nexport type HostedClientLike = Pick<\n HostedClient,\n \"getSession\" | \"recordConsent\" | \"presign\" | \"uploadToSpaces\" | \"completeMedia\" | \"submitCheck\"\n>;\n\n/** The presign policy allowlist for `kind: 'clip'` (media.policy.ts mirror). */\nexport const CLIP_MIME_ALLOWLIST: readonly string[] = [\"video/webm\", \"video/mp4\"];\n\n/** The engine's keyframe encode (captureMirroredKeyframe → canvas JPEG). */\nconst KEYFRAME_MIME = \"image/jpeg\";\n\n/** Same-key transport replays AFTER the first try, and the pause between them. */\nexport const TRANSPORT_REPLAY_ATTEMPTS = 2;\nexport const TRANSPORT_REPLAY_DELAY_MS = 1_000;\n/** Cap on honouring a 429's Retry-After before giving up on this capture. */\nconst MAX_RETRY_AFTER_MS = 10_000;\n\n/** No (usable) response arrived — a same-key replay can succeed. */\nconst TRANSPORT_CODES: ReadonlySet<string> = new Set([\n \"network_error\",\n \"timeout\",\n \"upload_failed\",\n \"unknown_error\",\n \"rate_limit_exceeded\",\n]);\n\n/** The server moved past what we hold — re-GET and map from truth. */\nconst RESYNC_CODES: ReadonlySet<string> = new Set([\n \"session_not_active\",\n \"check_not_found\",\n \"check_already_finalized\",\n \"check_out_of_order\",\n]);\n\n/** A Turnstile challenge failed client-side (load / render / error-callback / timeout). */\nexport class AttestationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AttestationError\";\n }\n}\n\n/** One logical submission: receives the client and ITS idempotency key. */\nexport type SubmitExecutor = (\n client: HostedClientLike,\n idempotencyKey: string,\n) => Promise<SessionView>;\n\nexport interface SubmitRuntime {\n client: HostedClientLike;\n /** Force-record consent (the server just said it is missing). */\n ensureConsent(opts: { force: true }): Promise<void>;\n sleep(ms: number): Promise<void>;\n uuid(): string;\n}\n\n/**\n * Run one logical submission (ported use-submit semantics):\n * - a fresh `crypto.randomUUID()` key per logical submission;\n * - a transport-class failure replays the SAME executor under the SAME key\n * (bounded) — the backend's IdempotencyInterceptor returns the first\n * response instead of burning another attempt;\n * - ONE `consent_required` recovery: force-record consent and re-run under a\n * FRESH key (the 409 envelope is idempotency-cached under the old one);\n * - ONE attestation retry (client-side Turnstile failure or a server\n * `attestation_invalid`): `onAttestationInvalid` drops the cached token and\n * the executor re-runs under a fresh key.\n * Throws the final error otherwise.\n */\nexport async function runSubmission(\n rt: SubmitRuntime,\n executor: SubmitExecutor,\n hooks: { onAttestationInvalid?: () => void } = {},\n): Promise<SessionView> {\n let key = rt.uuid();\n let consentRecovered = false;\n let attestationRetried = false;\n let transportReplays = 0;\n for (;;) {\n try {\n return await executor(rt.client, key);\n } catch (err) {\n if (err instanceof AttestationError) {\n if (attestationRetried) throw err;\n attestationRetried = true;\n hooks.onAttestationInvalid?.();\n key = rt.uuid();\n continue;\n }\n if (!(err instanceof HostedApiError)) throw err;\n if (err.code === \"consent_required\" && !consentRecovered) {\n consentRecovered = true;\n await rt.ensureConsent({ force: true });\n key = rt.uuid();\n continue;\n }\n if (err.code === \"attestation_invalid\" && !attestationRetried) {\n attestationRetried = true;\n hooks.onAttestationInvalid?.();\n key = rt.uuid();\n continue;\n }\n if (TRANSPORT_CODES.has(err.code) && transportReplays < TRANSPORT_REPLAY_ATTEMPTS) {\n transportReplays += 1;\n const retryAfterMs =\n err.retryAfterSeconds !== undefined ? err.retryAfterSeconds * 1000 : undefined;\n await rt.sleep(\n retryAfterMs !== undefined\n ? Math.min(retryAfterMs, MAX_RETRY_AFTER_MS)\n : TRANSPORT_REPLAY_DELAY_MS,\n );\n continue;\n }\n throw err;\n }\n }\n}\n\nexport interface LivenessFlowDeps extends SubmitRuntime {\n artifacts: CapturedArtifacts;\n /** Fresh attestation per call (Turnstile execute, or `{sandbox: true}`). */\n getAttestation(): Promise<AttestationInput>;\n /** Latest hosted view (frontier decisions). */\n getView(): HostedSessionView;\n /** Fold a submit response into the view. */\n applySubmit(view: SessionView, submitted: CheckType): void;\n /** Flow-state hook for the capture handle. */\n onPhase(phase: \"uploading\" | \"checking\"): void;\n /** A newer capture / destroy superseded this flow. */\n isCancelled(): boolean;\n}\n\nexport type LivenessFlowOutcome =\n | { kind: \"settled\" }\n | { kind: \"resync\" }\n | { kind: \"unavailable\"; reason: SdkFailureReason }\n | { kind: \"cancelled\" };\n\n/**\n * Run the biometric submissions that are still executable: `liveness` when its\n * step is `pending | input_required`, then `face_match` when face_match is\n * executable and liveness either was NOT part of this capture (it had already\n * settled — any state, incl. `failed`: the selfie-fallback case, where this\n * capture only feeds face_match) or settled not-failed from this capture\n * (passed / review / processing / skipped — or absent). Returns `settled` for\n * the caller to map the view.\n */\nexport async function runLivenessFlow(deps: LivenessFlowDeps): Promise<LivenessFlowOutcome> {\n const { artifacts } = deps;\n const clip = artifacts.clip;\n const bareMime = stripMimeParams(artifacts.clipMime ?? \"\");\n if (\n artifacts.degraded === \"NO_RECORDER\" ||\n clip === undefined ||\n !CLIP_MIME_ALLOWLIST.includes(bareMime)\n ) {\n // No clip, or a container outside the presign allowlist (never sign an\n // off-allowlist type): v2 cannot run THIS capture.\n return { kind: \"unavailable\", reason: \"no_recorder\" };\n }\n\n try {\n const livenessWasExecutable = isExecutable(stepOf(deps.getView(), \"liveness\"));\n if (livenessWasExecutable) {\n let clipKey: string | null = null;\n let attestation: AttestationInput | null = null;\n const response = await runSubmission(\n deps,\n async (client, idempotencyKey) => {\n if (clipKey === null) {\n deps.onPhase(\"uploading\");\n const presigned = await client.presign(\"clip\", bareMime, clip.size);\n await client.uploadToSpaces(presigned.uploadUrl, clip, bareMime);\n clipKey = (await client.completeMedia(presigned.key)).key;\n }\n // Attestation AFTER the upload settles (token stays fresh), cached\n // for the same-key replay.\n if (attestation === null) {\n try {\n attestation = await deps.getAttestation();\n } catch (err) {\n throw new AttestationError(err instanceof Error ? err.message : String(err));\n }\n }\n deps.onPhase(\"checking\");\n return client.submitCheck(\n \"liveness\",\n { clipKey, attestation, captureChannel: artifacts.captureChannel },\n { idempotencyKey },\n );\n },\n {\n onAttestationInvalid: () => {\n attestation = null;\n },\n },\n );\n if (deps.isCancelled()) return { kind: \"cancelled\" };\n deps.applySubmit(response, \"liveness\");\n }\n\n const view = deps.getView();\n const liveness = stepOf(view, \"liveness\");\n const livenessOk =\n !livenessWasExecutable ||\n liveness === undefined ||\n liveness.state === \"passed\" ||\n liveness.state === \"review\" ||\n liveness.state === \"processing\" ||\n liveness.state === \"skipped\";\n if (!isTerminalView(view) && livenessOk && isExecutable(stepOf(view, \"face_match\"))) {\n const keyframe = artifacts.keyframe;\n let imageKey: string | null = null;\n const response = await runSubmission(deps, async (client, idempotencyKey) => {\n if (imageKey === null) {\n deps.onPhase(\"uploading\");\n const presigned = await client.presign(\"image\", KEYFRAME_MIME, keyframe.size);\n await client.uploadToSpaces(presigned.uploadUrl, keyframe, KEYFRAME_MIME);\n imageKey = (await client.completeMedia(presigned.key)).key;\n }\n deps.onPhase(\"checking\");\n return client.submitCheck(\"face_match\", { imageKey }, { idempotencyKey });\n });\n if (deps.isCancelled()) return { kind: \"cancelled\" };\n deps.applySubmit(response, \"face_match\");\n }\n return { kind: \"settled\" };\n } catch (err) {\n if (deps.isCancelled()) return { kind: \"cancelled\" };\n return classifyFailure(err);\n }\n}\n\nfunction classifyFailure(err: unknown): LivenessFlowOutcome {\n if (err instanceof AttestationError) return { kind: \"unavailable\", reason: \"attestation\" };\n if (err instanceof HostedApiError) {\n if (err.code === \"attestation_invalid\") return { kind: \"unavailable\", reason: \"attestation\" };\n if (RESYNC_CODES.has(err.code)) return { kind: \"resync\" };\n }\n return { kind: \"unavailable\", reason: \"network\" };\n}\n","// Cloudflare Turnstile: lazy script loader + execute-mode invisible challenge.\n// Ported from identity-hosted/lib/turnstile.ts (Plan 6 Task 10 AC4) — same\n// behaviour: the script loads ONCE, lazily, only when a liveness submit first\n// needs a token; each token request renders a fresh execute-mode widget,\n// resolves the callback token, and removes the widget. Hosts' CSP must allow\n// challenges.cloudflare.com.\n//\n// The site key is RUNTIME data (`HostedSessionView.turnstileSiteKey`); when it\n// is null the verification sends `{sandbox: true}` instead of calling this.\n\nexport const TURNSTILE_SCRIPT_SRC = \"https://challenges.cloudflare.com/turnstile/v0/api.js\";\n\n/** Bound on a single execute round-trip (challenge fetch + solve). */\nexport const TURNSTILE_EXECUTE_TIMEOUT_MS = 30_000;\n\nexport interface TurnstileRenderParams {\n sitekey: string;\n callback: (token: string) => void;\n \"error-callback\"?: (code?: string) => void;\n execution?: \"render\" | \"execute\";\n appearance?: \"always\" | \"execute\" | \"interaction-only\";\n}\n\n/** The subset of the Turnstile JS API this module uses. */\nexport interface TurnstileApi {\n render(container: HTMLElement, params: TurnstileRenderParams): string | undefined;\n execute(widgetId: string): void;\n remove(widgetId: string): void;\n}\n\n/** `window.turnstile` without augmenting the global Window type for consumers. */\nfunction installedApi(): TurnstileApi | undefined {\n return (window as unknown as { turnstile?: TurnstileApi }).turnstile;\n}\n\nlet loaderPromise: Promise<TurnstileApi> | null = null;\n\n/** Test-only: reset the module singleton between cases. */\nexport function resetTurnstileForTests(): void {\n loaderPromise = null;\n}\n\n/** Load the Turnstile script once (singleton; a load failure resets it so a\n * retry can re-attempt). */\nexport function loadTurnstile(): Promise<TurnstileApi> {\n loaderPromise ??= new Promise<TurnstileApi>((resolve, reject) => {\n const present = installedApi();\n if (present) {\n resolve(present);\n return;\n }\n const script = document.createElement(\"script\");\n script.src = TURNSTILE_SCRIPT_SRC;\n script.async = true;\n script.onload = () => {\n const api = installedApi();\n if (api) {\n resolve(api);\n } else {\n loaderPromise = null;\n reject(new Error(\"Turnstile script loaded but exposed no API\"));\n }\n };\n script.onerror = () => {\n loaderPromise = null;\n script.remove(); // a retry appends a fresh tag\n reject(new Error(\"Turnstile script failed to load\"));\n };\n document.head.appendChild(script);\n });\n return loaderPromise;\n}\n\n/** Options of {@link executeTurnstile}. */\nexport interface ExecuteTurnstileOptions {\n /** Bound on the challenge round-trip; default {@link TURNSTILE_EXECUTE_TIMEOUT_MS}. */\n timeoutMs?: number;\n /**\n * Aborts the challenge: the promise rejects and the widget, its container and\n * the timeout are cleaned up at once. `createVerification().destroy()` passes\n * its own signal so a torn-down verification never leaves a widget behind.\n */\n signal?: AbortSignal;\n}\n\nfunction abortError(): Error {\n return new Error(\"Turnstile challenge aborted\");\n}\n\n/**\n * Run one invisible execute-mode challenge and resolve its token. Rejects on\n * challenge error, on `signal` abort, or after {@link TURNSTILE_EXECUTE_TIMEOUT_MS}\n * — the verification maps a failure to `result {status:'unavailable',\n * reason:'attestation'}` after one retry.\n */\nexport async function executeTurnstile(\n siteKey: string,\n opts: ExecuteTurnstileOptions = {},\n): Promise<string> {\n const timeoutMs = opts.timeoutMs ?? TURNSTILE_EXECUTE_TIMEOUT_MS;\n const signal = opts.signal;\n if (signal?.aborted) throw abortError();\n const api = await loadTurnstile();\n if (signal?.aborted) throw abortError();\n const container = document.createElement(\"div\");\n document.body.appendChild(container);\n let widgetId: string | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let onAbort: (() => void) | undefined;\n try {\n return await new Promise<string>((resolve, reject) => {\n timer = setTimeout(\n () => reject(new Error(`Turnstile challenge timed out (${timeoutMs} ms)`)),\n timeoutMs,\n );\n if (signal !== undefined) {\n onAbort = () => reject(abortError());\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n widgetId = api.render(container, {\n sitekey: siteKey,\n execution: \"execute\",\n appearance: \"execute\",\n callback: resolve,\n \"error-callback\": (code) => {\n reject(new Error(`Turnstile challenge failed${code ? ` (${code})` : \"\"}`));\n },\n });\n if (widgetId === undefined) {\n reject(new Error(\"Turnstile render returned no widget id\"));\n return;\n }\n api.execute(widgetId);\n });\n } finally {\n // ONE cleanup path for every outcome (token / challenge error / timeout /\n // abort / render failure): the challenge timer and the abort listener must\n // never leak past settlement.\n clearTimeout(timer);\n if (signal !== undefined && onAbort !== undefined) signal.removeEventListener(\"abort\", onAbort);\n if (widgetId !== undefined) {\n try {\n api.remove(widgetId);\n } catch {\n // widget already gone — nothing to clean\n }\n }\n container.remove();\n }\n}\n","// createVerification — composes the hosted client, consent, the early registry\n// submit, the capture handle and the post-capture flow behind the public\n// contract (spec §3.1). Framework-free; the only state is the latest hosted\n// view (re-emitted on every update via `on('view')`).\nimport { HostedClient } from \"../client/hosted-client\";\nimport type { AttestationInput, CheckType, HostedSessionView, SessionView } from \"../client/types\";\nimport {\n createLivenessCaptureInternal,\n type LivenessCaptureInternalHandle,\n type LivenessCaptureInternals,\n} from \"../capture/create-liveness-capture\";\nimport { createEmitter } from \"./emitter\";\nimport {\n type HostedClientLike,\n runLivenessFlow,\n runSubmission,\n type SubmitRuntime,\n} from \"./liveness-flow\";\nimport {\n frontierStep,\n isTerminalView,\n mapSessionResult,\n mergeSessionView,\n stepOf,\n unavailable,\n} from \"./result\";\nimport { executeTurnstile } from \"./turnstile\";\nimport type {\n CapturedArtifacts,\n LivenessCaptureHandle,\n LivenessCaptureOptions,\n Verification,\n VerificationEvents,\n VerificationOptions,\n VerificationResult,\n} from \"./types\";\n\n/** Production API origin — the `apiOrigin` default. */\nexport const DEFAULT_API_ORIGIN = \"https://api.sekyro.ai\";\n\n/** Pause before the single `registry_unavailable` re-submit. */\nexport const REGISTRY_RETRY_DELAY_MS = 5_000;\n\n/** Bounded poll while a biometric verdict is still `processing` after the submits. */\nexport const PROCESSING_POLL_DELAY_MS = 1_500;\nexport const PROCESSING_POLL_MAX = 10;\n\n/**\n * Construction seams — tests and Sekyro's own hosted page (`@sekyro/web/internal`).\n * Not part of the partner contract.\n */\nexport interface VerificationInternals {\n /** Replace the HTTP client (default: `new HostedClient({...})`). */\n client?: HostedClientLike;\n /**\n * Replace the Turnstile challenge (default: {@link executeTurnstile}). The\n * `signal` aborts when the verification is destroyed (M1).\n */\n getTurnstileToken?: (siteKey: string, opts: { signal: AbortSignal }) => Promise<string>;\n /** Capture-engine seams forwarded to {@link createLivenessCaptureInternal}. */\n capture?: LivenessCaptureInternals;\n sleep?: (ms: number) => Promise<void>;\n uuid?: () => string;\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** `crypto.randomUUID()` with a v4 fallback for hosts that lack it. */\nexport function randomUuid(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c !== undefined && typeof c.randomUUID === \"function\") return c.randomUUID();\n const bytes = new Uint8Array(16);\n if (c !== undefined && typeof c.getRandomValues === \"function\") c.getRandomValues(bytes);\n else for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);\n bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;\n bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;\n const hex = [...bytes].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/**\n * Create a verification bound to one session (spec §3.1):\n * `consent()` → `startRegistry()` → `liveness({video})` → `on('result')`.\n */\nexport function createVerification(options: VerificationOptions): Verification {\n return createVerificationInternal(options, {});\n}\n\n/** {@link createVerification} with construction seams (tests / Sekyro-internal hosts). */\nexport function createVerificationInternal(\n options: VerificationOptions,\n internals: VerificationInternals,\n): Verification {\n const apiOrigin = options.apiOrigin ?? DEFAULT_API_ORIGIN;\n const autoRetry = options.autoRetry ?? true;\n const rearm = options.rearm ?? \"auto\";\n const client: HostedClientLike =\n internals.client ??\n new HostedClient({\n apiOrigin,\n sessionToken: options.clientToken,\n ...(options.fetch !== undefined ? { fetch: options.fetch } : {}),\n });\n const sleep = internals.sleep ?? defaultSleep;\n const uuid = internals.uuid ?? randomUuid;\n const getTurnstileToken = internals.getTurnstileToken ?? executeTurnstile;\n const emitter = createEmitter<VerificationEvents>();\n\n let destroyed = false;\n // Aborts an in-flight Turnstile challenge on destroy() (widget + timer clean up).\n const lifetime = new AbortController();\n\n // ── view ───────────────────────────────────────────────────────────────────\n let view: HostedSessionView | null = null;\n let bootstrap: Promise<HostedSessionView> | null = null;\n const setView = (next: HostedSessionView): void => {\n view = next;\n emitter.emit(\"view\", next);\n };\n const ensureView = (): Promise<HostedSessionView> => {\n if (view !== null) return Promise.resolve(view);\n bootstrap ??= client\n .getSession()\n .then((next) => {\n setView(next);\n return next;\n })\n .finally(() => {\n bootstrap = null;\n });\n return bootstrap;\n };\n const refreshView = async (): Promise<HostedSessionView> => {\n const next = await client.getSession();\n setView(next);\n return next;\n };\n const applySubmit = (response: SessionView, submitted: CheckType): void => {\n if (view !== null) setView(mergeSessionView(view, response, submitted));\n };\n\n // ── consent (idempotent; `force` after a server consent_required) ──────────\n let consentInFlight: Promise<void> | null = null;\n const recordConsent = async (force: boolean): Promise<void> => {\n const current = await ensureView();\n if (!force && current.consentRecorded) return;\n await client.recordConsent(options.locale);\n if (view !== null) setView({ ...view, consentRecorded: true });\n };\n const ensureConsent = (opts?: { force?: boolean }): Promise<void> => {\n if (opts?.force === true) return recordConsent(true);\n consentInFlight ??= recordConsent(false).finally(() => {\n consentInFlight = null;\n });\n return consentInFlight;\n };\n\n const runtime: SubmitRuntime = {\n client,\n ensureConsent: (opts) => ensureConsent(opts),\n sleep,\n uuid,\n };\n\n // ── result emission ────────────────────────────────────────────────────────\n // A session verdict is emitted ONCE (the registry may settle it before any\n // capture); `retry` / `unavailable` are per capture.\n let terminalEmitted = false;\n const emitResult = (result: VerificationResult): void => {\n if (result.status === \"approved\" || result.status === \"rejected\" || result.status === \"review\") {\n if (terminalEmitted) return;\n terminalEmitted = true;\n }\n emitter.emit(\"result\", result);\n };\n\n // ── early registry ─────────────────────────────────────────────────────────\n let registry: Promise<void> | null = null;\n let registryFailed = false;\n // The frontier is population_registry on a session minted WITHOUT a bound\n // NNI: the SDK has nothing to submit (and must never guess one).\n let registryUnbound = false;\n const submitRegistry = (): Promise<SessionView> =>\n runSubmission(runtime, (c, idempotencyKey) =>\n // `{}` input: the session is bound to a SERVER-provided NNI (Plan A Task 2).\n c.submitCheck(\"population_registry\", {}, { idempotencyKey }),\n );\n const runRegistry = async (): Promise<void> => {\n try {\n const current = await ensureView();\n if (isTerminalView(current) || frontierStep(current)?.checkType !== \"population_registry\") {\n return;\n }\n if (current.nniBound !== true) {\n registryUnbound = true;\n return;\n }\n await ensureConsent();\n applySubmit(await submitRegistry(), \"population_registry\");\n const step = view === null ? undefined : stepOf(view, \"population_registry\");\n if (\n step?.state === \"input_required\" &&\n step.reason === \"registry_unavailable\" &&\n step.attemptsRemaining > 0\n ) {\n await sleep(REGISTRY_RETRY_DELAY_MS);\n if (destroyed) return;\n applySubmit(await submitRegistry(), \"population_registry\");\n }\n } catch {\n // Never throws (incl. 409 nni_locked / transport failures): the flow\n // surfaces it through result mapping.\n registryFailed = true;\n return;\n }\n if (view !== null && isTerminalView(view)) emitResult(mapSessionResult(view, { autoRetry }));\n };\n const startRegistry = (): Promise<void> => {\n registry ??= runRegistry();\n return registry;\n };\n\n // ── attestation ────────────────────────────────────────────────────────────\n // The SDK echoes the site key it rendered (`siteKey`) so the backend selects\n // the matching widget secret deterministically — Turnstile tokens are\n // single-use, so the verifier never retries with another secret (C1).\n const getAttestation = async (): Promise<AttestationInput> => {\n const current = await ensureView();\n const siteKey = current.turnstileSiteKey;\n if (siteKey === null) return { sandbox: true };\n return {\n channel: \"hosted_web\",\n siteKey,\n turnstileToken: await getTurnstileToken(siteKey, { signal: lifetime.signal }),\n };\n };\n\n // ── capture + flow ─────────────────────────────────────────────────────────\n let active: LivenessCaptureInternalHandle | null = null;\n let flowToken = 0;\n\n const settleProcessing = async (): Promise<HostedSessionView> => {\n let current = await ensureView();\n for (\n let i = 0;\n i < PROCESSING_POLL_MAX &&\n !destroyed &&\n !isTerminalView(current) &&\n frontierStep(current) === undefined &&\n current.steps.some((step) => step.state === \"processing\");\n i += 1\n ) {\n await sleep(PROCESSING_POLL_DELAY_MS);\n if (destroyed) break;\n current = await refreshView();\n }\n return current;\n };\n\n const runFlow = async (\n capture: LivenessCaptureInternalHandle,\n artifacts: CapturedArtifacts,\n token: number,\n ): Promise<void> => {\n const isCancelled = (): boolean => destroyed || token !== flowToken || active !== capture;\n const finish = (result: VerificationResult): void => {\n if (isCancelled()) return;\n emitResult(result);\n if (result.status === \"retry\" && rearm === \"auto\") {\n // Re-arm: the engine restarts and the next `captured` runs a new flow.\n capture.handle.retry();\n return;\n }\n // `done` (incl. a manual-rearm `retry`): the host calls `retry()` on the\n // handle when the user is ready — the engine restarts from there.\n capture.setFlowState(\"done\");\n };\n\n if (artifacts.degraded === \"NO_RECORDER\") {\n // No MediaRecorder: nothing is submitted (no liveness, no face_match).\n const sessionId = view?.sessionId ?? (await ensureView().catch(() => null))?.sessionId ?? \"\";\n finish(unavailable(sessionId, \"no_recorder\"));\n return;\n }\n\n capture.setFlowState(\"uploading\");\n try {\n // Registry first: it records consent itself only when it actually\n // submits — an unbound / failed registry frontier ends the flow below\n // without recording a consent nothing was submitted under.\n await startRegistry();\n if (isCancelled()) return;\n let current = await ensureView();\n if (!isTerminalView(current)) {\n if (frontierStep(current)?.checkType === \"population_registry\") {\n // The registry never passed (outage after the retry, or a request-\n // level failure): the biometric checks cannot be submitted.\n finish(\n unavailable(\n current.sessionId,\n registryUnbound ? \"nni_unbound\" : registryFailed ? \"network\" : \"registry_unavailable\",\n ),\n );\n return;\n }\n await ensureConsent();\n if (isCancelled()) return;\n const outcome = await runLivenessFlow({\n ...runtime,\n artifacts,\n getAttestation,\n getView: () => view ?? current,\n applySubmit,\n onPhase: (phase) => {\n if (!isCancelled()) capture.setFlowState(phase);\n },\n isCancelled,\n });\n if (isCancelled()) return;\n if (outcome.kind === \"unavailable\") {\n finish(unavailable(current.sessionId, outcome.reason));\n return;\n }\n if (outcome.kind === \"resync\") await refreshView();\n current = await settleProcessing();\n }\n finish(mapSessionResult(current, { autoRetry }));\n } catch {\n if (isCancelled()) return;\n finish(unavailable(view?.sessionId ?? \"\", \"network\"));\n }\n };\n\n const liveness = (captureOptions: LivenessCaptureOptions): LivenessCaptureHandle => {\n active?.handle.destroy();\n const capture = createLivenessCaptureInternal(captureOptions, internals.capture);\n active = capture;\n capture.handle.on(\"state\", (state) => {\n // A fresh engine attempt supersedes any in-flight flow for the old artifacts.\n if (state === \"starting\") flowToken += 1;\n });\n capture.handle.on(\"captured\", (artifacts) => {\n const token = ++flowToken;\n // Partners' own `captured` handlers run first (the event fires BEFORE\n // any upload); the flow starts on the next microtask.\n void Promise.resolve().then(() => runFlow(capture, artifacts, token));\n });\n return capture.handle;\n };\n\n return {\n consent: () => ensureConsent(),\n startRegistry,\n liveness,\n on: (event, handler) => emitter.on(event, handler),\n destroy: () => {\n destroyed = true;\n lifetime.abort();\n active?.handle.destroy();\n active = null;\n emitter.clear();\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,359 @@
1
+ // src/core/v3/detector-blaze.ts
2
+ var BLAZE_INIT_TIMEOUT_MS = 1e4;
3
+ var BlazeDetector = class _BlazeDetector {
4
+ constructor(inner, fallbackReason) {
5
+ this.tier = "blaze";
6
+ this.inner = inner;
7
+ if (fallbackReason !== void 0) this.fallbackReason = fallbackReason;
8
+ }
9
+ static async create(assetBaseUrl, fallbackReason) {
10
+ const vision = await import("@mediapipe/tasks-vision");
11
+ const fileset = await vision.FilesetResolver.forVisionTasks(assetBaseUrl);
12
+ const inner = await vision.FaceDetector.createFromOptions(fileset, {
13
+ baseOptions: {
14
+ modelAssetPath: `${assetBaseUrl}/blaze_face_short_range.tflite`,
15
+ // CPU delegate — the iOS-Safari-safe path (see header). NEVER 'GPU'.
16
+ delegate: "CPU"
17
+ },
18
+ runningMode: "IMAGE"
19
+ });
20
+ return new _BlazeDetector(inner, fallbackReason);
21
+ }
22
+ async detect(source, sourceWidth, sourceHeight) {
23
+ let detections;
24
+ try {
25
+ detections = this.inner.detect(source).detections;
26
+ } catch {
27
+ return null;
28
+ }
29
+ const best = pickMostConfident(detections);
30
+ if (best?.boundingBox === void 0) return null;
31
+ const size = intrinsicSize(source);
32
+ const eyes = extractEyes(best.keypoints, size);
33
+ const observation = {
34
+ box: {
35
+ x: best.boundingBox.originX,
36
+ y: best.boundingBox.originY,
37
+ width: best.boundingBox.width,
38
+ height: best.boundingBox.height
39
+ },
40
+ ...eyes !== void 0 ? { eyes } : {},
41
+ score: best.categories[0]?.score ?? 0
42
+ };
43
+ return toStreamPixels(observation, source, sourceWidth, sourceHeight);
44
+ }
45
+ destroy() {
46
+ try {
47
+ this.inner.close();
48
+ } catch {
49
+ }
50
+ }
51
+ };
52
+ function pickMostConfident(detections) {
53
+ let best;
54
+ let bestScore = -1;
55
+ for (const detection of detections) {
56
+ const score = detection.categories[0]?.score ?? 0;
57
+ if (score > bestScore) {
58
+ best = detection;
59
+ bestScore = score;
60
+ }
61
+ }
62
+ return best;
63
+ }
64
+ function extractEyes(keypoints, size) {
65
+ const a = keypoints?.[0];
66
+ const b = keypoints?.[1];
67
+ if (a === void 0 || b === void 0) return void 0;
68
+ const toSource = (p) => ({ x: p.x * size.width, y: p.y * size.height });
69
+ const [pa, pb] = [toSource(a), toSource(b)];
70
+ return pa.x <= pb.x ? { left: pa, right: pb } : { left: pb, right: pa };
71
+ }
72
+
73
+ // src/core/v3/detector-native.ts
74
+ function smokeSource() {
75
+ if (typeof OffscreenCanvas === "function") return new OffscreenCanvas(2, 2);
76
+ const canvas = document.createElement("canvas");
77
+ canvas.width = 2;
78
+ canvas.height = 2;
79
+ return canvas;
80
+ }
81
+ async function probeNativeDetector() {
82
+ const ctor = globalThis.FaceDetector;
83
+ if (typeof ctor !== "function") return { ok: false, reason: "unsupported" };
84
+ try {
85
+ const instance = new ctor({ fastMode: true, maxDetectedFaces: 1 });
86
+ await instance.detect(smokeSource());
87
+ return { ok: true, instance };
88
+ } catch (err) {
89
+ return { ok: false, reason: `smoke_failed:${err instanceof Error ? err.message : String(err)}` };
90
+ }
91
+ }
92
+ var NativeDetector = class {
93
+ constructor(inner, fallbackReason) {
94
+ this.tier = "native";
95
+ this.inner = inner;
96
+ if (fallbackReason !== void 0) this.fallbackReason = fallbackReason;
97
+ }
98
+ async detect(source, sourceWidth, sourceHeight) {
99
+ let faces;
100
+ try {
101
+ faces = await this.inner.detect(source);
102
+ } catch {
103
+ return null;
104
+ }
105
+ const face = faces[0];
106
+ if (face === void 0) return null;
107
+ const eyes = extractEyes2(face.landmarks);
108
+ const observation = {
109
+ box: {
110
+ x: face.boundingBox.x,
111
+ y: face.boundingBox.y,
112
+ width: face.boundingBox.width,
113
+ height: face.boundingBox.height
114
+ },
115
+ ...eyes !== void 0 ? { eyes } : {},
116
+ // The Shape Detection API exposes no confidence — the smoke-tested
117
+ // native tier treats any detection as confident.
118
+ score: 1
119
+ };
120
+ return toStreamPixels(observation, source, sourceWidth, sourceHeight);
121
+ }
122
+ destroy() {
123
+ }
124
+ };
125
+ function extractEyes2(landmarks) {
126
+ if (landmarks === void 0) return void 0;
127
+ const eyePoints = landmarks.filter((landmark) => landmark.type === "eye").map((landmark) => landmark.locations[0]).filter((point) => point !== void 0);
128
+ const [a, b] = eyePoints;
129
+ if (a === void 0 || b === void 0) return void 0;
130
+ return a.x <= b.x ? { left: a, right: b } : { left: b, right: a };
131
+ }
132
+
133
+ // src/core/v3/detector.ts
134
+ var DEFAULT_V3_ASSET_BASE_URL = "/models-v3";
135
+ var DEFAULT_TIER_ORDER = ["native", "blaze"];
136
+ var DetectorLadderError = class extends Error {
137
+ constructor(fallbackReason) {
138
+ super(`no detector tier available (${fallbackReason})`);
139
+ this.name = "DetectorLadderError";
140
+ this.fallbackReason = fallbackReason;
141
+ }
142
+ };
143
+ function intrinsicSize(source) {
144
+ const candidate = source;
145
+ if (typeof candidate.videoWidth === "number" && candidate.videoWidth > 0) {
146
+ return { width: candidate.videoWidth, height: candidate.videoHeight ?? 0 };
147
+ }
148
+ if (typeof candidate.naturalWidth === "number" && candidate.naturalWidth > 0) {
149
+ return { width: candidate.naturalWidth, height: candidate.naturalHeight ?? 0 };
150
+ }
151
+ const width = typeof candidate.width === "number" ? candidate.width : 0;
152
+ const height = typeof candidate.height === "number" ? candidate.height : 0;
153
+ return { width, height };
154
+ }
155
+ function toStreamPixels(observation, source, streamWidth, streamHeight) {
156
+ const size = intrinsicSize(source);
157
+ const scaleX = size.width > 0 ? streamWidth / size.width : 1;
158
+ const scaleY = size.height > 0 ? streamHeight / size.height : 1;
159
+ const scalePoint = (p) => ({ x: p.x * scaleX, y: p.y * scaleY });
160
+ return {
161
+ box: {
162
+ x: observation.box.x * scaleX,
163
+ y: observation.box.y * scaleY,
164
+ width: observation.box.width * scaleX,
165
+ height: observation.box.height * scaleY
166
+ },
167
+ ...observation.eyes !== void 0 ? { eyes: { left: scalePoint(observation.eyes.left), right: scalePoint(observation.eyes.right) } } : {},
168
+ score: observation.score
169
+ };
170
+ }
171
+ function describeError(err) {
172
+ return err instanceof Error ? err.message : String(err);
173
+ }
174
+ async function createDetector(options) {
175
+ const order = options.preferred ?? DEFAULT_TIER_ORDER;
176
+ const reasons = [];
177
+ for (const tier of order) {
178
+ if (tier === "native") {
179
+ const probed = await probeNativeDetector();
180
+ if (probed.ok) {
181
+ return new NativeDetector(probed.instance, reasonOf(reasons));
182
+ }
183
+ reasons.push(`native:${probed.reason}`);
184
+ continue;
185
+ }
186
+ try {
187
+ return await withTimeout(
188
+ BlazeDetector.create(
189
+ options.v3AssetBaseUrl ?? DEFAULT_V3_ASSET_BASE_URL,
190
+ reasonOf(reasons)
191
+ ),
192
+ options.blazeInitTimeoutMs ?? BLAZE_INIT_TIMEOUT_MS
193
+ );
194
+ } catch (err) {
195
+ reasons.push(
196
+ err instanceof InitTimeoutError ? "blaze:blaze_init_timeout" : `blaze:${describeError(err)}`
197
+ );
198
+ }
199
+ }
200
+ throw new DetectorLadderError(reasonOf(reasons) ?? "no_tier_available");
201
+ }
202
+ function reasonOf(reasons) {
203
+ return reasons.length > 0 ? reasons.join("; ") : void 0;
204
+ }
205
+ var InitTimeoutError = class extends Error {
206
+ };
207
+ function withTimeout(promise, deadlineMs) {
208
+ return new Promise((resolvePromise, rejectPromise) => {
209
+ const timer = setTimeout(() => {
210
+ rejectPromise(new InitTimeoutError(`init exceeded ${deadlineMs}ms`));
211
+ promise.then(
212
+ (value) => value.destroy(),
213
+ () => void 0
214
+ );
215
+ }, deadlineMs);
216
+ promise.then(
217
+ (value) => {
218
+ clearTimeout(timer);
219
+ resolvePromise(value);
220
+ },
221
+ (err) => {
222
+ clearTimeout(timer);
223
+ rejectPromise(err);
224
+ }
225
+ );
226
+ });
227
+ }
228
+
229
+ // src/core/v3/detect-worker.ts
230
+ var WORKER_PROTOCOL_VERSION = 1;
231
+ function workerOffloadSupported() {
232
+ return typeof OffscreenCanvas === "function" && typeof createImageBitmap === "function" && typeof Worker === "function";
233
+ }
234
+ function installWorkerDetectorHost(scope, deps = {}) {
235
+ const create = deps.createDetector ?? createDetector;
236
+ let detector = null;
237
+ scope.onmessage = (event) => {
238
+ const message = event.data;
239
+ if (message.v !== WORKER_PROTOCOL_VERSION) {
240
+ message.bitmap?.close?.();
241
+ return;
242
+ }
243
+ if (message.type === "init") {
244
+ void create(message.options).then((created) => {
245
+ detector = created;
246
+ const ready = {
247
+ v: WORKER_PROTOCOL_VERSION,
248
+ type: "ready",
249
+ tier: created.tier,
250
+ ...created.fallbackReason !== void 0 ? { fallbackReason: created.fallbackReason } : {}
251
+ };
252
+ scope.postMessage(ready);
253
+ });
254
+ return;
255
+ }
256
+ if (message.type === "detect") {
257
+ const respond = (observation) => {
258
+ const result = {
259
+ v: WORKER_PROTOCOL_VERSION,
260
+ type: "result",
261
+ id: message.id,
262
+ observation
263
+ };
264
+ scope.postMessage(result);
265
+ message.bitmap.close?.();
266
+ };
267
+ if (detector === null) {
268
+ respond(null);
269
+ return;
270
+ }
271
+ void detector.detect(message.bitmap, message.streamWidth, message.streamHeight).then(respond, () => respond(null));
272
+ }
273
+ };
274
+ }
275
+ if (typeof self !== "undefined" && typeof self.importScripts === "function") {
276
+ installWorkerDetectorHost(self);
277
+ }
278
+ var WorkerDetectorProxy = class _WorkerDetectorProxy {
279
+ constructor(worker, ready) {
280
+ this.pending = /* @__PURE__ */ new Map();
281
+ this.nextId = 1;
282
+ this.worker = worker;
283
+ this.tier = ready.tier;
284
+ if (ready.fallbackReason !== void 0) this.fallbackReason = ready.fallbackReason;
285
+ worker.addEventListener("message", (event) => {
286
+ const message = event.data;
287
+ if (message.v !== WORKER_PROTOCOL_VERSION || message.type !== "result") return;
288
+ const resolve = this.pending.get(message.id);
289
+ this.pending.delete(message.id);
290
+ resolve?.(message.observation);
291
+ });
292
+ }
293
+ static create(worker, options, readyTimeoutMs = 1e4) {
294
+ return new Promise((resolve, reject) => {
295
+ const timer = setTimeout(() => {
296
+ worker.removeEventListener("message", onReady);
297
+ worker.terminate();
298
+ reject(new Error(`worker detector not ready within ${readyTimeoutMs}ms`));
299
+ }, readyTimeoutMs);
300
+ const onReady = (event) => {
301
+ const message = event.data;
302
+ if (message.v !== WORKER_PROTOCOL_VERSION || message.type !== "ready") return;
303
+ clearTimeout(timer);
304
+ worker.removeEventListener("message", onReady);
305
+ resolve(new _WorkerDetectorProxy(worker, message));
306
+ };
307
+ worker.addEventListener("message", onReady);
308
+ const init = { v: WORKER_PROTOCOL_VERSION, type: "init", options };
309
+ worker.postMessage(init);
310
+ });
311
+ }
312
+ async detect(source, sourceWidth, sourceHeight) {
313
+ let bitmap;
314
+ try {
315
+ if (typeof ImageBitmap !== "undefined" && source instanceof ImageBitmap) {
316
+ bitmap = source;
317
+ } else if (typeof createImageBitmap === "function") {
318
+ bitmap = await createImageBitmap(source);
319
+ } else {
320
+ bitmap = source;
321
+ }
322
+ } catch {
323
+ return null;
324
+ }
325
+ const id = this.nextId;
326
+ this.nextId += 1;
327
+ return new Promise((resolve) => {
328
+ this.pending.set(id, resolve);
329
+ const message = {
330
+ v: WORKER_PROTOCOL_VERSION,
331
+ type: "detect",
332
+ id,
333
+ bitmap,
334
+ streamWidth: sourceWidth,
335
+ streamHeight: sourceHeight
336
+ };
337
+ this.worker.postMessage(message, [bitmap]);
338
+ });
339
+ }
340
+ destroy() {
341
+ for (const resolve of this.pending.values()) resolve(null);
342
+ this.pending.clear();
343
+ this.worker.terminate();
344
+ }
345
+ };
346
+
347
+ export {
348
+ DEFAULT_V3_ASSET_BASE_URL,
349
+ DEFAULT_TIER_ORDER,
350
+ DetectorLadderError,
351
+ intrinsicSize,
352
+ toStreamPixels,
353
+ createDetector,
354
+ WORKER_PROTOCOL_VERSION,
355
+ workerOffloadSupported,
356
+ installWorkerDetectorHost,
357
+ WorkerDetectorProxy
358
+ };
359
+ //# sourceMappingURL=chunk-NWF46H7K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/v3/detector-blaze.ts","../src/core/v3/detector-native.ts","../src/core/v3/detector.ts","../src/core/v3/detect-worker.ts"],"sourcesContent":["// BlazeFace short-range tier (Task 7) — MediaPipe tasks-vision FaceDetector,\n// the modern default: strong on large/close faces, ~15–40ms on low-end\n// WASM+SIMD. tasks-vision is imported LAZILY (only when this tier is\n// selected) and every asset is SELF-HOSTED under the v3 asset base (hosted\n// CSP is connect-src 'self' — no CDN): the wasm fileset via\n// FilesetResolver.forVisionTasks(base) and the model at\n// `${base}/blaze_face_short_range.tflite` (synced by\n// identity-hosted/scripts/sync-models-v3.mjs, sha256-manifested).\n//\n// DELEGATE = 'CPU' (2026-07-09, capture-v3 hardening — the load-bearing fix):\n// tasks-vision defaults `baseOptions.delegate` to the WebGL/GPU delegate,\n// which HANGS on iOS Safari — every field capture blew the 10s init deadline\n// (`blaze:blaze_init_timeout`) and silently fell to the deprecated face-api\n// floor at ~2.4fps. BlazeFace short-range is tiny (~229 KB) and fast on the\n// WASM+SIMD CPU delegate, so we pin CPU UNCONDITIONALLY: reliability across\n// iOS Safari + Chrome beats the marginal desktop-GPU speedup (and the GPU\n// path is exactly the bug). A GPU-first-with-CPU-fallback dance would only\n// re-introduce the hang risk inside the init budget for no real win.\nimport { type Detector, type FaceObservation, intrinsicSize, toStreamPixels } from \"./detector\";\n\n/** createFromOptions must resolve within this window or the ladder falls\n * through with `blaze_init_timeout` (overridable via CreateDetectorOptions).\n * Raised 5s→10s: on iOS Safari the COLD wasm SIMD compile of tasks-vision can\n * exceed 5s on the FIRST (unwarmed) capture. Since the face-api floor was\n * dropped (2026-07-09), blowing this deadline is now a HARD capture failure\n * (error{kind:'models'} — retryable), not a silent degrade, so the CPU\n * delegate above (which makes init actually complete on iOS) is what keeps\n * this deadline reachable. The hosted warm path\n * (identity-hosted/lib/warm-models.ts) pre-warms this tier in idle time so\n * capture usually reuses a hot instance; this larger deadline only covers the\n * un-warmed fallback (Data-Saver, deep-link straight to liveness). */\nexport const BLAZE_INIT_TIMEOUT_MS = 10_000;\n\ninterface TasksVisionDetection {\n boundingBox?: { originX: number; originY: number; width: number; height: number };\n categories: Array<{ score: number }>;\n /** NORMALIZED to the input image (0..1) — tasks-vision docs; index 0 =\n * right eye, 1 = left eye (SUBJECT perspective). We re-order by image-x\n * (the FaceObservation convention), so the doc order is informative only. */\n keypoints?: Array<{ x: number; y: number }>;\n}\n\ninterface TasksVisionFaceDetector {\n detect(source: unknown): { detections: TasksVisionDetection[] };\n close(): void;\n}\n\nexport class BlazeDetector implements Detector {\n readonly tier = \"blaze\" as const;\n readonly fallbackReason?: string;\n private readonly inner: TasksVisionFaceDetector;\n\n private constructor(inner: TasksVisionFaceDetector, fallbackReason?: string) {\n this.inner = inner;\n if (fallbackReason !== undefined) this.fallbackReason = fallbackReason;\n }\n\n static async create(assetBaseUrl: string, fallbackReason?: string): Promise<BlazeDetector> {\n const vision = await import(\"@mediapipe/tasks-vision\");\n const fileset = await vision.FilesetResolver.forVisionTasks(assetBaseUrl);\n const inner = (await vision.FaceDetector.createFromOptions(fileset, {\n baseOptions: {\n modelAssetPath: `${assetBaseUrl}/blaze_face_short_range.tflite`,\n // CPU delegate — the iOS-Safari-safe path (see header). NEVER 'GPU'.\n delegate: \"CPU\",\n },\n runningMode: \"IMAGE\",\n })) as unknown as TasksVisionFaceDetector;\n return new BlazeDetector(inner, fallbackReason);\n }\n\n async detect(\n source: CanvasImageSource,\n sourceWidth: number,\n sourceHeight: number,\n ): Promise<FaceObservation | null> {\n let detections: TasksVisionDetection[];\n try {\n detections = this.inner.detect(source).detections;\n } catch {\n return null; // never rejects mid-loop\n }\n const best = pickMostConfident(detections);\n if (best?.boundingBox === undefined) return null;\n const size = intrinsicSize(source);\n const eyes = extractEyes(best.keypoints, size);\n const observation: FaceObservation = {\n box: {\n x: best.boundingBox.originX,\n y: best.boundingBox.originY,\n width: best.boundingBox.width,\n height: best.boundingBox.height,\n },\n ...(eyes !== undefined ? { eyes } : {}),\n score: best.categories[0]?.score ?? 0,\n };\n return toStreamPixels(observation, source, sourceWidth, sourceHeight);\n }\n\n destroy(): void {\n try {\n this.inner.close();\n } catch {\n // wasm teardown failures are inert\n }\n }\n}\n\nfunction pickMostConfident(detections: TasksVisionDetection[]): TasksVisionDetection | undefined {\n let best: TasksVisionDetection | undefined;\n let bestScore = -1;\n for (const detection of detections) {\n const score = detection.categories[0]?.score ?? 0;\n if (score > bestScore) {\n best = detection;\n bestScore = score;\n }\n }\n return best;\n}\n\n/** Keypoints 0/1 (the eye pair) — normalized → SOURCE px, ordered by image-x. */\nfunction extractEyes(\n keypoints: Array<{ x: number; y: number }> | undefined,\n size: { width: number; height: number },\n): FaceObservation[\"eyes\"] | undefined {\n const a = keypoints?.[0];\n const b = keypoints?.[1];\n if (a === undefined || b === undefined) return undefined;\n const toSource = (p: { x: number; y: number }) => ({ x: p.x * size.width, y: p.y * size.height });\n const [pa, pb] = [toSource(a), toSource(b)];\n return pa.x <= pb.x ? { left: pa, right: pb } : { left: pb, right: pa };\n}\n","// Native FaceDetector tier (Task 6) — the Shape Detection API: free,\n// hardware-backed where implemented (Chrome/Android, some WebViews). Probed\n// with a 1-frame smoke detect because several browsers EXPOSE the constructor\n// but throw `Face detection service unavailable` at runtime.\nimport { type Detector, type FaceObservation, toStreamPixels } from \"./detector\";\n\ninterface NativeLandmark {\n type: string;\n locations: Array<{ x: number; y: number }>;\n}\n\ninterface NativeDetectedFace {\n boundingBox: { x: number; y: number; width: number; height: number };\n landmarks?: NativeLandmark[];\n}\n\nexport interface NativeFaceDetectorLike {\n detect(source: CanvasImageSource): Promise<NativeDetectedFace[]>;\n}\n\ntype NativeGlobal = typeof globalThis & {\n FaceDetector?: new (options: { fastMode: boolean; maxDetectedFaces: number }) => NativeFaceDetectorLike;\n};\n\nfunction smokeSource(): CanvasImageSource {\n if (typeof OffscreenCanvas === \"function\") return new OffscreenCanvas(2, 2);\n const canvas = document.createElement(\"canvas\");\n canvas.width = 2;\n canvas.height = 2;\n return canvas;\n}\n\n/** Constructor + 1-frame smoke detect (must not throw; result irrelevant). */\nexport async function probeNativeDetector(): Promise<\n { ok: true; instance: NativeFaceDetectorLike } | { ok: false; reason: string }\n> {\n const ctor = (globalThis as NativeGlobal).FaceDetector;\n if (typeof ctor !== \"function\") return { ok: false, reason: \"unsupported\" };\n try {\n const instance = new ctor({ fastMode: true, maxDetectedFaces: 1 });\n await instance.detect(smokeSource());\n return { ok: true, instance };\n } catch (err) {\n return { ok: false, reason: `smoke_failed:${err instanceof Error ? err.message : String(err)}` };\n }\n}\n\nexport class NativeDetector implements Detector {\n readonly tier = \"native\" as const;\n readonly fallbackReason?: string;\n private readonly inner: NativeFaceDetectorLike;\n\n constructor(inner: NativeFaceDetectorLike, fallbackReason?: string) {\n this.inner = inner;\n if (fallbackReason !== undefined) this.fallbackReason = fallbackReason;\n }\n\n async detect(\n source: CanvasImageSource,\n sourceWidth: number,\n sourceHeight: number,\n ): Promise<FaceObservation | null> {\n let faces: NativeDetectedFace[];\n try {\n faces = await this.inner.detect(source);\n } catch {\n return null; // never rejects mid-loop\n }\n const face = faces[0];\n if (face === undefined) return null;\n const eyes = extractEyes(face.landmarks);\n const observation: FaceObservation = {\n box: {\n x: face.boundingBox.x,\n y: face.boundingBox.y,\n width: face.boundingBox.width,\n height: face.boundingBox.height,\n },\n ...(eyes !== undefined ? { eyes } : {}),\n // The Shape Detection API exposes no confidence — the smoke-tested\n // native tier treats any detection as confident.\n score: 1,\n };\n return toStreamPixels(observation, source, sourceWidth, sourceHeight);\n }\n\n destroy(): void {\n // The native detector holds no resources the page must release.\n }\n}\n\n/** Two `eye` landmarks → {left, right} by image-x (see FaceObservation). */\nfunction extractEyes(\n landmarks: NativeLandmark[] | undefined,\n): FaceObservation[\"eyes\"] | undefined {\n if (landmarks === undefined) return undefined;\n const eyePoints = landmarks\n .filter((landmark) => landmark.type === \"eye\")\n .map((landmark) => landmark.locations[0])\n .filter((point): point is { x: number; y: number } => point !== undefined);\n const [a, b] = eyePoints;\n if (a === undefined || b === undefined) return undefined;\n return a.x <= b.x ? { left: a, right: b } : { left: b, right: a };\n}\n","// v3 Detector abstraction (capture program Task 6) — one detection seam with\n// capability-probed tiers. NEW parallel module: nothing in the v1 core\n// imports from v3/*, and the calibration-pinned v1 gate math is untouched.\n//\n// Ladder (DEFAULT_TIER_ORDER): `native` (the browser's FaceDetector — free,\n// hardware-backed where it exists) → `blaze` (MediaPipe BlazeFace short-range,\n// Task 7). The legacy `faceapi` floor was DROPPED (2026-07-09, capture-v3\n// hardening): field telemetry showed BlazeFace never initialized (the GPU\n// delegate hung on iOS → `blaze_init_timeout`) so EVERY capture silently ran\n// the deprecated ~2.4fps face-api floor. With the CPU-delegate blaze fix,\n// face-api is dead weight — and a floor that masks a blaze regression is worse\n// than none, so the ladder is now native→blaze with NO fallback.\n//\n// Contract change: createDetector still never throws while a tier can be\n// built, but with no floor it REJECTS (DetectorLadderError) once the ladder is\n// exhausted — a CLEAN, non-hanging failure carrying the per-tier reasons.\n// CaptureSessionV3.run() maps that rejection to error{kind:'models'}\n// (retryable; the hosted UI renders a retry). A skipped tier still records a\n// `fallbackReason` (exposed on the returned Detector) so a native→blaze\n// fall-through stays observable in telemetry.\nimport { BLAZE_INIT_TIMEOUT_MS, BlazeDetector } from \"./detector-blaze\";\nimport { NativeDetector, probeNativeDetector } from \"./detector-native\";\n\nexport interface FaceObservation {\n /** Stream-pixel box. */\n box: { x: number; y: number; width: number; height: number };\n /** Stream-pixel eye centers when the tier provides them (native:\n * landmarks?, blaze: keypoints). Convention: `left` = smaller x in IMAGE\n * coordinates — gate math only uses the midpoint and |roll|, so the\n * person-left/image-left distinction never matters. */\n eyes?: { left: { x: number; y: number }; right: { x: number; y: number } };\n score: number;\n}\n\nexport interface Detector {\n readonly tier: \"native\" | \"blaze\";\n /** Why higher tiers were skipped (telemetry) — absent when the first\n * preference won. */\n readonly fallbackReason?: string;\n /** Detect on a (possibly downscaled) snapshot; `sourceWidth/Height` are the\n * STREAM dimensions the observation must be normalized to. Returns the\n * single most-confident face or null; NEVER rejects. */\n detect(\n source: CanvasImageSource,\n sourceWidth: number,\n sourceHeight: number,\n ): Promise<FaceObservation | null>;\n destroy(): void;\n}\n\nexport interface CreateDetectorOptions {\n /** v3 self-hosted assets base (tasks-vision wasm + blaze tflite) — hosted\n * serves `/models-v3`, a sha256-manifested directory. */\n v3AssetBaseUrl?: string;\n preferred?: Detector[\"tier\"][];\n /** Test/tuning override for the blaze init deadline. */\n blazeInitTimeoutMs?: number;\n}\n\nexport const DEFAULT_V3_ASSET_BASE_URL = \"/models-v3\";\n\nexport const DEFAULT_TIER_ORDER: Detector[\"tier\"][] = [\"native\", \"blaze\"];\n\n/** Thrown by createDetector when the ENTIRE tier ladder is exhausted. Since the\n * face-api floor was dropped (2026-07-09) there is no last resort, so blaze\n * failing on an already-native-less platform leaves NO detector. Carries the\n * joined per-tier reasons ('native:unsupported; blaze:blaze_init_timeout') as\n * `fallbackReason` for telemetry. Callers MUST map this to a retryable models\n * error — never a hang (CaptureSessionV3.run → error{kind:'models'}). */\nexport class DetectorLadderError extends Error {\n readonly fallbackReason: string;\n constructor(fallbackReason: string) {\n super(`no detector tier available (${fallbackReason})`);\n this.name = \"DetectorLadderError\";\n this.fallbackReason = fallbackReason;\n }\n}\n\n/** Intrinsic pixel size of a snapshot source (video → videoWidth, image →\n * naturalWidth, canvas/bitmap → width). Property-sniffed so worker-side\n * sources (OffscreenCanvas, ImageBitmap) work without instanceof. */\nexport function intrinsicSize(source: CanvasImageSource): { width: number; height: number } {\n const candidate = source as {\n videoWidth?: number;\n videoHeight?: number;\n naturalWidth?: number;\n naturalHeight?: number;\n width?: number | { baseVal?: { value?: number } };\n height?: number | { baseVal?: { value?: number } };\n };\n if (typeof candidate.videoWidth === \"number\" && candidate.videoWidth > 0) {\n return { width: candidate.videoWidth, height: candidate.videoHeight ?? 0 };\n }\n if (typeof candidate.naturalWidth === \"number\" && candidate.naturalWidth > 0) {\n return { width: candidate.naturalWidth, height: candidate.naturalHeight ?? 0 };\n }\n const width = typeof candidate.width === \"number\" ? candidate.width : 0;\n const height = typeof candidate.height === \"number\" ? candidate.height : 0;\n return { width, height };\n}\n\n/** Scale a source-space observation to stream px. Exported for adapters. */\nexport function toStreamPixels(\n observation: FaceObservation,\n source: CanvasImageSource,\n streamWidth: number,\n streamHeight: number,\n): FaceObservation {\n const size = intrinsicSize(source);\n const scaleX = size.width > 0 ? streamWidth / size.width : 1;\n const scaleY = size.height > 0 ? streamHeight / size.height : 1;\n const scalePoint = (p: { x: number; y: number }) => ({ x: p.x * scaleX, y: p.y * scaleY });\n return {\n box: {\n x: observation.box.x * scaleX,\n y: observation.box.y * scaleY,\n width: observation.box.width * scaleX,\n height: observation.box.height * scaleY,\n },\n ...(observation.eyes !== undefined\n ? { eyes: { left: scalePoint(observation.eyes.left), right: scalePoint(observation.eyes.right) } }\n : {}),\n score: observation.score,\n };\n}\n\nfunction describeError(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport async function createDetector(options: CreateDetectorOptions): Promise<Detector> {\n const order = options.preferred ?? DEFAULT_TIER_ORDER;\n const reasons: string[] = [];\n for (const tier of order) {\n if (tier === \"native\") {\n const probed = await probeNativeDetector();\n if (probed.ok) {\n return new NativeDetector(probed.instance, reasonOf(reasons));\n }\n reasons.push(`native:${probed.reason}`);\n continue;\n }\n // tier === \"blaze\": smoke test = createFromOptions resolving within the\n // deadline (wasm fetch/compile can hang on hostile networks — never stall\n // the ladder).\n try {\n return await withTimeout(\n BlazeDetector.create(\n options.v3AssetBaseUrl ?? DEFAULT_V3_ASSET_BASE_URL,\n reasonOf(reasons),\n ),\n options.blazeInitTimeoutMs ?? BLAZE_INIT_TIMEOUT_MS,\n );\n } catch (err) {\n reasons.push(\n err instanceof InitTimeoutError\n ? \"blaze:blaze_init_timeout\"\n : `blaze:${describeError(err)}`,\n );\n }\n }\n // Ladder exhausted — the face-api floor is gone, so there is NO fallback.\n // REJECT cleanly with the recorded reasons; CaptureSessionV3.run() maps this\n // to a retryable error{kind:'models'} (a bounded failure the hosted UI\n // retries) rather than hanging. NEVER return a null-yielding detector here.\n throw new DetectorLadderError(reasonOf(reasons) ?? \"no_tier_available\");\n}\n\nfunction reasonOf(reasons: string[]): string | undefined {\n return reasons.length > 0 ? reasons.join(\"; \") : undefined;\n}\n\nclass InitTimeoutError extends Error {}\n\nfunction withTimeout<T extends { destroy(): void }>(\n promise: Promise<T>,\n deadlineMs: number,\n): Promise<T> {\n return new Promise<T>((resolvePromise, rejectPromise) => {\n const timer = setTimeout(() => {\n rejectPromise(new InitTimeoutError(`init exceeded ${deadlineMs}ms`));\n // A late resolution after the deadline is an orphan (wasm/GPU\n // delegate) — release it instead of leaking until navigation.\n promise.then(\n (value) => value.destroy(),\n () => undefined,\n );\n }, deadlineMs);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolvePromise(value);\n },\n (err) => {\n clearTimeout(timer);\n rejectPromise(err);\n },\n );\n });\n}\n","// Worker offload (Task 9) — detection off the main thread when the platform\n// allows (OffscreenCanvas + createImageBitmap + Worker). BOTH protocol sides\n// live in this file so the tiny versioned protocol (v:1) can never drift:\n// - installWorkerDetectorHost: runs INSIDE the worker (auto-installed when\n// this module loads in a DedicatedWorkerGlobalScope; tsup builds it as\n// its own entry so hosts can `new Worker(new URL(...))` the dist file);\n// - WorkerDetectorProxy: main-thread Detector implementation that posts\n// ImageBitmap frames as transferables and resolves results by id.\nimport {\n type CreateDetectorOptions,\n createDetector as defaultCreateDetector,\n type Detector,\n type FaceObservation,\n} from \"./detector\";\n\nexport const WORKER_PROTOCOL_VERSION = 1;\n\ninterface InitMessage {\n v: typeof WORKER_PROTOCOL_VERSION;\n type: \"init\";\n options: CreateDetectorOptions;\n}\ninterface DetectMessage {\n v: typeof WORKER_PROTOCOL_VERSION;\n type: \"detect\";\n id: number;\n bitmap: ImageBitmap;\n streamWidth: number;\n streamHeight: number;\n}\ninterface ReadyMessage {\n v: typeof WORKER_PROTOCOL_VERSION;\n type: \"ready\";\n tier: Detector[\"tier\"];\n fallbackReason?: string;\n}\ninterface ResultMessage {\n v: typeof WORKER_PROTOCOL_VERSION;\n type: \"result\";\n id: number;\n observation: FaceObservation | null;\n}\n\ntype ToWorker = InitMessage | DetectMessage;\ntype ToMain = ReadyMessage | ResultMessage;\n\n/** The slice of Worker/scope surface the protocol needs — test-stubbable. */\nexport interface WorkerScopeLike {\n onmessage: ((event: { data: unknown }) => void) | null;\n postMessage(data: unknown): void;\n}\nexport interface WorkerLike {\n postMessage(data: unknown, transfer?: Transferable[]): void;\n addEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n removeEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n terminate(): void;\n}\n\n/** Feature gate: bitmap capture + transfer + workers must all exist. */\nexport function workerOffloadSupported(): boolean {\n return (\n typeof OffscreenCanvas === \"function\" &&\n typeof createImageBitmap === \"function\" &&\n typeof Worker === \"function\"\n );\n}\n\n/** Worker-side message host. `deps.createDetector` is a test seam. */\nexport function installWorkerDetectorHost(\n scope: WorkerScopeLike,\n deps: { createDetector?: (options: CreateDetectorOptions) => Promise<Detector> } = {},\n): void {\n const create = deps.createDetector ?? defaultCreateDetector;\n let detector: Detector | null = null;\n scope.onmessage = (event) => {\n const message = event.data as ToWorker;\n if (message.v !== WORKER_PROTOCOL_VERSION) {\n // Ignore unknown versions — but never strand a transferred bitmap.\n (message as { bitmap?: { close?: () => void } }).bitmap?.close?.();\n return;\n }\n if (message.type === \"init\") {\n void create(message.options).then((created) => {\n detector = created;\n const ready: ReadyMessage = {\n v: WORKER_PROTOCOL_VERSION,\n type: \"ready\",\n tier: created.tier,\n ...(created.fallbackReason !== undefined\n ? { fallbackReason: created.fallbackReason }\n : {}),\n };\n scope.postMessage(ready);\n });\n return;\n }\n if (message.type === \"detect\") {\n const respond = (observation: FaceObservation | null) => {\n const result: ResultMessage = {\n v: WORKER_PROTOCOL_VERSION,\n type: \"result\",\n id: message.id,\n observation,\n };\n scope.postMessage(result);\n (message.bitmap as { close?: () => void }).close?.();\n };\n if (detector === null) {\n respond(null);\n return;\n }\n void detector\n .detect(message.bitmap, message.streamWidth, message.streamHeight)\n .then(respond, () => respond(null));\n }\n };\n}\n\n// Auto-install when this module IS the worker entry (DedicatedWorkerGlobalScope\n// has importScripts; window does not). Inert in main-thread bundles/tests.\ndeclare const self: (WorkerScopeLike & { importScripts?: unknown }) | undefined;\nif (typeof self !== \"undefined\" && typeof self.importScripts === \"function\") {\n installWorkerDetectorHost(self);\n}\n\n/** Main-side Detector over the worker protocol. Same interface as the\n * in-thread detectors — Task 10's engine cannot tell the difference. */\nexport class WorkerDetectorProxy implements Detector {\n readonly tier: Detector[\"tier\"];\n readonly fallbackReason?: string;\n private readonly worker: WorkerLike;\n private readonly pending = new Map<number, (observation: FaceObservation | null) => void>();\n private nextId = 1;\n\n private constructor(worker: WorkerLike, ready: ReadyMessage) {\n this.worker = worker;\n this.tier = ready.tier;\n if (ready.fallbackReason !== undefined) this.fallbackReason = ready.fallbackReason;\n worker.addEventListener(\"message\", (event) => {\n const message = event.data as ToMain;\n if (message.v !== WORKER_PROTOCOL_VERSION || message.type !== \"result\") return;\n const resolve = this.pending.get(message.id);\n this.pending.delete(message.id);\n resolve?.(message.observation);\n });\n }\n\n static create(\n worker: WorkerLike,\n options: CreateDetectorOptions,\n readyTimeoutMs = 10_000,\n ): Promise<WorkerDetectorProxy> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n worker.removeEventListener(\"message\", onReady);\n worker.terminate();\n reject(new Error(`worker detector not ready within ${readyTimeoutMs}ms`));\n }, readyTimeoutMs);\n const onReady = (event: { data: unknown }) => {\n const message = event.data as ToMain;\n if (message.v !== WORKER_PROTOCOL_VERSION || message.type !== \"ready\") return;\n clearTimeout(timer);\n worker.removeEventListener(\"message\", onReady);\n resolve(new WorkerDetectorProxy(worker, message));\n };\n worker.addEventListener(\"message\", onReady);\n const init: InitMessage = { v: WORKER_PROTOCOL_VERSION, type: \"init\", options };\n worker.postMessage(init);\n });\n }\n\n async detect(\n source: CanvasImageSource,\n sourceWidth: number,\n sourceHeight: number,\n ): Promise<FaceObservation | null> {\n // Only ImageBitmap is Transferable — convert other sources (video,\n // canvas) via createImageBitmap (the feature gate guarantees it). A\n // failed conversion yields null, never a DataCloneError.\n let bitmap: ImageBitmap;\n try {\n if (typeof ImageBitmap !== \"undefined\" && source instanceof ImageBitmap) {\n bitmap = source;\n } else if (typeof createImageBitmap === \"function\") {\n bitmap = await createImageBitmap(source as ImageBitmapSource);\n } else {\n // No conversion surface (stubbed environments) — trust the caller.\n bitmap = source as ImageBitmap;\n }\n } catch {\n return null;\n }\n const id = this.nextId;\n this.nextId += 1;\n return new Promise((resolve) => {\n this.pending.set(id, resolve);\n const message: DetectMessage = {\n v: WORKER_PROTOCOL_VERSION,\n type: \"detect\",\n id,\n bitmap,\n streamWidth: sourceWidth,\n streamHeight: sourceHeight,\n };\n this.worker.postMessage(message, [bitmap]);\n });\n }\n\n destroy(): void {\n // Settle in-flight detects (an awaiting scheduler run must not hang\n // busy=true forever) before tearing the worker down.\n for (const resolve of this.pending.values()) resolve(null);\n this.pending.clear();\n this.worker.terminate();\n }\n}\n"],"mappings":"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","names":["extractEyes"]}