@powercred/liveness-web 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,630 @@
1
+ const ee = { gettingReady: "Getting ready…", center: "Center your face in the oval", moveCloser: "Move a little closer", moveBack: "Move a little back", lookStraight: "Look straight at the camera", noFace: "We can't see your face", multipleFaces: "Only one person, please", turnLeft: "Turn your head to your LEFT", turnRight: "Turn your head to your RIGHT", lookUp: "Look UP", lookDown: "Look DOWN", smile: "Smile", blink: "Blink once", openMouth: "Open your mouth", holdStill: "Hold still", great: "Great!", wrongDirection: "That was the other way — let's try again", faceLost: "We lost your face — let's try again", timeUp: "Time's up — let's try again", tooSlow: "That took a while — let's try again", flashIntro: "Hold still — the screen will flash colours", checking: "Checking…", passedTitle: "Done", passedDetail: "Thank you, your liveness check is complete.", submittedTitle: "Thank you", failedTitle: "We couldn't confirm that", failedDetail: "Please try again in good light, facing the camera.", noAttemptsDetail: "Please contact the company that asked you to do this check.", tryAgain: "Try again", cameraDenied: "Camera access is needed. Allow it in your browser settings and try again.", cameraUnavailable: "We couldn't start your camera.", unsupportedBrowser: "This browser can't run the check. Please use an up-to-date Chrome, Safari, Edge or Firefox.", insecureContext: "This page must be opened over https.", network: "Connection problem. Please check your internet and try again.", sessionExpired: "This check has expired. Please start again.", serviceUnavailable: "The check is temporarily unavailable. Please try again shortly.", genericError: "Something went wrong. Please try again." }, te = `
2
+ :host { all: initial; display: block; }
3
+ .pcl { --primary: var(--pcl-primary, #4b45a1); --ok: #1f9d55; --warn: #e3a008; --bad: #d64545;
4
+ font-family: var(--pcl-font, system-ui, -apple-system, "Segoe UI", sans-serif); color: #fff;
5
+ position: relative; width: 100%; max-width: 560px; margin: 0 auto; }
6
+ .stage { position: relative; width: 100%; aspect-ratio: 3 / 4; background: #000; border-radius: 16px; overflow: hidden; }
7
+ @media (min-aspect-ratio: 4/3) and (min-width: 700px) { .stage { aspect-ratio: 4 / 3; } }
8
+ video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1); }
9
+ .oval { position: absolute; left: 50%; top: 45%; width: 62%; aspect-ratio: 3 / 4; max-height: 76%; transform: translate(-50%, -50%);
10
+ border-radius: 50%; border: 3px solid rgba(255,255,255,.85); box-shadow: 0 0 0 9999px rgba(0,0,0,.5);
11
+ transition: border-color .2s; }
12
+ .oval[data-state="ok"] { border-color: var(--ok); }
13
+ .oval[data-state="warn"] { border-color: var(--warn); }
14
+ .prompt { position: absolute; left: 0; right: 0; bottom: 6%; padding: 0 16px; text-align: center;
15
+ font-size: clamp(18px, 5vw, 26px); font-weight: 650; line-height: 1.25; text-shadow: 0 2px 8px rgba(0,0,0,.8); }
16
+ .progress { position: absolute; top: 14px; left: 0; right: 0; display: flex; gap: 8px; justify-content: center; }
17
+ .dot { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,.35); }
18
+ .dot[data-state="current"] { background: #fff; }
19
+ .dot[data-state="done"] { background: var(--ok); }
20
+ .countdown { position: absolute; top: 10px; right: 12px; font: 600 14px/1 inherit; background: rgba(0,0,0,.5);
21
+ padding: 6px 10px; border-radius: 99px; font-family: inherit; }
22
+ .debug { position: absolute; left: 8px; top: 34px; font: 12px/1.35 ui-monospace, monospace; background: rgba(0,0,0,.6);
23
+ padding: 6px 8px; border-radius: 6px; white-space: pre; }
24
+ .pcl.flashing .stage { position: fixed; inset: 0; z-index: 2147483647; border-radius: 0; aspect-ratio: auto;
25
+ background: var(--flash, #000); max-width: none; }
26
+ .pcl.flashing video { left: 50%; top: 50%; width: auto; height: 45vh; aspect-ratio: 3 / 4; transform: translate(-50%, -50%) scaleX(-1);
27
+ border-radius: 50%; inset: auto; }
28
+ .pcl.flashing .oval, .pcl.flashing .progress, .pcl.flashing .countdown { display: none; }
29
+ .pcl.flashing .prompt { bottom: 10%; }
30
+ .panel { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
31
+ gap: 12px; padding: 24px; text-align: center; background: rgba(15,17,21,.92); border-radius: 16px; }
32
+ .panel[hidden] { display: none; }
33
+ .icon { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-size: 30px; font-weight: 700; }
34
+ .icon.ok { background: var(--ok); } .icon.bad { background: var(--bad); }
35
+ .spinner { width: 44px; height: 44px; border-radius: 50%; border: 4px solid rgba(255,255,255,.25); border-top-color: #fff;
36
+ animation: spin 0.9s linear infinite; }
37
+ @keyframes spin { to { transform: rotate(360deg); } }
38
+ @media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 3s; } }
39
+ .title { font-size: 20px; font-weight: 650; margin: 0; }
40
+ .detail { font-size: 15px; margin: 0; color: #d0d4dc; max-width: 34ch; }
41
+ button { font: inherit; font-size: 16px; font-weight: 600; color: #fff; background: var(--primary); border: 0; border-radius: 10px;
42
+ padding: 12px 22px; cursor: pointer; }
43
+ button:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
44
+ .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
45
+ `;
46
+ class se {
47
+ constructor(e = {}) {
48
+ this.host = document.createElement("div");
49
+ const s = this.host.attachShadow({ mode: "open" });
50
+ s.innerHTML = `<style>${te}</style>
51
+ <div class="pcl">
52
+ <div class="stage">
53
+ <video playsinline muted autoplay></video>
54
+ <div class="oval" data-state="idle"></div>
55
+ <div class="progress"></div>
56
+ <div class="countdown" hidden></div>
57
+ <div class="debug" hidden></div>
58
+ <div class="prompt" role="status" aria-live="polite"></div>
59
+ </div>
60
+ <div class="panel" hidden role="dialog" aria-live="assertive"></div>
61
+ </div>`;
62
+ const i = (n) => s.querySelector(n);
63
+ this.root = i(".pcl"), this.video = i("video"), this.prompt = i(".prompt"), this.countdownEl = i(".countdown"), this.progressEl = i(".progress"), this.oval = i(".oval"), this.debugEl = i(".debug"), this.panel = i(".panel"), e.primaryColor && this.root.style.setProperty("--pcl-primary", e.primaryColor), e.fontFamily && this.root.style.setProperty("--pcl-font", e.fontFamily);
64
+ }
65
+ mount(e) {
66
+ e.appendChild(this.host);
67
+ }
68
+ unmount() {
69
+ this.host.remove();
70
+ }
71
+ async attach(e) {
72
+ this.video.srcObject = e, await this.video.play().catch(() => {
73
+ }), this.video.videoWidth || await new Promise((s) => this.video.addEventListener("loadedmetadata", () => s(), { once: !0 }));
74
+ }
75
+ setPrompt(e) {
76
+ this.prompt.textContent !== e && (this.prompt.textContent = e);
77
+ }
78
+ setCountdown(e) {
79
+ this.countdownEl.hidden = e === null, e !== null && (this.countdownEl.textContent = `${e}s`);
80
+ }
81
+ setProgress(e, s, i) {
82
+ this.progressEl.childElementCount !== e && (this.progressEl.innerHTML = Array.from({ length: e }, () => '<span class="dot"></span>').join("")), Array.from(this.progressEl.children).forEach((n, a) => {
83
+ n.setAttribute("data-state", a < s ? "done" : a === i ? "current" : "todo");
84
+ });
85
+ }
86
+ setOval(e) {
87
+ this.oval.setAttribute("data-state", e);
88
+ }
89
+ setFlash(e) {
90
+ this.root.classList.toggle("flashing", e !== null), e ? this.root.style.setProperty("--flash", e) : this.root.style.removeProperty("--flash");
91
+ }
92
+ setDebug(e) {
93
+ this.debugEl.hidden = e === null, e !== null && (this.debugEl.textContent = e);
94
+ }
95
+ showPanel(e, s, i = "", n) {
96
+ const a = e === "working" ? '<div class="spinner" aria-hidden="true"></div>' : `<div class="icon ${e}" aria-hidden="true">${e === "ok" ? "✓" : "!"}</div>`;
97
+ if (this.panel.innerHTML = `${a}<p class="title"></p>${i ? '<p class="detail"></p>' : ""}${n ? '<button type="button"></button>' : ""}`, this.panel.querySelector(".title").textContent = s, i && (this.panel.querySelector(".detail").textContent = i), this.panel.hidden = !1, !n) return Promise.resolve();
98
+ const o = this.panel.querySelector("button");
99
+ return o.textContent = n, o.focus(), new Promise((r) => o.addEventListener("click", () => r(), { once: !0 }));
100
+ }
101
+ hidePanel() {
102
+ this.panel.hidden = !0, this.panel.innerHTML = "";
103
+ }
104
+ }
105
+ const ie = { runtime: "liveness-runtime", runtimeCompat: "liveness-runtime-compat", model: "liveness-face.bin" }, ne = import.meta.url, ae = "assets/";
106
+ function oe(t) {
107
+ const e = new URL(t || ae, t ? document.baseURI : ne).href;
108
+ return e.endsWith("/") ? e : e + "/";
109
+ }
110
+ class L extends Error {
111
+ }
112
+ class O {
113
+ constructor(e) {
114
+ this.engine = e, this.lastTs = 0, this.inferenceMs = [];
115
+ }
116
+ static async create(e, s = 2e4) {
117
+ const i = oe(e), n = import("./liveness-device-C5ygqTPx.js").then((o) => o.createEngine(i, ie));
118
+ let a;
119
+ try {
120
+ const o = await Promise.race([n, new Promise((r, l) => {
121
+ a = setTimeout(() => l(new L("face tracker load timed out")), s);
122
+ })]);
123
+ return new O(o);
124
+ } catch (o) {
125
+ throw n.then((r) => r.close(), () => {
126
+ }), o instanceof L ? o : new L(String(o));
127
+ } finally {
128
+ clearTimeout(a);
129
+ }
130
+ }
131
+ detect(e, s) {
132
+ const i = performance.now(), n = Math.max(i, this.lastTs + 1);
133
+ this.lastTs = n;
134
+ const a = this.engine.detect(e, n, s);
135
+ return this.inferenceMs.push(performance.now() - i), this.inferenceMs.length > 60 && this.inferenceMs.shift(), a;
136
+ }
137
+ medianInferenceMs() {
138
+ if (this.inferenceMs.length < 10) return null;
139
+ const e = [...this.inferenceMs].sort((s, i) => s - i);
140
+ return e[Math.floor(e.length / 2)];
141
+ }
142
+ close() {
143
+ this.engine.close();
144
+ }
145
+ }
146
+ function P(t) {
147
+ return t.faceCount >= 1 && t.yaw !== void 0;
148
+ }
149
+ class re {
150
+ constructor(e = 0.5) {
151
+ this.alpha = e;
152
+ }
153
+ apply(e) {
154
+ return P(e) ? (this.yaw = this.yaw === void 0 ? e.yaw : this.alpha * e.yaw + (1 - this.alpha) * this.yaw, this.pitch = this.pitch === void 0 ? e.pitch : this.alpha * e.pitch + (1 - this.alpha) * this.pitch, { ...e, yaw: this.yaw, pitch: this.pitch }) : e;
155
+ }
156
+ }
157
+ function le(t) {
158
+ if (t.length === 0) return;
159
+ const e = [...t].sort((i, n) => i - n), s = Math.floor(e.length / 2);
160
+ return e.length % 2 ? e[s] : (e[s - 1] + e[s]) / 2;
161
+ }
162
+ const f = { turnHeadDeg: 20, nodDeg: 15, jawOpen: 0.4, smile: 0.5, blinkClosed: 0.6, blinkOpen: 0.25, blinkMaxMs: 600, poseHoldMs: 150, expressionHoldMs: 300, baselineMs: 300 };
163
+ class A {
164
+ constructor(e) {
165
+ this.ms = e, this.since = null, this.misses = 0;
166
+ }
167
+ update(e, s) {
168
+ return s ? (this.since === null && (this.since = e), this.misses = 0, e - this.since >= this.ms) : this.since !== null && this.misses === 0 ? (this.misses = 1, !1) : (this.since = null, this.misses = 0, !1);
169
+ }
170
+ }
171
+ class z {
172
+ constructor(e, s, i) {
173
+ this.axis = e, this.negative = s, this.threshold = i, this.passedAt = null, this.start = null, this.samples = [], this.extreme = 0, this.hold = new A(f.poseHoldMs), this.opposite = new A(f.poseHoldMs);
174
+ }
175
+ feed(e) {
176
+ if (this.passedAt !== null) return "passed";
177
+ if (this.start === null && (this.start = e.t), !P(e)) return this.hold.update(e.t, !1), this.opposite.update(e.t, !1), "pending";
178
+ const s = e[this.axis];
179
+ if (this.baseline === void 0) {
180
+ if (this.samples.push(s), e.t - this.start < f.baselineMs) return "pending";
181
+ this.baseline = le(this.samples);
182
+ }
183
+ const i = s - this.baseline;
184
+ (this.negative ? i < this.extreme : i > this.extreme) && (this.extreme = i);
185
+ const n = this.negative ? i <= -this.threshold : i >= this.threshold, a = this.negative ? i >= this.threshold : i <= -this.threshold;
186
+ return this.opposite.update(e.t, a) ? "wrong_direction" : this.hold.update(e.t, n) ? (this.passedAt = e.t, "passed") : "pending";
187
+ }
188
+ get peak() {
189
+ return this.axis === "yaw" ? { yaw: R(this.extreme) } : { pitch: R(this.extreme) };
190
+ }
191
+ get confidence() {
192
+ return U(Math.abs(this.extreme) / (this.threshold * 1.5));
193
+ }
194
+ }
195
+ class q {
196
+ constructor(e, s) {
197
+ this.attr = e, this.threshold = s, this.passedAt = null, this.highest = 0, this.hold = new A(f.expressionHoldMs);
198
+ }
199
+ feed(e) {
200
+ if (this.passedAt !== null) return "passed";
201
+ const s = P(e) ? e[this.attr] ?? 0 : 0;
202
+ return this.highest = Math.max(this.highest, s), this.hold.update(e.t, P(e) && s >= this.threshold) ? (this.passedAt = e.t, "passed") : "pending";
203
+ }
204
+ get peak() {
205
+ return { score: R(this.highest, 3) };
206
+ }
207
+ get confidence() {
208
+ return U(this.highest);
209
+ }
210
+ }
211
+ class ce {
212
+ constructor() {
213
+ this.passedAt = null, this.closedAt = null, this.highest = 0;
214
+ }
215
+ feed(e) {
216
+ if (this.passedAt !== null) return "passed";
217
+ if (!P(e)) return "pending";
218
+ if (this.highest = Math.max(this.highest, e.blink), e.blink >= f.blinkClosed) this.closedAt === null && (this.closedAt = e.t);
219
+ else if (this.closedAt !== null) {
220
+ if (e.t - this.closedAt > f.blinkMaxMs) this.closedAt = null;
221
+ else if (e.blink <= f.blinkOpen) return this.passedAt = this.closedAt, "passed";
222
+ }
223
+ return "pending";
224
+ }
225
+ get peak() {
226
+ return { score: R(this.highest, 3) };
227
+ }
228
+ get confidence() {
229
+ return U(this.highest);
230
+ }
231
+ }
232
+ function B(t, e) {
233
+ switch (t) {
234
+ case "turn_head":
235
+ return new z("yaw", e === "left", f.turnHeadDeg);
236
+ case "nod":
237
+ return new z("pitch", e === "up", f.nodDeg);
238
+ case "smile":
239
+ return new q("smile", f.smile);
240
+ case "open_mouth":
241
+ return new q("jawOpen", f.jawOpen);
242
+ case "blink":
243
+ return new ce();
244
+ }
245
+ }
246
+ const x = { holdMs: 800, center: 0.1, minHeight: 0.35, maxHeight: 0.7, maxAngle: 12 };
247
+ function de(t) {
248
+ return t.faceCount > 1 ? "multiple_faces" : P(t) ? t.boxHeight < x.minHeight ? "move_closer" : t.boxHeight > x.maxHeight ? "move_back" : Math.abs(t.centerX - 0.5) > x.center || Math.abs(t.centerY - 0.5) > x.center ? "center" : Math.abs(t.yaw) > x.maxAngle || Math.abs(t.pitch) > x.maxAngle ? "look_straight" : "ok" : "no_face";
249
+ }
250
+ class he {
251
+ constructor() {
252
+ this.hold = new A(x.holdMs);
253
+ }
254
+ feed(e) {
255
+ const s = de(e);
256
+ return { hint: s, ready: this.hold.update(e.t, s === "ok") };
257
+ }
258
+ }
259
+ function N(t) {
260
+ return P(t) && t.faceCount === 1 && Math.abs(t.yaw) < 10 && Math.abs(t.pitch) < 10 && t.blink < 0.3;
261
+ }
262
+ class ue {
263
+ constructor() {
264
+ this.multi = 0, this.lostSince = null;
265
+ }
266
+ feed(e) {
267
+ if (this.multi = e.faceCount > 1 ? this.multi + 1 : 0, this.multi >= 5) return "multiple_faces";
268
+ if (e.faceCount === 0) {
269
+ if (this.lostSince === null && (this.lostSince = e.t), e.t - this.lostSince >= 1500) return "face_lost";
270
+ } else this.lostSince = null;
271
+ return null;
272
+ }
273
+ }
274
+ function U(t) {
275
+ return Math.max(0, Math.min(1, t));
276
+ }
277
+ function R(t, e = 1) {
278
+ const s = 10 ** e;
279
+ return Math.round(t * s) / s;
280
+ }
281
+ const pe = "liveness-web", me = "0.1.0";
282
+ function fe(t) {
283
+ const e = navigator.userAgent, s = navigator.userAgentData;
284
+ return { schema_version: 1, liveness_session_id: t.session.liveness_session_id, sdk: { name: pe, version: me }, platform: { os: (s?.platform || navigator.platform || "").slice(0, 64) || null, device: null, browser: e.slice(0, 64) }, camera: { width: t.camera.width ?? null, height: t.camera.height ?? null, fps_measured: t.camera.frameRate ?? null, facing: "user" }, video: { mime: t.video.mime, duration_ms: t.video.durationMs, started_at_epoch_ms: t.video.startedAtEpochMs }, mode: t.session.mode, challenges: t.challenges, flash: t.flash, stills: t.stills, device_result: { passed: !0, aborted_reason: null, mode: t.mode, fallback_reason: t.fallbackReason, frames_total: t.tally.framesTotal, frames_with_face: t.tally.framesWithFace, frames_with_two_faces: t.tally.framesWithTwoFaces } };
285
+ }
286
+ class p extends Error {
287
+ constructor(e, s) {
288
+ super(s || e), this.code = e;
289
+ }
290
+ }
291
+ async function we() {
292
+ if (!window.isSecureContext) throw new p("insecure_context", "The camera needs an https page");
293
+ if (!navigator.mediaDevices?.getUserMedia) throw new p("unsupported_browser");
294
+ if (typeof MediaRecorder > "u") throw new p("unsupported_browser");
295
+ try {
296
+ return await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user", width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 30 } }, audio: !1 });
297
+ } catch (t) {
298
+ const e = t?.name;
299
+ throw e === "NotAllowedError" || e === "SecurityError" ? new p("camera_denied") : new p("camera_unavailable", e);
300
+ }
301
+ }
302
+ function ge(t) {
303
+ t?.getTracks().forEach((e) => e.stop());
304
+ }
305
+ function ve(t = (e) => MediaRecorder.isTypeSupported(e)) {
306
+ return ["video/mp4;codecs=avc1.42E01E", "video/mp4", "video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"].find((e) => t(e)) || "";
307
+ }
308
+ class be {
309
+ constructor(e, s = 12e5) {
310
+ this.chunks = [], this.t0 = 0, this.epoch = 0;
311
+ const i = ve();
312
+ this.recorder = new MediaRecorder(e, i ? { mimeType: i, videoBitsPerSecond: s } : { videoBitsPerSecond: s }), this.recorder.ondataavailable = (n) => {
313
+ n.data && n.data.size && this.chunks.push(n.data);
314
+ };
315
+ }
316
+ async start() {
317
+ const e = new Promise((s) => this.recorder.addEventListener("start", () => s(), { once: !0 }));
318
+ this.stopped = new Promise((s) => this.recorder.addEventListener("stop", () => s(), { once: !0 })), this.recorder.start(1e3), await e, this.t0 = performance.now(), this.epoch = Date.now();
319
+ }
320
+ now() {
321
+ return Math.round(performance.now() - this.t0);
322
+ }
323
+ async stop() {
324
+ this.recorder.state !== "inactive" && this.recorder.stop(), await this.stopped;
325
+ const e = (this.recorder.mimeType || "video/webm").split(";")[0];
326
+ return new Blob(this.chunks, { type: e });
327
+ }
328
+ abort() {
329
+ try {
330
+ this.recorder.state !== "inactive" && this.recorder.stop();
331
+ } catch {
332
+ }
333
+ }
334
+ }
335
+ async function ye(t, e) {
336
+ const s = t.videoWidth, i = t.videoHeight, n = Math.min(1, 1280 / Math.max(s, i)), a = document.createElement("canvas");
337
+ a.width = Math.round(s * n), a.height = Math.round(i * n), a.getContext("2d").drawImage(t, 0, 0, a.width, a.height);
338
+ let o = 0.9;
339
+ for (; ; ) {
340
+ const r = await new Promise((l) => a.toBlob(l, "image/jpeg", o));
341
+ if (!r) throw new p("capture_failed");
342
+ if (r.size <= e || o <= 0.45) return r;
343
+ o -= 0.1;
344
+ }
345
+ }
346
+ const M = (t) => new Promise((e) => setTimeout(e, t)), V = () => new Promise((t) => requestAnimationFrame(t)), ke = 120, _e = 700, xe = 400, Me = 1e3, Pe = 5e3, Ce = 2200;
347
+ class C extends Error {
348
+ }
349
+ function H(t, e) {
350
+ switch (t.type) {
351
+ case "turn_head":
352
+ return t.direction === "left" ? e.turnLeft : e.turnRight;
353
+ case "nod":
354
+ return t.direction === "up" ? e.lookUp : e.lookDown;
355
+ case "smile":
356
+ return e.smile;
357
+ case "blink":
358
+ return e.blink;
359
+ case "open_mouth":
360
+ return e.openMouth;
361
+ }
362
+ }
363
+ function Te(t, e) {
364
+ switch (t) {
365
+ case "ok":
366
+ return e.lookStraight;
367
+ case "no_face":
368
+ return e.noFace;
369
+ case "multiple_faces":
370
+ return e.multipleFaces;
371
+ case "move_closer":
372
+ return e.moveCloser;
373
+ case "move_back":
374
+ return e.moveBack;
375
+ case "center":
376
+ return e.center;
377
+ case "look_straight":
378
+ return e.lookStraight;
379
+ }
380
+ }
381
+ async function Se(t) {
382
+ const { session: e, view: s, strings: i } = t, n = t.mode === "device" && t.tracker !== null, a = new re(), o = { framesTotal: 0, framesWithFace: 0, framesWithTwoFaces: 0 };
383
+ let r = null;
384
+ const l = () => {
385
+ if (t.isCancelled()) throw new C();
386
+ }, w = () => new Promise((h) => {
387
+ const d = s.video;
388
+ d.requestVideoFrameCallback ? d.requestVideoFrameCallback(() => h()) : requestAnimationFrame(() => h()), setTimeout(h, 250);
389
+ }), b = async (h, d) => {
390
+ await w(), l();
391
+ const m = a.apply(t.tracker.detect(s.video, h));
392
+ return d && (o.framesTotal++, m.faceCount >= 1 && o.framesWithFace++, m.faceCount >= 2 && o.framesWithTwoFaces++), m;
393
+ };
394
+ try {
395
+ let h = null;
396
+ if (n) {
397
+ const v = new he(), y = performance.now();
398
+ let c = 0;
399
+ for (; ; ) {
400
+ h = await b(performance.now() - y, !1), c++;
401
+ const S = t.tracker.medianInferenceMs();
402
+ if (c >= 15 && S !== null && S > ke) return { kind: "fallback", reason: "slow_device" };
403
+ const { hint: _, ready: E } = v.feed(h);
404
+ if (s.setPrompt(Te(_, i)), s.setOval(_ === "ok" ? "ok" : "warn"), E) break;
405
+ }
406
+ }
407
+ r = new be(t.stream), await r.start(), t.emit({ type: "recording_started" });
408
+ const d = e.limits.max_video_seconds * 1e3, m = [], T = [], u = async (v) => {
409
+ T.push({ index: m.length, captured_at_ms: r.now(), reason: v }), m.push(await ye(s.video, e.limits.max_still_bytes));
410
+ };
411
+ if (n) {
412
+ const v = performance.now() + 1e3;
413
+ for (; h && !N(h) && performance.now() < v; ) h = await b(r.now(), !0);
414
+ await u("positioning");
415
+ } else s.setOval("idle"), s.setPrompt(i.lookStraight), await M(1500), l(), await u("positioning");
416
+ const g = [], k = [], D = e.challenges.length;
417
+ if (e.mode === "active") {
418
+ const v = new ue();
419
+ for (const [y, c] of e.challenges.entries()) {
420
+ s.setProgress(D, y, y), s.setPrompt(H(c, i)), s.setOval("idle"), t.emit({ type: "challenge_started", challenge: c });
421
+ const S = r.now();
422
+ if (n) {
423
+ let _ = B(c.type, c.direction), E = 1, W = performance.now();
424
+ for (; ; ) {
425
+ const I = await b(r.now(), !0), j = v.feed(I);
426
+ if (j) return { kind: "aborted", reason: j };
427
+ if (r.now() > d - Pe) return { kind: "aborted", reason: "too_slow" };
428
+ const $ = _.feed(I);
429
+ if ($ === "wrong_direction") return { kind: "aborted", reason: "wrong_direction" };
430
+ if ($ === "passed") {
431
+ g.push({ order: c.order, type: c.type, direction: c.direction, start_ms: S, end_ms: r.now(), passed: !0, confidence: Ae(_.confidence), timed_out: E > 1, peak: _.peak }), k.push(c.type), s.setOval("ok"), s.setPrompt(i.great), s.setProgress(D, y + 1, null), t.emit({ type: "challenge_passed", challenge: c }), await M(xe);
432
+ break;
433
+ }
434
+ if (performance.now() - W > c.timeout_ms) {
435
+ if (E > 1) return { kind: "aborted", reason: `challenge_timeout:${c.type}` };
436
+ E = 2, _ = B(c.type, c.direction), W = performance.now(), s.setPrompt(H(c, i));
437
+ }
438
+ }
439
+ } else await G(s, t, t.timedWindowMs, H(c, i)), g.push({ order: c.order, type: c.type, direction: c.direction, start_ms: S, end_ms: r.now(), passed: null, confidence: null, timed_out: !1, peak: null }), s.setProgress(D, y + 1, null), s.setPrompt(i.lookStraight), await M(_e), l();
440
+ }
441
+ } else await G(s, t, Ce, i.holdStill);
442
+ const Y = await Ee(t, r);
443
+ if (s.setFlash(null), s.setPrompt(i.lookStraight), n) {
444
+ const v = performance.now() + 2e3;
445
+ for (; ; ) {
446
+ const y = await b(r.now(), !0);
447
+ if (N(y) || performance.now() > v) break;
448
+ }
449
+ } else await M(1500), l();
450
+ await u("return_to_centre");
451
+ const K = r.now(), Q = r.epoch, F = await r.stop();
452
+ if (r = null, F.size > e.limits.max_video_bytes) throw new p("recording_too_large");
453
+ const Z = fe({ session: e, mode: t.mode, fallbackReason: t.fallbackReason, camera: t.stream.getVideoTracks()[0]?.getSettings() ?? {}, video: { mime: F.type, durationMs: K, startedAtEpochMs: Q }, challenges: g, flash: Y, stills: T, tally: o });
454
+ return { kind: "recorded", video: F, stills: m, metadata: Z, passedChallenges: k };
455
+ } finally {
456
+ r?.abort(), s.setFlash(null), s.setCountdown(null), s.setDebug(null);
457
+ }
458
+ }
459
+ async function G(t, e, s, i) {
460
+ t.setPrompt(i);
461
+ const n = performance.now() + s;
462
+ for (; performance.now() < n; ) {
463
+ if (e.isCancelled()) throw new C();
464
+ t.setCountdown(Math.ceil((n - performance.now()) / 1e3)), await M(100);
465
+ }
466
+ t.setCountdown(null);
467
+ }
468
+ async function Ee(t, e) {
469
+ const s = t.session.flash, i = { enabled: !1, started_at_ms: null, segments: [], screen_brightness: null, exposure_locked: !1, skipped_reason: null };
470
+ if (!s.enabled) return i;
471
+ if (!t.allowFlash) return { ...i, skipped_reason: "disabled_by_host" };
472
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return { ...i, skipped_reason: "reduced_motion" };
473
+ if (t.emit({ type: "flash_started" }), t.view.setFlash("#000000"), t.view.setPrompt(t.strings.flashIntro), await M(Me), t.isCancelled()) throw new C();
474
+ t.view.setPrompt(""), i.enabled = !0;
475
+ for (const n of s.segments) {
476
+ await V(), t.view.setFlash(n.color);
477
+ const a = await V(), o = Math.round(a - e.t0);
478
+ i.started_at_ms === null && (i.started_at_ms = o), await M(Math.max(0, n.duration_ms - (performance.now() - a))), i.segments.push({ color: n.color, start_ms: o, end_ms: e.now() });
479
+ }
480
+ return t.view.setFlash(null), t.emit({ type: "flash_ended" }), i;
481
+ }
482
+ function Ae(t) {
483
+ return Math.round(t * 100) / 100;
484
+ }
485
+ const Re = 6e4;
486
+ async function De(t, e, s) {
487
+ const i = new FormData();
488
+ i.append("video", s.video, s.video.type === "video/mp4" ? "attempt.mp4" : "attempt.webm"), s.stills.forEach((a, o) => i.append("stills", a, `${o}.jpg`)), i.append("metadata", JSON.stringify(s.metadata)), i.append("token", e.token);
489
+ const n = t.replace(/\/+$/, "") + e.upload_url;
490
+ for (let a = 1; ; a++) {
491
+ const o = new AbortController(), r = setTimeout(() => o.abort(), Re);
492
+ let l;
493
+ try {
494
+ l = await fetch(n, { method: "POST", body: i, signal: o.signal });
495
+ } catch (b) {
496
+ if (clearTimeout(r), a < 2) continue;
497
+ throw new p("network", String(b));
498
+ }
499
+ clearTimeout(r);
500
+ const w = await l.json().catch(() => ({}));
501
+ if (l.ok) return w;
502
+ throw new p(Fe(l.status), Le(w) || `HTTP ${l.status}`);
503
+ }
504
+ }
505
+ function Fe(t) {
506
+ switch (t) {
507
+ case 401:
508
+ return "session_invalid";
509
+ case 409:
510
+ return "already_passed";
511
+ case 410:
512
+ return "session_expired";
513
+ case 413:
514
+ return "recording_too_large";
515
+ case 422:
516
+ return "recording_rejected";
517
+ case 429:
518
+ return "attempts_exhausted";
519
+ case 503:
520
+ return "service_unavailable";
521
+ default:
522
+ return "server_error";
523
+ }
524
+ }
525
+ function Le(t) {
526
+ const e = t?.detail;
527
+ return e && typeof e == "object" && "error" in e ? String(e.error) : typeof e == "string" ? e : null;
528
+ }
529
+ const He = { wrong_direction: "wrongDirection", face_lost: "faceLost", multiple_faces: "multipleFaces", too_slow: "tooSlow" }, Oe = { camera_denied: "cameraDenied", camera_unavailable: "cameraUnavailable", unsupported_browser: "unsupportedBrowser", insecure_context: "insecureContext", network: "network", session_expired: "sessionExpired", session_invalid: "sessionExpired", service_unavailable: "serviceUnavailable", attempts_exhausted: "noAttemptsDetail" };
530
+ class Ue {
531
+ constructor(e) {
532
+ if (this.config = e, this.view = null, this.stream = null, this.tracker = null, this.cancelled = !1, this.running = !1, this.wakeOnCancel = null, !e.baseUrl) throw new Error("PowerCredLiveness: baseUrl is required");
533
+ if (!e.session && !e.sessionProvider) throw new Error("PowerCredLiveness: pass session or sessionProvider");
534
+ this.strings = { ...ee, ...e.strings || {} };
535
+ }
536
+ async start(e) {
537
+ if (this.running) throw new Error("PowerCredLiveness: already running");
538
+ this.running = !0, this.cancelled = !1;
539
+ const s = this.strings, i = this.view = new se(this.config.theme);
540
+ i.mount(e), i.setPrompt(s.gettingReady);
541
+ let n = null, a = null;
542
+ try {
543
+ if (n = this.config.session ?? await this.config.sessionProvider(), Date.parse(n.expires_at) < Date.now()) throw new p("session_expired");
544
+ const o = n.client_checks === "none" ? "none" : "device", r = o === "device" ? O.create(this.config.assetsUrl).catch(() => null) : Promise.resolve(null);
545
+ this.stream = await we(), await i.attach(this.stream), this.emit({ type: "camera_ready" }), this.tracker = await r;
546
+ let l = o === "device" && this.tracker ? "device" : "none", w = o === "device" && !this.tracker ? "model_unavailable" : null;
547
+ this.emit({ type: "mode", mode: l, fallbackReason: w });
548
+ const b = this.config.maxLocalRetries ?? 2;
549
+ let h = 0;
550
+ for (; ; ) {
551
+ const d = await Se({ session: n, view: i, strings: s, stream: this.stream, tracker: l === "device" ? this.tracker : null, mode: l, fallbackReason: w, allowFlash: this.config.flash !== !1, timedWindowMs: Math.round((this.config.timedWindowSeconds ?? 3.5) * 1e3), debug: !1, emit: (g) => this.emit(g), isCancelled: () => this.cancelled });
552
+ if (d.kind === "fallback") {
553
+ this.tracker?.close(), this.tracker = null, l = "none", w = d.reason, this.emit({ type: "mode", mode: l, fallbackReason: w });
554
+ continue;
555
+ }
556
+ if (a = { mode: l, fallbackReason: w, passedChallenges: d.kind === "recorded" ? d.passedChallenges : [] }, d.kind === "aborted") {
557
+ this.emit({ type: "attempt_aborted", reason: d.reason }), h++;
558
+ const g = s[He[d.reason] ?? "timeUp"];
559
+ if (h > b) return this.config.showResult !== !1 && await i.showPanel("bad", s.failedTitle, s.failedDetail), this.finish({ status: "failed", livenessSessionId: n.liveness_session_id, device: a, reason: d.reason });
560
+ await this.untilCancelled(i.showPanel("bad", g, "", s.tryAgain)), i.hidePanel();
561
+ continue;
562
+ }
563
+ const m = d.stills[d.stills.length - 1], T = () => De(this.config.baseUrl, n, d);
564
+ if (this.emit({ type: "uploading", bytes: d.video.size }), this.config.waitForServer === !1) {
565
+ const g = n.liveness_session_id;
566
+ return T().then((k) => this.serverResult(k), (k) => this.emit({ type: "error", code: X(k), message: J(k) })), this.config.showResult !== !1 && await i.showPanel("ok", s.submittedTitle), this.finish({ status: "submitted", livenessSessionId: g, device: a, selfie: m });
567
+ }
568
+ await i.showPanel("working", s.checking);
569
+ const u = await T();
570
+ if (this.serverResult(u), u.status === "passed") return this.config.showResult !== !1 && await i.showPanel("ok", s.passedTitle, s.passedDetail), this.finish({ status: "passed", livenessSessionId: u.liveness_session_id, attempt: u.attempt, device: a, server: u, retryAllowed: !1, selfie: m });
571
+ if (u.retry_allowed) {
572
+ await this.untilCancelled(i.showPanel("bad", s.failedTitle, s.failedDetail, s.tryAgain)), i.hidePanel();
573
+ continue;
574
+ }
575
+ return this.config.showResult !== !1 && await i.showPanel("bad", s.failedTitle, s.noAttemptsDetail), this.finish({ status: u.status === "inconclusive" ? "inconclusive" : "failed", livenessSessionId: u.liveness_session_id, attempt: u.attempt, device: a, server: u, retryAllowed: !1, selfie: m });
576
+ }
577
+ } catch (o) {
578
+ if (o instanceof C) return this.finish({ status: "cancelled", livenessSessionId: n?.liveness_session_id ?? null, device: a, reason: "cancelled" });
579
+ const r = X(o);
580
+ this.emit({ type: "error", code: r, message: J(o) });
581
+ const l = s[Oe[r] ?? "genericError"];
582
+ return this.config.showResult !== !1 && await i.showPanel("bad", l), this.finish({ status: "error", livenessSessionId: n?.liveness_session_id ?? null, device: a, reason: r });
583
+ }
584
+ }
585
+ cancel() {
586
+ this.cancelled = !0, this.wakeOnCancel?.();
587
+ }
588
+ untilCancelled(e) {
589
+ return new Promise((s, i) => {
590
+ if (this.cancelled) return i(new C());
591
+ this.wakeOnCancel = () => i(new C()), e.then(() => {
592
+ this.wakeOnCancel = null, s();
593
+ });
594
+ });
595
+ }
596
+ destroy() {
597
+ this.cancel(), this.release(), this.view?.unmount(), this.view = null;
598
+ }
599
+ finish(e) {
600
+ return this.release(), this.running = !1, e;
601
+ }
602
+ release() {
603
+ ge(this.stream), this.stream = null, this.tracker?.close(), this.tracker = null;
604
+ }
605
+ serverResult(e) {
606
+ this.emit({ type: "server_result", result: e });
607
+ try {
608
+ this.config.onServerResult?.(e);
609
+ } catch {
610
+ }
611
+ }
612
+ emit(e) {
613
+ try {
614
+ this.config.onEvent?.(e);
615
+ } catch {
616
+ }
617
+ }
618
+ }
619
+ function X(t) {
620
+ return t instanceof p ? t.code : "unexpected";
621
+ }
622
+ function J(t) {
623
+ return t instanceof p ? t.message : "Unexpected error";
624
+ }
625
+ const We = "0.1.0";
626
+ export {
627
+ Ue as PowerCredLiveness,
628
+ We as VERSION,
629
+ ee as defaultStrings
630
+ };
package/dist/sdk.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * PowerCredLiveness: mounts the check into a container, runs it, uploads, and resolves with the
3
+ * outcome. The server's verdict is the only one that counts; the device's checks only guide the
4
+ * user and catch problems early.
5
+ */
6
+ import type { LivenessConfig, LivenessResult } from './types';
7
+ export declare class PowerCredLiveness {
8
+ private readonly config;
9
+ private view;
10
+ private stream;
11
+ private tracker;
12
+ private cancelled;
13
+ private running;
14
+ private wakeOnCancel;
15
+ private readonly strings;
16
+ constructor(config: LivenessConfig);
17
+ /** Runs the check inside `container`. Resolves once; never rejects for user-facing problems. */
18
+ start(container: HTMLElement): Promise<LivenessResult>;
19
+ /** Stops a running check (also while it waits on a "Try again" button); start() then resolves "cancelled". */
20
+ cancel(): void;
21
+ /** Waits for a user action, unless the host cancels first. */
22
+ private untilCancelled;
23
+ /** Releases the camera and removes the UI. */
24
+ destroy(): void;
25
+ private finish;
26
+ private release;
27
+ private serverResult;
28
+ private emit;
29
+ }