@sekyro/web 0.1.0-rc.2 → 0.1.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/{capture-session-v3-CAoWM8iT.d.cts → capture-session-v3-DEInCOUg.d.cts} +37 -7
- package/dist/{capture-session-v3-BJ8ciaWl.d.ts → capture-session-v3-uXJr1Wlm.d.ts} +37 -7
- package/dist/{chunk-LOTAJDRE.js → chunk-EWRNPKTR.js} +38 -9
- package/dist/chunk-EWRNPKTR.js.map +1 -0
- package/dist/{chunk-BPYVZUJA.js → chunk-TBANUT3X.js} +39 -6
- package/dist/chunk-TBANUT3X.js.map +1 -0
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/core/index.cjs +37 -8
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/{create-verification-EnZ13uY4.d.cts → create-verification-Ce1y5kJR.d.ts} +26 -3
- package/dist/{create-verification-CjLnJ0w6.d.ts → create-verification-Dkb2tXuY.d.cts} +26 -3
- package/dist/{hosted-client-BpvBqu9f.d.cts → hosted-client-DWzCRYJK.d.cts} +14 -1
- package/dist/{hosted-client-BpvBqu9f.d.ts → hosted-client-DWzCRYJK.d.ts} +14 -1
- package/dist/index.cjs +74 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/internal/index.cjs +74 -12
- package/dist/internal/index.cjs.map +1 -1
- package/dist/internal/index.d.cts +4 -4
- package/dist/internal/index.d.ts +4 -4
- package/dist/internal/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-BPYVZUJA.js.map +0 -1
- package/dist/chunk-LOTAJDRE.js.map +0 -1
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
CaptureSessionV3,
|
|
3
3
|
detectIsMobile,
|
|
4
4
|
isMediaRecorderSupported
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EWRNPKTR.js";
|
|
6
6
|
import {
|
|
7
7
|
HostedApiError,
|
|
8
8
|
HostedClient,
|
|
@@ -116,6 +116,14 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
116
116
|
let animations = "full";
|
|
117
117
|
let recordingStartedAt = null;
|
|
118
118
|
let frozenAt = null;
|
|
119
|
+
let frameWidth = 0;
|
|
120
|
+
let frameHeight = 0;
|
|
121
|
+
const latchFrameSize = () => {
|
|
122
|
+
if (options.video.videoWidth > 0 && options.video.videoHeight > 0) {
|
|
123
|
+
frameWidth = options.video.videoWidth;
|
|
124
|
+
frameHeight = options.video.videoHeight;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
119
127
|
const setState = (next) => {
|
|
120
128
|
state = next;
|
|
121
129
|
emitter.emit("state", next);
|
|
@@ -136,10 +144,13 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
136
144
|
faceDetected = false;
|
|
137
145
|
recordingStartedAt = null;
|
|
138
146
|
frozenAt = null;
|
|
147
|
+
frameWidth = 0;
|
|
148
|
+
frameHeight = 0;
|
|
139
149
|
}
|
|
140
150
|
setState(next);
|
|
141
151
|
syncProfile();
|
|
142
152
|
if (next === "recording") {
|
|
153
|
+
latchFrameSize();
|
|
143
154
|
recordingStartedAt = now();
|
|
144
155
|
hint = "hold";
|
|
145
156
|
progress = 1;
|
|
@@ -147,6 +158,7 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
147
158
|
}
|
|
148
159
|
}),
|
|
149
160
|
engine.on("checks", (payload) => {
|
|
161
|
+
latchFrameSize();
|
|
150
162
|
progress = payload.progress;
|
|
151
163
|
faceDetected = payload.factors.presence > 0;
|
|
152
164
|
emitGate();
|
|
@@ -166,11 +178,16 @@ function createLivenessCaptureInternal(options, internals = {}) {
|
|
|
166
178
|
keyframe: artifacts.keyframe,
|
|
167
179
|
...artifacts.clip !== void 0 ? { clip: artifacts.clip } : {},
|
|
168
180
|
...artifacts.clipMimeType !== void 0 ? { clipMime: artifacts.clipMimeType } : {},
|
|
169
|
-
width: options.video.videoWidth,
|
|
170
|
-
height: options.video.videoHeight,
|
|
181
|
+
width: options.video.videoWidth || frameWidth,
|
|
182
|
+
height: options.video.videoHeight || frameHeight,
|
|
171
183
|
...clipMs !== void 0 ? { clipMs } : {},
|
|
172
184
|
captureChannel: detectIsMobile() ? "mobile" : "desktop",
|
|
173
|
-
...degraded ? { degraded: "NO_RECORDER" } : {}
|
|
185
|
+
...degraded ? { degraded: "NO_RECORDER" } : {},
|
|
186
|
+
engine: "v3",
|
|
187
|
+
// Face-capture timing: the engine's marks, forwarded flat (absent → absent).
|
|
188
|
+
...artifacts.timing?.cameraReadyMs !== void 0 ? { cameraReadyMs: artifacts.timing.cameraReadyMs } : {},
|
|
189
|
+
...artifacts.timing?.faceAcquireMs !== void 0 ? { faceAcquireMs: artifacts.timing.faceAcquireMs } : {},
|
|
190
|
+
...artifacts.timing?.captureMs !== void 0 ? { captureMs: artifacts.timing.captureMs } : {}
|
|
174
191
|
});
|
|
175
192
|
}),
|
|
176
193
|
engine.on("error", ({ kind, retryable }) => {
|
|
@@ -426,7 +443,12 @@ function buildLivenessSubmit(deps, clip, bareMime) {
|
|
|
426
443
|
deps.onPhase("checking");
|
|
427
444
|
return client.submitCheck(
|
|
428
445
|
"liveness",
|
|
429
|
-
{
|
|
446
|
+
{
|
|
447
|
+
clipKey,
|
|
448
|
+
attestation,
|
|
449
|
+
captureChannel: deps.artifacts.captureChannel,
|
|
450
|
+
capture: captureInputOf(deps.artifacts)
|
|
451
|
+
},
|
|
430
452
|
{ idempotencyKey }
|
|
431
453
|
);
|
|
432
454
|
},
|
|
@@ -437,6 +459,17 @@ function buildLivenessSubmit(deps, clip, bareMime) {
|
|
|
437
459
|
}
|
|
438
460
|
);
|
|
439
461
|
}
|
|
462
|
+
function captureInputOf(artifacts) {
|
|
463
|
+
return {
|
|
464
|
+
...artifacts.engine !== void 0 ? { engine: artifacts.engine } : {},
|
|
465
|
+
width: artifacts.width,
|
|
466
|
+
height: artifacts.height,
|
|
467
|
+
...artifacts.clipMs !== void 0 ? { clipMs: artifacts.clipMs } : {},
|
|
468
|
+
...artifacts.cameraReadyMs !== void 0 ? { cameraReadyMs: artifacts.cameraReadyMs } : {},
|
|
469
|
+
...artifacts.faceAcquireMs !== void 0 ? { faceAcquireMs: artifacts.faceAcquireMs } : {},
|
|
470
|
+
...artifacts.captureMs !== void 0 ? { captureMs: artifacts.captureMs } : {}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
440
473
|
function buildFaceMatchSubmit(deps, keyframe) {
|
|
441
474
|
let imageKey = null;
|
|
442
475
|
return () => runSubmission(deps, async (client, idempotencyKey) => {
|
|
@@ -849,4 +882,4 @@ export {
|
|
|
849
882
|
createVerification,
|
|
850
883
|
createVerificationInternal
|
|
851
884
|
};
|
|
852
|
-
//# sourceMappingURL=chunk-
|
|
885
|
+
//# sourceMappingURL=chunk-TBANUT3X.js.map
|
|
@@ -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 // Frame size latched while the stream is LIVE: the engine stops the tracks\n // before it emits `artifacts`, and iOS Safari then reports videoWidth /\n // videoHeight as 0 (field: comparison records with width 0 / height 0).\n let frameWidth = 0;\n let frameHeight = 0;\n const latchFrameSize = (): void => {\n if (options.video.videoWidth > 0 && options.video.videoHeight > 0) {\n frameWidth = options.video.videoWidth;\n frameHeight = options.video.videoHeight;\n }\n };\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 frameWidth = 0;\n frameHeight = 0;\n }\n setState(next);\n syncProfile();\n if (next === \"recording\") {\n latchFrameSize();\n recordingStartedAt = now();\n hint = \"hold\";\n progress = 1;\n emitGate();\n }\n }),\n engine.on(\"checks\", (payload) => {\n latchFrameSize(); // the engine only ticks on a live frame (videoWidth > 0)\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 || frameWidth,\n height: options.video.videoHeight || frameHeight,\n ...(clipMs !== undefined ? { clipMs } : {}),\n captureChannel: detectIsMobile() ? \"mobile\" : \"desktop\",\n ...(degraded ? { degraded: \"NO_RECORDER\" as const } : {}),\n engine: \"v3\",\n // Face-capture timing: the engine's marks, forwarded flat (absent → absent).\n ...(artifacts.timing?.cameraReadyMs !== undefined\n ? { cameraReadyMs: artifacts.timing.cameraReadyMs }\n : {}),\n ...(artifacts.timing?.faceAcquireMs !== undefined\n ? { faceAcquireMs: artifacts.timing.faceAcquireMs }\n : {}),\n ...(artifacts.timing?.captureMs !== undefined\n ? { captureMs: artifacts.timing.captureMs }\n : {}),\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, driven by the CURRENT hosted frontier: when `liveness`\n// is the frontier → upload the clip → attestation → submit `liveness`, then\n// `face_match` from this capture's keyframe; when liveness already settled and\n// `face_match` is the frontier → keyframe + `face_match` only; nothing the SDK\n// can drive → settled (the caller maps the view). A RESYNC 409\n// (check_already_finalized / check_out_of_order / stale frontier) re-GETs the\n// view and RE-ENTERS the same frontier logic (bounded) instead of giving up —\n// a liveness that passed under an earlier run still gets its face_match.\n// 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 LivenessCaptureInput,\n SessionView,\n} from \"../client/types\";\nimport { frontierStep, 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 * Bound on frontier re-entries after a RESYNC code per capture (each re-entry\n * re-GETs the view; each executable check is submitted at most once per capture).\n */\nexport const RESYNC_REENTRY_MAX = 3;\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 /** Re-GET the view from the server (after a RESYNC code) and fold it in. */\n refreshView(): Promise<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 /** The verification was destroyed (the only thing that cancels a chain). */\n isCancelled(): boolean;\n}\n\nexport type LivenessFlowOutcome =\n | { kind: \"settled\" }\n | { kind: \"resync\" }\n | { kind: \"unavailable\"; reason: SdkFailureReason }\n | { kind: \"cancelled\" };\n\n/** Liveness states after which THIS capture's keyframe may feed face_match. */\nconst LIVENESS_CHAINABLE: ReadonlySet<string> = new Set([\"passed\", \"review\", \"processing\", \"skipped\"]);\n\n/**\n * Run the biometric submissions the CURRENT frontier allows, re-evaluating\n * after every submit and after every RESYNC re-GET:\n * - frontier `liveness` → clip upload + attestation + `liveness` (once per capture);\n * - frontier `face_match` → keyframe upload + `face_match` (once per capture),\n * allowed when liveness 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 chainable from this capture\n * (passed / review / processing / skipped — or absent);\n * - anything else (terminal view, a non-biometric frontier, a check this\n * capture already submitted) → `settled` for the caller to map the view.\n * A RESYNC code re-GETs the view and re-enters (at most {@link RESYNC_REENTRY_MAX}\n * times); the final RESYNC surfaces as `resync` for the caller to map.\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 // Per-capture executors: their closure caches (promoted media key,\n // attestation) survive a same-key transport replay; each is run at most once.\n const submitLiveness = buildLivenessSubmit(deps, clip, bareMime);\n const submitFaceMatch = buildFaceMatchSubmit(deps, artifacts.keyframe);\n let livenessSubmitted = false;\n let faceMatchSubmitted = false;\n let livenessChainable = true;\n let resyncs = 0;\n\n for (;;) {\n if (deps.isCancelled()) return { kind: \"cancelled\" };\n const view = deps.getView();\n if (isTerminalView(view)) return { kind: \"settled\" };\n const frontier = frontierStep(view)?.checkType;\n const next =\n frontier === \"liveness\" && !livenessSubmitted\n ? \"liveness\"\n : frontier === \"face_match\" && !faceMatchSubmitted && livenessChainable\n ? \"face_match\"\n : null;\n if (next === null) return { kind: \"settled\" };\n try {\n if (next === \"liveness\") {\n livenessSubmitted = true;\n const response = await submitLiveness();\n if (deps.isCancelled()) return { kind: \"cancelled\" };\n deps.applySubmit(response, \"liveness\");\n const state = stepOf(deps.getView(), \"liveness\")?.state;\n livenessChainable = state === undefined || LIVENESS_CHAINABLE.has(state);\n } else {\n faceMatchSubmitted = true;\n const response = await submitFaceMatch();\n if (deps.isCancelled()) return { kind: \"cancelled\" };\n deps.applySubmit(response, \"face_match\");\n }\n } catch (err) {\n if (deps.isCancelled()) return { kind: \"cancelled\" };\n const outcome = classifyFailure(err);\n if (outcome.kind !== \"resync\" || resyncs >= RESYNC_REENTRY_MAX) return outcome;\n resyncs += 1;\n // The server moved past what we hold: re-GET and re-enter from truth.\n await deps.refreshView();\n }\n }\n}\n\nfunction buildLivenessSubmit(\n deps: LivenessFlowDeps,\n clip: Blob,\n bareMime: string,\n): () => Promise<SessionView> {\n let clipKey: string | null = null;\n let attestation: AttestationInput | null = null;\n return () =>\n 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 {\n clipKey,\n attestation,\n captureChannel: deps.artifacts.captureChannel,\n capture: captureInputOf(deps.artifacts),\n },\n { idempotencyKey },\n );\n },\n {\n onAttestationInvalid: () => {\n attestation = null;\n },\n },\n );\n}\n\n/**\n * The `input.capture` block of the liveness submission: the engine id, the\n * stream dimensions, the clip length and the face-capture timing from the\n * `captured` event — exactly the backend's capture-metadata allowlist\n * (identity-backend `capture-metadata.ts`). Never media, never tokens.\n */\nexport function captureInputOf(artifacts: CapturedArtifacts): LivenessCaptureInput {\n return {\n ...(artifacts.engine !== undefined ? { engine: artifacts.engine } : {}),\n width: artifacts.width,\n height: artifacts.height,\n ...(artifacts.clipMs !== undefined ? { clipMs: artifacts.clipMs } : {}),\n ...(artifacts.cameraReadyMs !== undefined ? { cameraReadyMs: artifacts.cameraReadyMs } : {}),\n ...(artifacts.faceAcquireMs !== undefined ? { faceAcquireMs: artifacts.faceAcquireMs } : {}),\n ...(artifacts.captureMs !== undefined ? { captureMs: artifacts.captureMs } : {}),\n };\n}\n\nfunction buildFaceMatchSubmit(deps: LivenessFlowDeps, keyframe: Blob): () => Promise<SessionView> {\n let imageKey: string | null = null;\n return () =>\n 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}\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 // Submission chains are SERIALIZED: a new capture (incl. a host remount that\n // destroyed the old handle mid-upload) never abandons an in-flight chain — it\n // queues behind it and then evaluates the CURRENT frontier. Only destroy() of\n // the verification cancels. Handle side effects (flow states, auto re-arm)\n // reach the handle only while it is alive and still on the attempt that\n // produced the artifacts.\n interface BoundCapture {\n capture: LivenessCaptureInternalHandle;\n alive: boolean;\n /** Engine attempt counter (bumped on every `starting`). */\n attempt: number;\n }\n let active: BoundCapture | null = null;\n let chain: Promise<void> = Promise.resolve();\n let chainDepth = 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 bound: BoundCapture,\n artifacts: CapturedArtifacts,\n capturedAttempt: number,\n ): Promise<void> => {\n const { capture } = bound;\n const isCancelled = (): boolean => destroyed;\n // The handle still shows the attempt these artifacts came from (not\n // destroyed by a remount, no retry()/switchCamera() since).\n const ownsHandle = (): boolean => bound.alive && bound.attempt === capturedAttempt;\n const setPhase = (phase: \"uploading\" | \"checking\" | \"done\"): void => {\n if (ownsHandle()) capture.setFlowState(phase);\n };\n const finish = (result: VerificationResult): void => {\n if (isCancelled()) return;\n emitResult(result);\n if (!ownsHandle()) return;\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 setPhase(\"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 refreshView,\n applySubmit,\n onPhase: setPhase,\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 enqueueFlow = (bound: BoundCapture, artifacts: CapturedArtifacts): void => {\n const capturedAttempt = bound.attempt;\n if (chainDepth > 0 && bound.alive) {\n // Queued behind an in-flight chain: the host sees `uploading` meanwhile.\n bound.capture.setFlowState(\"uploading\");\n }\n chainDepth += 1;\n chain = chain\n .then(() => runFlow(bound, artifacts, capturedAttempt))\n .finally(() => {\n chainDepth -= 1;\n })\n // runFlow never rejects (it maps failures to results); a rejection must\n // still never poison the chain for the captures queued after it.\n .catch(() => undefined);\n };\n\n const liveness = (captureOptions: LivenessCaptureOptions): LivenessCaptureHandle => {\n // Camera teardown of the previous handle (its chain, if any, survives).\n active?.capture.handle.destroy();\n if (active !== null) active.alive = false;\n const capture = createLivenessCaptureInternal(captureOptions, internals.capture);\n const bound: BoundCapture = { capture, alive: true, attempt: 0 };\n active = bound;\n capture.handle.on(\"state\", (state) => {\n // A fresh engine attempt: in-flight flows for older artifacts complete,\n // but no longer push states into (or re-arm) this handle.\n if (state === \"starting\") bound.attempt += 1;\n });\n capture.handle.on(\"captured\", (artifacts) => {\n // Partners' own `captured` handlers run first (the event fires BEFORE\n // any upload); the flow starts on the next microtask, after any\n // in-flight chain.\n void Promise.resolve().then(() => enqueueFlow(bound, artifacts));\n });\n const inner = capture.handle;\n return {\n get state() {\n return inner.state;\n },\n on: (event, handler) => inner.on(event, handler),\n start: () => inner.start(),\n retry: () => inner.retry(),\n switchCamera: () => inner.switchCamera(),\n destroy: () => {\n bound.alive = false;\n if (active === bound) active = null;\n inner.destroy();\n },\n };\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 if (active !== null) {\n active.alive = false;\n active.capture.handle.destroy();\n active = null;\n }\n emitter.clear();\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/index.ts","../../src/client/errors.ts","../../src/client/hosted-client.ts"],"sourcesContent":["// @sekyro/web/client — typed API client for the hosted\n// session-token surface (Plan 6 Task 5). Framework-free browser code.\nexport {\n HostedClient,\n stripMimeParams,\n type HostedClientOptions,\n type MediaCheckType,\n type SubmitOptions,\n type UploadMedia,\n} from \"./hosted-client\";\nexport { HostedApiError, KNOWN_CODES, isKnownCode, type KnownCode } from \"./errors\";\nexport type {\n AttestationInput,\n CheckInputByType,\n CheckReason,\n CheckState,\n CheckType,\n CompleteMediaResult,\n ConsentLocale,\n DocOcrInput,\n DocReason,\n DocSide,\n FaceMatchInput,\n FaceMatchReason,\n HostedOtpView,\n HostedSessionView,\n HostedStepView,\n LivenessInput,\n LivenessReason,\n MediaKind,\n PhoneOtpInput,\n PhoneOtpReason,\n PopulationInput,\n PopulationReason,\n PresignRequest,\n PresignUploadResult,\n RecordConsentResult,\n SessionOutcome,\n SessionStatus,\n SessionStepView,\n SessionView,\n SubmitCheckBody,\n} from \"./types\";\n","// Public error map for the session-token surface (Plan 6 Task 5).\n//\n// The backend's AllExceptionsFilter (audience 'public') emits the FLAT envelope\n// `{code, message}` — no details, no request_id, no traceId. `code` is the\n// snake_cased `error` key of the thrown HttpException body (extractError →\n// camelToSnake). 429 responses additionally carry a `Retry-After` header in\n// SECONDS (SessionTokenGuard / RateLimitGuard).\n//\n// KNOWN_CODES below is the VERIFIED set reachable through the five hosted\n// endpoints (grep of `error: '…'` sites in identity-backend @ Tasks 0+1+2 —\n// hosted.controller, checks.controller, media.{controller,service},\n// sessions.service submit flow, session-token.guard, valibot.pipe,\n// check-executor registry, idempotency.interceptor, attestation mapping),\n// plus four CLIENT-SYNTHESIZED codes that never come from the backend:\n// - 'unknown_error' — a response body that was not parseable JSON (proxy HTML\n// on an error, or a malformed 2xx body)\n// - 'upload_failed' — the bare Spaces PUT answered non-2xx (S3 XML, no envelope)\n// - 'network_error' — fetch itself rejected: connection drop / DNS / CORS /\n// mid-transfer failure (status: 0 — no response arrived)\n// - 'timeout' — the request's AbortSignal.timeout fired (status: 0)\n// Task 7's error-copy map must carry entries for all four from day one.\n//\n// Deliberately EXCLUDED (verified unreachable on this surface today):\n// - payment_required / account_suspended — BillingInterceptor gates only\n// handlers decorated @BillableCheck, and no handler is decorated yet.\n// - not_found — the generic unmatched-route 404; a correctly configured client\n// never produces it. Unknown codes route through `isKnownCode() === false`.\n\n/**\n * Every error code the hosted page can receive from the session-token surface.\n * A closed string-literal union — switch over {@link KnownCode} with a `never`\n * default arm for compile-checked exhaustiveness.\n */\nexport const KNOWN_CODES = [\n // auth / addressing (session-token.guard, controllers' own-session guards)\n \"session_token_invalid\", // 401 — missing/invalid/expired/revoked token\n \"session_token_required\", // 403 — non-session-token principal on a hosted route (defensive)\n \"session_token_session_mismatch\", // 403 — route :id differs from the token's bound session\n \"session_not_found\", // 404 — token's session missing/foreign (findOwned)\n // rate limiting (session-token.guard — per-token + per-IP buckets)\n \"rate_limit_exceeded\", // 429 + Retry-After header (seconds)\n // consent gate (Task 0 — submit AND media presign/complete)\n \"consent_required\", // 409 — record consent before uploading/submitting\n // submit flow (sessions.service.submitCheck)\n \"session_not_active\", // 409 — session is terminal/review; no further submissions\n \"check_not_found\", // 404 — session has no such checkType\n \"check_already_finalized\", // 409 — re-submit of a terminal/review check\n \"check_out_of_order\", // 409 — only the frontier check accepts a submission\n \"check_type_unsupported\", // 400 — no registered executor for the checkType\n \"force_outcome_forbidden\", // 403 — forceOutcome on a live session (plan D-C)\n \"attestation_invalid\", // 400 (Task 1) — Turnstile/attestation failed; reload and retry\n \"nni_locked\", // 409 (Plan A Task 2) — session is bound to a server-provided NNI; a different client NNI was sent\n // media (media.service)\n \"media_content_type_not_allowed\", // 400 — outside the per-kind MIME allowlist\n \"media_too_large\", // 413 — over the per-kind byte cap\n \"media_key_invalid\", // 400 — not a well-formed pending key\n \"media_key_forbidden\", // 403 — key names another session's pending space\n \"media_not_found\", // 404 — no uploaded object behind the key\n \"media_unreadable\", // 422 — magic-byte sniff rejected the object\n \"media_session_invalid\", // 400 — sessionId unusable as a key segment (never in practice)\n // idempotency (idempotency.interceptor — Task 2)\n \"idempotency_key_collision\", // 409 — same key, different body hash\n \"idempotency_in_progress\", // 409 — concurrent identical request mid-flight (client auto-retries)\n \"idempotency_key_required\", // 400 — only on @Idempotent({required:true}) routes (none hosted yet)\n // validation + fallback\n \"validation_error\", // 400 — ValibotPipe rejection (camelToSnake of 'ValidationError')\n \"internal_error\", // 500 — unhandled server error\n // client-synthesized (never sent by the backend)\n \"unknown_error\", // unparseable (non-JSON) response body — error OR 2xx (defensive)\n \"upload_failed\", // the bare Spaces PUT answered non-2xx\n \"network_error\", // fetch rejected: connection drop / DNS / CORS / mid-transfer (status 0)\n \"timeout\", // AbortSignal.timeout fired — API calls 30 s default, Spaces PUT 15 min (status 0)\n] as const;\n\n/** Closed union of {@link KNOWN_CODES} — exhaustively switchable. */\nexport type KnownCode = (typeof KNOWN_CODES)[number];\n\n/** Narrow an arbitrary wire code to the known union (unknown codes stay `string`). */\nexport function isKnownCode(code: string): code is KnownCode {\n return (KNOWN_CODES as readonly string[]).includes(code);\n}\n\n/**\n * Typed transport error: EVERY failure surfaces as one of these — non-2xx\n * responses, rejected fetches, timeouts, and unparseable bodies alike. The\n * hosted page never sees a raw TypeError/DOMException from the client.\n *\n * - `code` — the public envelope's `code`, or a client-synthesized fallback\n * ('unknown_error' / 'upload_failed' / 'network_error' / 'timeout'). Typed\n * `string` because the wire may grow codes; narrow with {@link isKnownCode}\n * before an exhaustive switch.\n * - `status` — the HTTP status, or 0 when no response arrived at all\n * ('network_error' / 'timeout').\n * - `retryAfterSeconds` — parsed from the `Retry-After` header when present\n * (rate-limit 429s always set it).\n */\nexport class HostedApiError extends Error {\n readonly status: number;\n readonly code: string;\n readonly retryAfterSeconds?: number;\n\n constructor(args: { status: number; code: string; message: string; retryAfterSeconds?: number }) {\n super(args.message);\n this.name = \"HostedApiError\";\n this.status = args.status;\n this.code = args.code;\n if (args.retryAfterSeconds !== undefined) this.retryAfterSeconds = args.retryAfterSeconds;\n }\n}\n","// Typed API client for the hosted session-token surface (Plan 6 Task 5).\n// Framework-free browser code — fetch + AbortSignal only, same zero-React rule\n// as core/. Hand-authored against the CURRENT identity-backend (Tasks 0+1+2);\n// see types.ts / errors.ts for the verified wire shapes.\nimport { HostedApiError } from \"./errors\";\nimport type {\n CheckInputByType,\n CheckType,\n CompleteMediaResult,\n ConsentLocale,\n HostedSessionView,\n MediaKind,\n PresignRequest,\n PresignUploadResult,\n RecordConsentResult,\n SessionView,\n SubmitCheckBody,\n} from \"./types\";\n\n/** Default per-request timeout on API calls (override via `requestTimeoutMs`). */\nconst DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/**\n * Timeout on the Spaces PUT — matches the presign TTL (900 s): the signature is\n * unusable after that anyway, and a 15 MB clip at 20 KB/s still finishes in\n * ~13 min. Bounds a stalled in-flight socket (the TTL alone only bounds when a\n * PUT can START, not how long a hung transfer may sit).\n */\nconst SPACES_PUT_TIMEOUT_MS = 900_000;\n\n/** Max chars of an S3 error body quoted into an upload_failed message. */\nconst UPLOAD_ERROR_SNIPPET_CHARS = 200;\n\n/** Bounded auto-retry defaults for 409 `idempotency_in_progress` (retries AFTER the first try). */\nconst IN_PROGRESS_RETRY_ATTEMPTS = 5;\nconst IN_PROGRESS_RETRY_DELAY_MS = 400;\n\n/**\n * Strip MIME parameters: `video/webm;codecs=vp8,opus` → `video/webm`.\n *\n * The capture engine (Task 4) emits `clipMimeType` WITH codec parameters, but\n * the presign allowlist holds BARE types only (['video/webm','video/mp4'] /\n * ['image/jpeg','image/png']) and the presigned PUT signs the exact declared\n * Content-Type. Exported so pages can normalize once and stay byte-consistent\n * between presign and PUT.\n */\nexport function stripMimeParams(contentType: string): string {\n return (contentType.split(\";\")[0] ?? \"\").trim();\n}\n\n/** Media-bearing check types → the input field the promoted key goes into. */\nconst MEDIA_INPUT_FIELD = {\n liveness: \"clipKey\",\n face_match: \"imageKey\",\n doc_ocr: \"imageKey\",\n} as const;\n\n/** Check types that take an uploaded media key (population_registry does not). */\nexport type MediaCheckType = keyof typeof MEDIA_INPUT_FIELD;\n\nexport interface SubmitOptions {\n /**\n * Sent as `x-idempotency-key` (the header IdempotencyInterceptor reads).\n * One key per LOGICAL submit — a flaky-network retry with the same key\n * replays the first response without burning another attempt.\n */\n idempotencyKey?: string;\n /** SANDBOX-ONLY deterministic override (plan D-C); a live session 403s it. */\n forceOutcome?: \"pass\" | \"fail\";\n}\n\nexport interface UploadMedia {\n kind: MediaKind;\n blob: Blob;\n /** May carry codec params (engine `clipMimeType`) — stripped before presign AND PUT. */\n contentType: string;\n}\n\nexport interface HostedClientOptions {\n /** API origin, e.g. `https://api.sekyro.ai` — paths are `${apiOrigin}/api/v2/...`. */\n apiOrigin: string;\n /** The raw session token from the hosted URL (short code; legacy `cst_` during its TTL) — sent as `x-session-token` on every API call. */\n sessionToken: string;\n /** Injection seam (tests / non-window contexts). Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n /** Per-request timeout for API calls (NOT the Spaces PUT). Default 30 000 ms. */\n requestTimeoutMs?: number;\n /**\n * Tuning for the ONLY automatic retry the client performs: 409\n * `idempotency_in_progress` (a concurrent identical request is mid-flight\n * server-side; the real response lands once it finishes). `attempts` counts\n * retries AFTER the first try. Defaults: 5 × 400 ms.\n */\n inProgressRetry?: { attempts?: number; delayMs?: number };\n}\n\n/**\n * Hand-authored typed client for the five session-token endpoints:\n *\n * GET /api/v2/hosted/session → {@link getSession}\n * POST /api/v2/hosted/consent → {@link recordConsent}\n * POST /api/v2/sessions/:id/media → {@link presign}\n * POST /api/v2/sessions/:id/media/complete → {@link completeMedia}\n * POST /api/v2/sessions/:id/checks/:checkType → {@link submitCheck}\n *\n * plus the bare presigned Spaces PUT ({@link uploadToSpaces}) and the\n * presign→PUT→complete→submit composition ({@link uploadAndSubmit}).\n *\n * Envelopes (verified against sessions-docs.e2e-spec.ts):\n * - success: `{success: true, data}` — unwrapped here, methods return the payload;\n * - error: flat `{code, message}` → thrown as {@link HostedApiError}.\n *\n * The media/checks routes are `:id`-addressed while the token itself is the\n * only credential the page starts with — the client learns its sessionId from\n * `GET /hosted/session` (token-addressed) and lazily bootstraps it when a\n * `:id` route is called first.\n *\n * No automatic retry EXCEPT bounded short-delay retry on 409\n * `idempotency_in_progress`. Everything else — including 429 with its parsed\n * `retryAfterSeconds` — is surfaced to the page to handle.\n */\nexport class HostedClient {\n private readonly apiOrigin: string;\n private readonly sessionToken: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly requestTimeoutMs: number;\n private readonly retryAttempts: number;\n private readonly retryDelayMs: number;\n\n private sessionId: string | null = null;\n private bootstrap: Promise<HostedSessionView> | null = null;\n\n constructor(opts: HostedClientOptions) {\n this.apiOrigin = opts.apiOrigin.replace(/\\/+$/, \"\");\n this.sessionToken = opts.sessionToken;\n // Always wrap and `.call(globalThis, …)`: browsers throw \"Illegal\n // invocation\" when fetch runs with a `this` that isn't the global — both\n // for the default AND for an injected `window.fetch`.\n const impl = opts.fetch;\n this.fetchImpl = (input, init) => (impl ?? globalThis.fetch).call(globalThis, input, init);\n this.requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n this.retryAttempts = opts.inProgressRetry?.attempts ?? IN_PROGRESS_RETRY_ATTEMPTS;\n this.retryDelayMs = opts.inProgressRetry?.delayMs ?? IN_PROGRESS_RETRY_DELAY_MS;\n }\n\n /** Token-addressed bootstrap — the hosted page's first call. Caches the sessionId. */\n async getSession(): Promise<HostedSessionView> {\n const view = await this.api<HostedSessionView>(\"GET\", \"/hosted/session\");\n this.sessionId = view.sessionId;\n return view;\n }\n\n /**\n * Record consent once (201; an idempotent replay answers 200 with\n * `alreadyRecorded: true`). Until recorded, media presign/complete AND check\n * submission answer 409 `consent_required`.\n */\n async recordConsent(locale: ConsentLocale): Promise<RecordConsentResult> {\n return this.api<RecordConsentResult>(\"POST\", \"/hosted/consent\", { body: { locale } });\n }\n\n /**\n * Presign a Spaces PUT for a new pending object. `contentType` is stripped of\n * MIME parameters defensively (the allowlist holds bare types; the engine's\n * `clipMimeType` carries codec params) — PUT with the SAME bare type, or the\n * signed-header check rejects the upload.\n */\n async presign(\n kind: MediaKind,\n contentType: string,\n contentLength: number,\n ): Promise<PresignUploadResult> {\n const id = await this.ensureSessionId();\n const body: PresignRequest = { kind, contentType: stripMimeParams(contentType), contentLength };\n return this.api<PresignUploadResult>(\"POST\", `/sessions/${id}/media`, { body });\n }\n\n /**\n * Bare PUT to the presigned Spaces URL: EXACTLY the signed headers — the\n * declared Content-Type and the blob body. NO `x-session-token` (the\n * signature IS the credential; an extra header breaks nothing but leaks the\n * token to a third-party host) and no JSON envelope.\n *\n * `contentType` is param-stripped here too (idempotent for bare types): the\n * presign strips internally, so a caller composing manually with the engine's\n * parameterful `clipMimeType` would otherwise sign `video/webm` but PUT\n * `video/webm;codecs=…` — a signed-header mismatch S3 rejects at runtime.\n *\n * Timeout: 15 min (`AbortSignal.timeout(900_000)`, matching the presign TTL)\n * rather than the API calls' 30 s — a 15 MB clip on a slow link legitimately\n * exceeds 30 s, but a stalled in-flight socket must still be bounded (the TTL\n * alone only bounds when a PUT can START).\n *\n * Failures are ALWAYS typed: non-2xx throws {@link HostedApiError} with the\n * client-synthesized `upload_failed` (message carries a truncated S3 body\n * snippet — e.g. SignatureDoesNotMatch); a mid-transfer drop → `network_error`;\n * the timeout → `timeout`.\n */\n async uploadToSpaces(uploadUrl: string, blob: Blob, contentType: string): Promise<void> {\n let res: Response;\n try {\n res = await this.fetchImpl(uploadUrl, {\n method: \"PUT\",\n headers: { \"content-type\": stripMimeParams(contentType) },\n body: blob,\n signal: AbortSignal.timeout(SPACES_PUT_TIMEOUT_MS),\n });\n } catch (err) {\n throw asTransportError(err);\n }\n if (!res.ok) {\n const snippet = (await res.text().catch(() => \"\")).slice(0, UPLOAD_ERROR_SNIPPET_CHARS);\n throw new HostedApiError({\n status: res.status,\n code: \"upload_failed\",\n message: `media upload failed (HTTP ${res.status})${snippet ? `: ${snippet}` : \"\"}`,\n });\n }\n }\n\n /** Finalize an upload: the backend sniffs + promotes; returns the PROMOTED key to submit. */\n async completeMedia(key: string): Promise<CompleteMediaResult> {\n const id = await this.ensureSessionId();\n return this.api<CompleteMediaResult>(\"POST\", `/sessions/${id}/media/complete`, {\n body: { key },\n });\n }\n\n /**\n * Submit one check. Sends `{input, forceOutcome?}` (SubmitCheckSchema) and the\n * optional `x-idempotency-key` header. Returns the whitelisted SessionView.\n */\n async submitCheck<T extends CheckType>(\n checkType: T,\n input: CheckInputByType[T],\n opts: SubmitOptions = {},\n ): Promise<SessionView> {\n const id = await this.ensureSessionId();\n const body: SubmitCheckBody = {\n input: input as unknown as Record<string, unknown>,\n ...(opts.forceOutcome !== undefined ? { forceOutcome: opts.forceOutcome } : {}),\n };\n return this.api<SessionView>(\"POST\", `/sessions/${id}/checks/${checkType}`, {\n body,\n ...(opts.idempotencyKey !== undefined ? { idempotencyKey: opts.idempotencyKey } : {}),\n });\n }\n\n /**\n * Convenience composition: presign → PUT → complete → submit, passing the\n * PROMOTED key (from complete's response — never the pending presign key)\n * into the check's media input field (`clipKey` for liveness, `imageKey` for\n * face_match/doc_ocr). `extraInput` carries the remaining input fields\n * (liveness `attestation`, doc_ocr `side`). The media content type is\n * param-stripped ONCE and used for both presign and PUT, so the signed\n * header always matches.\n */\n async uploadAndSubmit<T extends MediaCheckType>(\n checkType: T,\n media: UploadMedia,\n extraInput: Omit<CheckInputByType[T], (typeof MEDIA_INPUT_FIELD)[T]>,\n opts: SubmitOptions = {},\n ): Promise<SessionView> {\n const bareType = stripMimeParams(media.contentType);\n const presigned = await this.presign(media.kind, bareType, media.blob.size);\n await this.uploadToSpaces(presigned.uploadUrl, media.blob, bareType);\n const completed = await this.completeMedia(presigned.key);\n const input = {\n ...extraInput,\n [MEDIA_INPUT_FIELD[checkType]]: completed.key,\n } as unknown as CheckInputByType[T];\n return this.submitCheck(checkType, input, opts);\n }\n\n // ── transport ──────────────────────────────────────────────────────────────\n\n /** The token's own sessionId — cached from getSession(), lazily bootstrapped. */\n private async ensureSessionId(): Promise<string> {\n if (this.sessionId !== null) return this.sessionId;\n // Dedupe concurrent bootstraps; reset on settle so a failure can be retried.\n this.bootstrap ??= this.getSession().finally(() => {\n this.bootstrap = null;\n });\n return (await this.bootstrap).sessionId;\n }\n\n /**\n * One API request: `${apiOrigin}/api/v2${path}`, `x-session-token` always,\n * AbortSignal timeout (30 s default), `{success, data}` unwrap on 2xx, flat\n * `{code, message}` → {@link HostedApiError} on non-2xx. EVERY failure is\n * typed: a rejected fetch → `network_error`, the timeout → `timeout`, an\n * unparseable 2xx body → `unknown_error` — the page never sees a raw\n * TypeError/DOMException. The ONLY automatic retry is 409\n * `idempotency_in_progress` (bounded, short delay).\n */\n private async api<T>(\n method: \"GET\" | \"POST\",\n path: string,\n opts: { body?: unknown; idempotencyKey?: string } = {},\n ): Promise<T> {\n const url = `${this.apiOrigin}/api/v2${path}`;\n const headers: Record<string, string> = { \"x-session-token\": this.sessionToken };\n if (opts.body !== undefined) headers[\"content-type\"] = \"application/json\";\n if (opts.idempotencyKey !== undefined) headers[\"x-idempotency-key\"] = opts.idempotencyKey;\n\n for (let attempt = 0; ; attempt++) {\n let res: Response;\n try {\n res = await this.fetchImpl(url, {\n method,\n headers,\n ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),\n signal: AbortSignal.timeout(this.requestTimeoutMs),\n });\n } catch (err) {\n throw asTransportError(err);\n }\n\n if (res.ok) {\n let raw: unknown;\n try {\n raw = await res.json();\n } catch {\n throw new HostedApiError({\n status: res.status,\n code: \"unknown_error\",\n message: `response body was not valid JSON (HTTP ${res.status})`,\n });\n }\n return unwrap<T>(raw);\n }\n\n const err = await parseErrorResponse(res);\n if (err.code === \"idempotency_in_progress\" && attempt < this.retryAttempts) {\n if (this.retryDelayMs > 0) await sleep(this.retryDelayMs);\n continue;\n }\n throw err;\n }\n }\n}\n\n/**\n * Normalize a rejected fetch into the typed contract (status 0 — no response\n * arrived): AbortSignal.timeout aborts with a DOMException named 'TimeoutError'\n * → `timeout`; everything else (TypeError on connection drop / DNS / CORS /\n * mid-transfer failure) → `network_error`. Duck-typed on `.name` because\n * DOMException does not inherit Error in every environment (e.g. jsdom).\n */\nfunction asTransportError(err: unknown): HostedApiError {\n const name = typeof err === \"object\" && err !== null ? (err as { name?: unknown }).name : undefined;\n if (name === \"TimeoutError\") {\n return new HostedApiError({ status: 0, code: \"timeout\", message: \"request timed out\" });\n }\n const detail = err instanceof Error ? err.message : String(err);\n return new HostedApiError({\n status: 0,\n code: \"network_error\",\n message: `network request failed: ${detail}`,\n });\n}\n\n/**\n * Unwrap the global success envelope `{success: true, data}` (ResponseTransform-\n * Interceptor wraps every object body that lacks a `success` key). Defensive:\n * a body without the envelope is returned as-is.\n */\nfunction unwrap<T>(raw: unknown): T {\n if (raw !== null && typeof raw === \"object\" && \"success\" in raw && \"data\" in raw) {\n return (raw as { data: T }).data;\n }\n return raw as T;\n}\n\n/**\n * Parse a non-2xx response into a {@link HostedApiError}: the public envelope\n * is flat `{code, message}`; a non-JSON body (proxy HTML, etc.) degrades to the\n * client-synthesized `unknown_error`. `Retry-After` (seconds) is parsed when\n * present — rate-limit 429s always set it.\n */\nasync function parseErrorResponse(res: Response): Promise<HostedApiError> {\n let code = \"unknown_error\";\n let message = `request failed (HTTP ${res.status})`;\n try {\n const body: unknown = await res.json();\n if (body !== null && typeof body === \"object\") {\n const env = body as { code?: unknown; message?: unknown };\n if (typeof env.code === \"string\" && env.code.length > 0) code = env.code;\n if (typeof env.message === \"string\" && env.message.length > 0) message = env.message;\n }\n } catch {\n // Not JSON — keep the defensive fallback.\n }\n const retryAfterRaw = res.headers.get(\"retry-after\");\n const retryAfter = retryAfterRaw === null ? Number.NaN : Number.parseInt(retryAfterRaw, 10);\n return new HostedApiError({\n status: res.status,\n code,\n message,\n ...(Number.isFinite(retryAfter) && retryAfter >= 0 ? { retryAfterSeconds: retryAfter } : {}),\n });\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":["err"]}
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts","../../src/client/errors.ts","../../src/client/hosted-client.ts"],"sourcesContent":["// @sekyro/web/client — typed API client for the hosted\n// session-token surface (Plan 6 Task 5). Framework-free browser code.\nexport {\n HostedClient,\n stripMimeParams,\n type HostedClientOptions,\n type MediaCheckType,\n type SubmitOptions,\n type UploadMedia,\n} from \"./hosted-client\";\nexport { HostedApiError, KNOWN_CODES, isKnownCode, type KnownCode } from \"./errors\";\nexport type {\n AttestationInput,\n CheckInputByType,\n CheckReason,\n CheckState,\n CheckType,\n CompleteMediaResult,\n ConsentLocale,\n DocOcrInput,\n DocReason,\n DocSide,\n FaceMatchInput,\n FaceMatchReason,\n HostedOtpView,\n HostedSessionView,\n HostedStepView,\n LivenessCaptureInput,\n LivenessInput,\n LivenessReason,\n MediaKind,\n PhoneOtpInput,\n PhoneOtpReason,\n PopulationInput,\n PopulationReason,\n PresignRequest,\n PresignUploadResult,\n RecordConsentResult,\n SessionOutcome,\n SessionStatus,\n SessionStepView,\n SessionView,\n SubmitCheckBody,\n} from \"./types\";\n","// Public error map for the session-token surface (Plan 6 Task 5).\n//\n// The backend's AllExceptionsFilter (audience 'public') emits the FLAT envelope\n// `{code, message}` — no details, no request_id, no traceId. `code` is the\n// snake_cased `error` key of the thrown HttpException body (extractError →\n// camelToSnake). 429 responses additionally carry a `Retry-After` header in\n// SECONDS (SessionTokenGuard / RateLimitGuard).\n//\n// KNOWN_CODES below is the VERIFIED set reachable through the five hosted\n// endpoints (grep of `error: '…'` sites in identity-backend @ Tasks 0+1+2 —\n// hosted.controller, checks.controller, media.{controller,service},\n// sessions.service submit flow, session-token.guard, valibot.pipe,\n// check-executor registry, idempotency.interceptor, attestation mapping),\n// plus four CLIENT-SYNTHESIZED codes that never come from the backend:\n// - 'unknown_error' — a response body that was not parseable JSON (proxy HTML\n// on an error, or a malformed 2xx body)\n// - 'upload_failed' — the bare Spaces PUT answered non-2xx (S3 XML, no envelope)\n// - 'network_error' — fetch itself rejected: connection drop / DNS / CORS /\n// mid-transfer failure (status: 0 — no response arrived)\n// - 'timeout' — the request's AbortSignal.timeout fired (status: 0)\n// Task 7's error-copy map must carry entries for all four from day one.\n//\n// Deliberately EXCLUDED (verified unreachable on this surface today):\n// - payment_required / account_suspended — BillingInterceptor gates only\n// handlers decorated @BillableCheck, and no handler is decorated yet.\n// - not_found — the generic unmatched-route 404; a correctly configured client\n// never produces it. Unknown codes route through `isKnownCode() === false`.\n\n/**\n * Every error code the hosted page can receive from the session-token surface.\n * A closed string-literal union — switch over {@link KnownCode} with a `never`\n * default arm for compile-checked exhaustiveness.\n */\nexport const KNOWN_CODES = [\n // auth / addressing (session-token.guard, controllers' own-session guards)\n \"session_token_invalid\", // 401 — missing/invalid/expired/revoked token\n \"session_token_required\", // 403 — non-session-token principal on a hosted route (defensive)\n \"session_token_session_mismatch\", // 403 — route :id differs from the token's bound session\n \"session_not_found\", // 404 — token's session missing/foreign (findOwned)\n // rate limiting (session-token.guard — per-token + per-IP buckets)\n \"rate_limit_exceeded\", // 429 + Retry-After header (seconds)\n // consent gate (Task 0 — submit AND media presign/complete)\n \"consent_required\", // 409 — record consent before uploading/submitting\n // submit flow (sessions.service.submitCheck)\n \"session_not_active\", // 409 — session is terminal/review; no further submissions\n \"check_not_found\", // 404 — session has no such checkType\n \"check_already_finalized\", // 409 — re-submit of a terminal/review check\n \"check_out_of_order\", // 409 — only the frontier check accepts a submission\n \"check_type_unsupported\", // 400 — no registered executor for the checkType\n \"force_outcome_forbidden\", // 403 — forceOutcome on a live session (plan D-C)\n \"attestation_invalid\", // 400 (Task 1) — Turnstile/attestation failed; reload and retry\n \"nni_locked\", // 409 (Plan A Task 2) — session is bound to a server-provided NNI; a different client NNI was sent\n // media (media.service)\n \"media_content_type_not_allowed\", // 400 — outside the per-kind MIME allowlist\n \"media_too_large\", // 413 — over the per-kind byte cap\n \"media_key_invalid\", // 400 — not a well-formed pending key\n \"media_key_forbidden\", // 403 — key names another session's pending space\n \"media_not_found\", // 404 — no uploaded object behind the key\n \"media_unreadable\", // 422 — magic-byte sniff rejected the object\n \"media_session_invalid\", // 400 — sessionId unusable as a key segment (never in practice)\n // idempotency (idempotency.interceptor — Task 2)\n \"idempotency_key_collision\", // 409 — same key, different body hash\n \"idempotency_in_progress\", // 409 — concurrent identical request mid-flight (client auto-retries)\n \"idempotency_key_required\", // 400 — only on @Idempotent({required:true}) routes (none hosted yet)\n // validation + fallback\n \"validation_error\", // 400 — ValibotPipe rejection (camelToSnake of 'ValidationError')\n \"internal_error\", // 500 — unhandled server error\n // client-synthesized (never sent by the backend)\n \"unknown_error\", // unparseable (non-JSON) response body — error OR 2xx (defensive)\n \"upload_failed\", // the bare Spaces PUT answered non-2xx\n \"network_error\", // fetch rejected: connection drop / DNS / CORS / mid-transfer (status 0)\n \"timeout\", // AbortSignal.timeout fired — API calls 30 s default, Spaces PUT 15 min (status 0)\n] as const;\n\n/** Closed union of {@link KNOWN_CODES} — exhaustively switchable. */\nexport type KnownCode = (typeof KNOWN_CODES)[number];\n\n/** Narrow an arbitrary wire code to the known union (unknown codes stay `string`). */\nexport function isKnownCode(code: string): code is KnownCode {\n return (KNOWN_CODES as readonly string[]).includes(code);\n}\n\n/**\n * Typed transport error: EVERY failure surfaces as one of these — non-2xx\n * responses, rejected fetches, timeouts, and unparseable bodies alike. The\n * hosted page never sees a raw TypeError/DOMException from the client.\n *\n * - `code` — the public envelope's `code`, or a client-synthesized fallback\n * ('unknown_error' / 'upload_failed' / 'network_error' / 'timeout'). Typed\n * `string` because the wire may grow codes; narrow with {@link isKnownCode}\n * before an exhaustive switch.\n * - `status` — the HTTP status, or 0 when no response arrived at all\n * ('network_error' / 'timeout').\n * - `retryAfterSeconds` — parsed from the `Retry-After` header when present\n * (rate-limit 429s always set it).\n */\nexport class HostedApiError extends Error {\n readonly status: number;\n readonly code: string;\n readonly retryAfterSeconds?: number;\n\n constructor(args: { status: number; code: string; message: string; retryAfterSeconds?: number }) {\n super(args.message);\n this.name = \"HostedApiError\";\n this.status = args.status;\n this.code = args.code;\n if (args.retryAfterSeconds !== undefined) this.retryAfterSeconds = args.retryAfterSeconds;\n }\n}\n","// Typed API client for the hosted session-token surface (Plan 6 Task 5).\n// Framework-free browser code — fetch + AbortSignal only, same zero-React rule\n// as core/. Hand-authored against the CURRENT identity-backend (Tasks 0+1+2);\n// see types.ts / errors.ts for the verified wire shapes.\nimport { HostedApiError } from \"./errors\";\nimport type {\n CheckInputByType,\n CheckType,\n CompleteMediaResult,\n ConsentLocale,\n HostedSessionView,\n MediaKind,\n PresignRequest,\n PresignUploadResult,\n RecordConsentResult,\n SessionView,\n SubmitCheckBody,\n} from \"./types\";\n\n/** Default per-request timeout on API calls (override via `requestTimeoutMs`). */\nconst DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/**\n * Timeout on the Spaces PUT — matches the presign TTL (900 s): the signature is\n * unusable after that anyway, and a 15 MB clip at 20 KB/s still finishes in\n * ~13 min. Bounds a stalled in-flight socket (the TTL alone only bounds when a\n * PUT can START, not how long a hung transfer may sit).\n */\nconst SPACES_PUT_TIMEOUT_MS = 900_000;\n\n/** Max chars of an S3 error body quoted into an upload_failed message. */\nconst UPLOAD_ERROR_SNIPPET_CHARS = 200;\n\n/** Bounded auto-retry defaults for 409 `idempotency_in_progress` (retries AFTER the first try). */\nconst IN_PROGRESS_RETRY_ATTEMPTS = 5;\nconst IN_PROGRESS_RETRY_DELAY_MS = 400;\n\n/**\n * Strip MIME parameters: `video/webm;codecs=vp8,opus` → `video/webm`.\n *\n * The capture engine (Task 4) emits `clipMimeType` WITH codec parameters, but\n * the presign allowlist holds BARE types only (['video/webm','video/mp4'] /\n * ['image/jpeg','image/png']) and the presigned PUT signs the exact declared\n * Content-Type. Exported so pages can normalize once and stay byte-consistent\n * between presign and PUT.\n */\nexport function stripMimeParams(contentType: string): string {\n return (contentType.split(\";\")[0] ?? \"\").trim();\n}\n\n/** Media-bearing check types → the input field the promoted key goes into. */\nconst MEDIA_INPUT_FIELD = {\n liveness: \"clipKey\",\n face_match: \"imageKey\",\n doc_ocr: \"imageKey\",\n} as const;\n\n/** Check types that take an uploaded media key (population_registry does not). */\nexport type MediaCheckType = keyof typeof MEDIA_INPUT_FIELD;\n\nexport interface SubmitOptions {\n /**\n * Sent as `x-idempotency-key` (the header IdempotencyInterceptor reads).\n * One key per LOGICAL submit — a flaky-network retry with the same key\n * replays the first response without burning another attempt.\n */\n idempotencyKey?: string;\n /** SANDBOX-ONLY deterministic override (plan D-C); a live session 403s it. */\n forceOutcome?: \"pass\" | \"fail\";\n}\n\nexport interface UploadMedia {\n kind: MediaKind;\n blob: Blob;\n /** May carry codec params (engine `clipMimeType`) — stripped before presign AND PUT. */\n contentType: string;\n}\n\nexport interface HostedClientOptions {\n /** API origin, e.g. `https://api.sekyro.ai` — paths are `${apiOrigin}/api/v2/...`. */\n apiOrigin: string;\n /** The raw session token from the hosted URL (short code; legacy `cst_` during its TTL) — sent as `x-session-token` on every API call. */\n sessionToken: string;\n /** Injection seam (tests / non-window contexts). Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n /** Per-request timeout for API calls (NOT the Spaces PUT). Default 30 000 ms. */\n requestTimeoutMs?: number;\n /**\n * Tuning for the ONLY automatic retry the client performs: 409\n * `idempotency_in_progress` (a concurrent identical request is mid-flight\n * server-side; the real response lands once it finishes). `attempts` counts\n * retries AFTER the first try. Defaults: 5 × 400 ms.\n */\n inProgressRetry?: { attempts?: number; delayMs?: number };\n}\n\n/**\n * Hand-authored typed client for the five session-token endpoints:\n *\n * GET /api/v2/hosted/session → {@link getSession}\n * POST /api/v2/hosted/consent → {@link recordConsent}\n * POST /api/v2/sessions/:id/media → {@link presign}\n * POST /api/v2/sessions/:id/media/complete → {@link completeMedia}\n * POST /api/v2/sessions/:id/checks/:checkType → {@link submitCheck}\n *\n * plus the bare presigned Spaces PUT ({@link uploadToSpaces}) and the\n * presign→PUT→complete→submit composition ({@link uploadAndSubmit}).\n *\n * Envelopes (verified against sessions-docs.e2e-spec.ts):\n * - success: `{success: true, data}` — unwrapped here, methods return the payload;\n * - error: flat `{code, message}` → thrown as {@link HostedApiError}.\n *\n * The media/checks routes are `:id`-addressed while the token itself is the\n * only credential the page starts with — the client learns its sessionId from\n * `GET /hosted/session` (token-addressed) and lazily bootstraps it when a\n * `:id` route is called first.\n *\n * No automatic retry EXCEPT bounded short-delay retry on 409\n * `idempotency_in_progress`. Everything else — including 429 with its parsed\n * `retryAfterSeconds` — is surfaced to the page to handle.\n */\nexport class HostedClient {\n private readonly apiOrigin: string;\n private readonly sessionToken: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly requestTimeoutMs: number;\n private readonly retryAttempts: number;\n private readonly retryDelayMs: number;\n\n private sessionId: string | null = null;\n private bootstrap: Promise<HostedSessionView> | null = null;\n\n constructor(opts: HostedClientOptions) {\n this.apiOrigin = opts.apiOrigin.replace(/\\/+$/, \"\");\n this.sessionToken = opts.sessionToken;\n // Always wrap and `.call(globalThis, …)`: browsers throw \"Illegal\n // invocation\" when fetch runs with a `this` that isn't the global — both\n // for the default AND for an injected `window.fetch`.\n const impl = opts.fetch;\n this.fetchImpl = (input, init) => (impl ?? globalThis.fetch).call(globalThis, input, init);\n this.requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n this.retryAttempts = opts.inProgressRetry?.attempts ?? IN_PROGRESS_RETRY_ATTEMPTS;\n this.retryDelayMs = opts.inProgressRetry?.delayMs ?? IN_PROGRESS_RETRY_DELAY_MS;\n }\n\n /** Token-addressed bootstrap — the hosted page's first call. Caches the sessionId. */\n async getSession(): Promise<HostedSessionView> {\n const view = await this.api<HostedSessionView>(\"GET\", \"/hosted/session\");\n this.sessionId = view.sessionId;\n return view;\n }\n\n /**\n * Record consent once (201; an idempotent replay answers 200 with\n * `alreadyRecorded: true`). Until recorded, media presign/complete AND check\n * submission answer 409 `consent_required`.\n */\n async recordConsent(locale: ConsentLocale): Promise<RecordConsentResult> {\n return this.api<RecordConsentResult>(\"POST\", \"/hosted/consent\", { body: { locale } });\n }\n\n /**\n * Presign a Spaces PUT for a new pending object. `contentType` is stripped of\n * MIME parameters defensively (the allowlist holds bare types; the engine's\n * `clipMimeType` carries codec params) — PUT with the SAME bare type, or the\n * signed-header check rejects the upload.\n */\n async presign(\n kind: MediaKind,\n contentType: string,\n contentLength: number,\n ): Promise<PresignUploadResult> {\n const id = await this.ensureSessionId();\n const body: PresignRequest = { kind, contentType: stripMimeParams(contentType), contentLength };\n return this.api<PresignUploadResult>(\"POST\", `/sessions/${id}/media`, { body });\n }\n\n /**\n * Bare PUT to the presigned Spaces URL: EXACTLY the signed headers — the\n * declared Content-Type and the blob body. NO `x-session-token` (the\n * signature IS the credential; an extra header breaks nothing but leaks the\n * token to a third-party host) and no JSON envelope.\n *\n * `contentType` is param-stripped here too (idempotent for bare types): the\n * presign strips internally, so a caller composing manually with the engine's\n * parameterful `clipMimeType` would otherwise sign `video/webm` but PUT\n * `video/webm;codecs=…` — a signed-header mismatch S3 rejects at runtime.\n *\n * Timeout: 15 min (`AbortSignal.timeout(900_000)`, matching the presign TTL)\n * rather than the API calls' 30 s — a 15 MB clip on a slow link legitimately\n * exceeds 30 s, but a stalled in-flight socket must still be bounded (the TTL\n * alone only bounds when a PUT can START).\n *\n * Failures are ALWAYS typed: non-2xx throws {@link HostedApiError} with the\n * client-synthesized `upload_failed` (message carries a truncated S3 body\n * snippet — e.g. SignatureDoesNotMatch); a mid-transfer drop → `network_error`;\n * the timeout → `timeout`.\n */\n async uploadToSpaces(uploadUrl: string, blob: Blob, contentType: string): Promise<void> {\n let res: Response;\n try {\n res = await this.fetchImpl(uploadUrl, {\n method: \"PUT\",\n headers: { \"content-type\": stripMimeParams(contentType) },\n body: blob,\n signal: AbortSignal.timeout(SPACES_PUT_TIMEOUT_MS),\n });\n } catch (err) {\n throw asTransportError(err);\n }\n if (!res.ok) {\n const snippet = (await res.text().catch(() => \"\")).slice(0, UPLOAD_ERROR_SNIPPET_CHARS);\n throw new HostedApiError({\n status: res.status,\n code: \"upload_failed\",\n message: `media upload failed (HTTP ${res.status})${snippet ? `: ${snippet}` : \"\"}`,\n });\n }\n }\n\n /** Finalize an upload: the backend sniffs + promotes; returns the PROMOTED key to submit. */\n async completeMedia(key: string): Promise<CompleteMediaResult> {\n const id = await this.ensureSessionId();\n return this.api<CompleteMediaResult>(\"POST\", `/sessions/${id}/media/complete`, {\n body: { key },\n });\n }\n\n /**\n * Submit one check. Sends `{input, forceOutcome?}` (SubmitCheckSchema) and the\n * optional `x-idempotency-key` header. Returns the whitelisted SessionView.\n */\n async submitCheck<T extends CheckType>(\n checkType: T,\n input: CheckInputByType[T],\n opts: SubmitOptions = {},\n ): Promise<SessionView> {\n const id = await this.ensureSessionId();\n const body: SubmitCheckBody = {\n input: input as unknown as Record<string, unknown>,\n ...(opts.forceOutcome !== undefined ? { forceOutcome: opts.forceOutcome } : {}),\n };\n return this.api<SessionView>(\"POST\", `/sessions/${id}/checks/${checkType}`, {\n body,\n ...(opts.idempotencyKey !== undefined ? { idempotencyKey: opts.idempotencyKey } : {}),\n });\n }\n\n /**\n * Convenience composition: presign → PUT → complete → submit, passing the\n * PROMOTED key (from complete's response — never the pending presign key)\n * into the check's media input field (`clipKey` for liveness, `imageKey` for\n * face_match/doc_ocr). `extraInput` carries the remaining input fields\n * (liveness `attestation`, doc_ocr `side`). The media content type is\n * param-stripped ONCE and used for both presign and PUT, so the signed\n * header always matches.\n */\n async uploadAndSubmit<T extends MediaCheckType>(\n checkType: T,\n media: UploadMedia,\n extraInput: Omit<CheckInputByType[T], (typeof MEDIA_INPUT_FIELD)[T]>,\n opts: SubmitOptions = {},\n ): Promise<SessionView> {\n const bareType = stripMimeParams(media.contentType);\n const presigned = await this.presign(media.kind, bareType, media.blob.size);\n await this.uploadToSpaces(presigned.uploadUrl, media.blob, bareType);\n const completed = await this.completeMedia(presigned.key);\n const input = {\n ...extraInput,\n [MEDIA_INPUT_FIELD[checkType]]: completed.key,\n } as unknown as CheckInputByType[T];\n return this.submitCheck(checkType, input, opts);\n }\n\n // ── transport ──────────────────────────────────────────────────────────────\n\n /** The token's own sessionId — cached from getSession(), lazily bootstrapped. */\n private async ensureSessionId(): Promise<string> {\n if (this.sessionId !== null) return this.sessionId;\n // Dedupe concurrent bootstraps; reset on settle so a failure can be retried.\n this.bootstrap ??= this.getSession().finally(() => {\n this.bootstrap = null;\n });\n return (await this.bootstrap).sessionId;\n }\n\n /**\n * One API request: `${apiOrigin}/api/v2${path}`, `x-session-token` always,\n * AbortSignal timeout (30 s default), `{success, data}` unwrap on 2xx, flat\n * `{code, message}` → {@link HostedApiError} on non-2xx. EVERY failure is\n * typed: a rejected fetch → `network_error`, the timeout → `timeout`, an\n * unparseable 2xx body → `unknown_error` — the page never sees a raw\n * TypeError/DOMException. The ONLY automatic retry is 409\n * `idempotency_in_progress` (bounded, short delay).\n */\n private async api<T>(\n method: \"GET\" | \"POST\",\n path: string,\n opts: { body?: unknown; idempotencyKey?: string } = {},\n ): Promise<T> {\n const url = `${this.apiOrigin}/api/v2${path}`;\n const headers: Record<string, string> = { \"x-session-token\": this.sessionToken };\n if (opts.body !== undefined) headers[\"content-type\"] = \"application/json\";\n if (opts.idempotencyKey !== undefined) headers[\"x-idempotency-key\"] = opts.idempotencyKey;\n\n for (let attempt = 0; ; attempt++) {\n let res: Response;\n try {\n res = await this.fetchImpl(url, {\n method,\n headers,\n ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),\n signal: AbortSignal.timeout(this.requestTimeoutMs),\n });\n } catch (err) {\n throw asTransportError(err);\n }\n\n if (res.ok) {\n let raw: unknown;\n try {\n raw = await res.json();\n } catch {\n throw new HostedApiError({\n status: res.status,\n code: \"unknown_error\",\n message: `response body was not valid JSON (HTTP ${res.status})`,\n });\n }\n return unwrap<T>(raw);\n }\n\n const err = await parseErrorResponse(res);\n if (err.code === \"idempotency_in_progress\" && attempt < this.retryAttempts) {\n if (this.retryDelayMs > 0) await sleep(this.retryDelayMs);\n continue;\n }\n throw err;\n }\n }\n}\n\n/**\n * Normalize a rejected fetch into the typed contract (status 0 — no response\n * arrived): AbortSignal.timeout aborts with a DOMException named 'TimeoutError'\n * → `timeout`; everything else (TypeError on connection drop / DNS / CORS /\n * mid-transfer failure) → `network_error`. Duck-typed on `.name` because\n * DOMException does not inherit Error in every environment (e.g. jsdom).\n */\nfunction asTransportError(err: unknown): HostedApiError {\n const name = typeof err === \"object\" && err !== null ? (err as { name?: unknown }).name : undefined;\n if (name === \"TimeoutError\") {\n return new HostedApiError({ status: 0, code: \"timeout\", message: \"request timed out\" });\n }\n const detail = err instanceof Error ? err.message : String(err);\n return new HostedApiError({\n status: 0,\n code: \"network_error\",\n message: `network request failed: ${detail}`,\n });\n}\n\n/**\n * Unwrap the global success envelope `{success: true, data}` (ResponseTransform-\n * Interceptor wraps every object body that lacks a `success` key). Defensive:\n * a body without the envelope is returned as-is.\n */\nfunction unwrap<T>(raw: unknown): T {\n if (raw !== null && typeof raw === \"object\" && \"success\" in raw && \"data\" in raw) {\n return (raw as { data: T }).data;\n }\n return raw as T;\n}\n\n/**\n * Parse a non-2xx response into a {@link HostedApiError}: the public envelope\n * is flat `{code, message}`; a non-JSON body (proxy HTML, etc.) degrades to the\n * client-synthesized `unknown_error`. `Retry-After` (seconds) is parsed when\n * present — rate-limit 429s always set it.\n */\nasync function parseErrorResponse(res: Response): Promise<HostedApiError> {\n let code = \"unknown_error\";\n let message = `request failed (HTTP ${res.status})`;\n try {\n const body: unknown = await res.json();\n if (body !== null && typeof body === \"object\") {\n const env = body as { code?: unknown; message?: unknown };\n if (typeof env.code === \"string\" && env.code.length > 0) code = env.code;\n if (typeof env.message === \"string\" && env.message.length > 0) message = env.message;\n }\n } catch {\n // Not JSON — keep the defensive fallback.\n }\n const retryAfterRaw = res.headers.get(\"retry-after\");\n const retryAfter = retryAfterRaw === null ? Number.NaN : Number.parseInt(retryAfterRaw, 10);\n return new HostedApiError({\n status: res.status,\n code,\n message,\n ...(Number.isFinite(retryAfter) && retryAfter >= 0 ? { retryAfterSeconds: retryAfter } : {}),\n });\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":["err"]}
|
package/dist/client/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AttestationInput, i as CheckInputByType, C as CheckReason, a as CheckState, b as CheckType, j as CompleteMediaResult, k as ConsentLocale, l as DocOcrInput, D as DocReason, m as DocSide, n as FaceMatchInput, F as FaceMatchReason, g as HostedClient, o as HostedClientOptions, H as HostedOtpView, c as HostedSessionView, d as HostedStepView, p as LivenessInput, L as LivenessReason, M as MediaCheckType,
|
|
1
|
+
export { A as AttestationInput, i as CheckInputByType, C as CheckReason, a as CheckState, b as CheckType, j as CompleteMediaResult, k as ConsentLocale, l as DocOcrInput, D as DocReason, m as DocSide, n as FaceMatchInput, F as FaceMatchReason, g as HostedClient, o as HostedClientOptions, H as HostedOtpView, c as HostedSessionView, d as HostedStepView, p as LivenessCaptureInput, q as LivenessInput, L as LivenessReason, M as MediaCheckType, r as MediaKind, s as PhoneOtpInput, P as PhoneOtpReason, t as PopulationInput, e as PopulationReason, u as PresignRequest, v as PresignUploadResult, R as RecordConsentResult, S as SessionOutcome, f as SessionStatus, w as SessionStepView, h as SessionView, x as SubmitCheckBody, y as SubmitOptions, U as UploadMedia, z as stripMimeParams } from '../hosted-client-DWzCRYJK.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Every error code the hosted page can receive from the session-token surface.
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AttestationInput, i as CheckInputByType, C as CheckReason, a as CheckState, b as CheckType, j as CompleteMediaResult, k as ConsentLocale, l as DocOcrInput, D as DocReason, m as DocSide, n as FaceMatchInput, F as FaceMatchReason, g as HostedClient, o as HostedClientOptions, H as HostedOtpView, c as HostedSessionView, d as HostedStepView, p as LivenessInput, L as LivenessReason, M as MediaCheckType,
|
|
1
|
+
export { A as AttestationInput, i as CheckInputByType, C as CheckReason, a as CheckState, b as CheckType, j as CompleteMediaResult, k as ConsentLocale, l as DocOcrInput, D as DocReason, m as DocSide, n as FaceMatchInput, F as FaceMatchReason, g as HostedClient, o as HostedClientOptions, H as HostedOtpView, c as HostedSessionView, d as HostedStepView, p as LivenessCaptureInput, q as LivenessInput, L as LivenessReason, M as MediaCheckType, r as MediaKind, s as PhoneOtpInput, P as PhoneOtpReason, t as PopulationInput, e as PopulationReason, u as PresignRequest, v as PresignUploadResult, R as RecordConsentResult, S as SessionOutcome, f as SessionStatus, w as SessionStepView, h as SessionView, x as SubmitCheckBody, y as SubmitOptions, U as UploadMedia, z as stripMimeParams } from '../hosted-client-DWzCRYJK.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Every error code the hosted page can receive from the session-token surface.
|
package/dist/core/index.cjs
CHANGED
|
@@ -687,10 +687,12 @@ var V3_GATE_DEFAULTS = Object.freeze({
|
|
|
687
687
|
MOTION_TAU_S: 0.4,
|
|
688
688
|
MOTION_DEADBAND_IOD_PER_S: 0.1,
|
|
689
689
|
READY_WINDOW_MS: 500,
|
|
690
|
-
// 0.
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
|
|
690
|
+
// (READY_MEAN_Q, the former 0.75 window MEAN, is gone — 2026-10-17 field
|
|
691
|
+
// bug: a steady q anywhere in [Q_GOOD, 0.75) saturated p at 1, passed every
|
|
692
|
+
// guide ("hold still") and could never reach the mean, so the ring sat full
|
|
693
|
+
// and green with nothing recording. Readiness now checks the window against
|
|
694
|
+
// Q_GOOD, which keeps the one-flaky-tick forgiveness and the second-bad-tick
|
|
695
|
+
// block without a dead band above the rise threshold.)
|
|
694
696
|
GUIDE_SIZE: 0.6,
|
|
695
697
|
GUIDE_CENTERING: 0.6,
|
|
696
698
|
GUIDE_POSE: 0.6,
|
|
@@ -768,15 +770,15 @@ var FaceGateV3 = class {
|
|
|
768
770
|
const worstIndex = windowSamples.indexOf(worst);
|
|
769
771
|
windowSamples = windowSamples.filter((_, index) => index !== worstIndex);
|
|
770
772
|
}
|
|
771
|
-
const
|
|
772
|
-
const ready = this.p >= 1 && windowMean >= this.t.READY_MEAN_Q;
|
|
773
|
+
const ready = this.p >= 1 && windowSamples.every((sample) => sample >= this.t.Q_GOOD);
|
|
773
774
|
return {
|
|
774
775
|
at: timestampMs,
|
|
775
776
|
q,
|
|
776
777
|
p: this.p,
|
|
777
778
|
factors,
|
|
778
779
|
guidanceKey: this.guidanceOf(factors, quality, q),
|
|
779
|
-
ready
|
|
780
|
+
ready,
|
|
781
|
+
accepted: q >= this.t.Q_GOOD
|
|
780
782
|
};
|
|
781
783
|
}
|
|
782
784
|
geometryOf(observation, input) {
|
|
@@ -1169,6 +1171,11 @@ var CaptureSessionV3 = class {
|
|
|
1169
1171
|
this.profiler = null;
|
|
1170
1172
|
this.lastGuidanceKey = null;
|
|
1171
1173
|
this.positioningStartedAt = 0;
|
|
1174
|
+
// Face-capture timing marks of the CURRENT attempt (engine clock; reset per run).
|
|
1175
|
+
this.cameraRequestedAt = null;
|
|
1176
|
+
this.cameraReadyAt = null;
|
|
1177
|
+
this.faceAcquiredAt = null;
|
|
1178
|
+
this.captureCompleteAt = null;
|
|
1172
1179
|
this.fired = false;
|
|
1173
1180
|
this.carriedDegrade = 0;
|
|
1174
1181
|
this.carriedMeans = [];
|
|
@@ -1223,6 +1230,10 @@ var CaptureSessionV3 = class {
|
|
|
1223
1230
|
this.lastGuidanceKey = null;
|
|
1224
1231
|
this.carriedDegrade = 0;
|
|
1225
1232
|
this.carriedMeans = [];
|
|
1233
|
+
this.cameraRequestedAt = null;
|
|
1234
|
+
this.cameraReadyAt = null;
|
|
1235
|
+
this.faceAcquiredAt = null;
|
|
1236
|
+
this.captureCompleteAt = null;
|
|
1226
1237
|
this.setState("preflight");
|
|
1227
1238
|
const preflight = cameraPreflight();
|
|
1228
1239
|
if (preflight !== "OK") {
|
|
@@ -1232,6 +1243,7 @@ var CaptureSessionV3 = class {
|
|
|
1232
1243
|
this.setState("acquiring");
|
|
1233
1244
|
const getUserMedia = options.getUserMedia ?? ((constraints) => navigator.mediaDevices.getUserMedia(constraints));
|
|
1234
1245
|
let stream;
|
|
1246
|
+
this.cameraRequestedAt = this.now();
|
|
1235
1247
|
try {
|
|
1236
1248
|
stream = await getUserMedia(buildCaptureConstraints(detectIsMobile(), this.facing));
|
|
1237
1249
|
} catch (err) {
|
|
@@ -1247,6 +1259,7 @@ var CaptureSessionV3 = class {
|
|
|
1247
1259
|
options.video.srcObject = stream;
|
|
1248
1260
|
await options.video.play().catch(() => void 0);
|
|
1249
1261
|
if (attempt !== this.attempt) return;
|
|
1262
|
+
this.cameraReadyAt = this.now();
|
|
1250
1263
|
if (this.detector === null) {
|
|
1251
1264
|
const ladderOptions = {
|
|
1252
1265
|
...options.v3AssetBaseUrl !== void 0 ? { v3AssetBaseUrl: options.v3AssetBaseUrl } : {}
|
|
@@ -1346,6 +1359,7 @@ var CaptureSessionV3 = class {
|
|
|
1346
1359
|
frameWidth: video.videoWidth,
|
|
1347
1360
|
frameHeight: video.videoHeight
|
|
1348
1361
|
});
|
|
1362
|
+
if (tick.accepted && this.faceAcquiredAt === null) this.faceAcquiredAt = tick.at;
|
|
1349
1363
|
this.emit("checks", {
|
|
1350
1364
|
engine: "v3",
|
|
1351
1365
|
progress: tick.p,
|
|
@@ -1359,6 +1373,7 @@ var CaptureSessionV3 = class {
|
|
|
1359
1373
|
}
|
|
1360
1374
|
if (tick.ready && this.stateValue === "positioning") {
|
|
1361
1375
|
this.fired = true;
|
|
1376
|
+
this.captureCompleteAt = tick.at;
|
|
1362
1377
|
void this.fire(attempt);
|
|
1363
1378
|
}
|
|
1364
1379
|
}
|
|
@@ -1453,7 +1468,21 @@ var CaptureSessionV3 = class {
|
|
|
1453
1468
|
finishCapture(artifacts) {
|
|
1454
1469
|
this.stopTracks();
|
|
1455
1470
|
this.setState("captured");
|
|
1456
|
-
this.
|
|
1471
|
+
const timing = this.captureTiming();
|
|
1472
|
+
this.emit("artifacts", timing === void 0 ? artifacts : { ...artifacts, timing });
|
|
1473
|
+
}
|
|
1474
|
+
/** The attempt's capture-phase timing (whole ms, ≥ 0); undefined when no mark landed. */
|
|
1475
|
+
captureTiming() {
|
|
1476
|
+
const span = (from, to) => from !== null && to !== null ? Math.max(0, Math.round(to - from)) : void 0;
|
|
1477
|
+
const cameraReadyMs = span(this.cameraRequestedAt, this.cameraReadyAt);
|
|
1478
|
+
const faceAcquireMs = span(this.cameraReadyAt, this.faceAcquiredAt);
|
|
1479
|
+
const captureMs = span(this.faceAcquiredAt, this.captureCompleteAt);
|
|
1480
|
+
const timing = {
|
|
1481
|
+
...cameraReadyMs !== void 0 ? { cameraReadyMs } : {},
|
|
1482
|
+
...faceAcquireMs !== void 0 ? { faceAcquireMs } : {},
|
|
1483
|
+
...captureMs !== void 0 ? { captureMs } : {}
|
|
1484
|
+
};
|
|
1485
|
+
return Object.keys(timing).length > 0 ? timing : void 0;
|
|
1457
1486
|
}
|
|
1458
1487
|
// ── defaults for the canvas-bound seams ────────────────────────────────────
|
|
1459
1488
|
async defaultKeyframe(video) {
|