@tn3w/openage 1.0.4 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tn3w/openage",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Privacy-first age verification widget. On-device face analysis with liveness detection.",
5
5
  "type": "module",
6
6
  "main": "dist/openage.umd.js",
package/src/challenge.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  import {
28
28
  BURST_FRAMES,
29
29
  BURST_INTERVAL_MS,
30
+ ERROR_STEP_SECONDS,
30
31
  MAX_RETRIES,
31
32
  MOTION_CAPTURE_MS,
32
33
  MOTION_SAMPLE_MS,
@@ -110,6 +111,64 @@ function sleep(ms) {
110
111
  return new Promise((r) => setTimeout(r, ms));
111
112
  }
112
113
 
114
+ function resolveChallengeErrorMessage(error) {
115
+ const name = typeof error?.name === 'string' ? error.name : '';
116
+ const message = typeof error?.message === 'string' ? error.message : String(error || '');
117
+ const normalized = `${name} ${message}`.toLowerCase();
118
+
119
+ if (
120
+ name === 'NotFoundError' ||
121
+ /request is not allowed by the user agent or the platform in the current context/.test(
122
+ normalized
123
+ ) ||
124
+ /requested device not found|device not found|no camera|could not start video source/.test(
125
+ normalized
126
+ )
127
+ ) {
128
+ return 'No camera available. Plug in a camera and try again.';
129
+ }
130
+
131
+ if (
132
+ name === 'NotAllowedError' ||
133
+ name === 'PermissionDeniedError' ||
134
+ /permission|camera access|access denied/.test(normalized)
135
+ ) {
136
+ return 'Camera access was blocked. Allow camera access and try again.';
137
+ }
138
+
139
+ if (/positioning timeout/.test(normalized)) {
140
+ return 'Face positioning timed out. Reopen the check and try again.';
141
+ }
142
+
143
+ return 'Verification failed. Please try again.';
144
+ }
145
+
146
+ async function showErrorStep(widget, error) {
147
+ const message = resolveChallengeErrorMessage(error);
148
+
149
+ if (!widget.popup) {
150
+ widget.openPopup?.();
151
+ }
152
+
153
+ widget.showError?.(message);
154
+
155
+ for (let seconds = ERROR_STEP_SECONDS; seconds > 0; seconds--) {
156
+ if (!widget.popup) break;
157
+ widget.setErrorCountdown?.(seconds);
158
+ await sleep(1000);
159
+ }
160
+
161
+ widget.closePopup?.();
162
+ widget.setState?.('retry');
163
+ }
164
+
165
+ async function handleChallengeError(widget, emitter, error) {
166
+ console.log('Error during challenge:', error);
167
+ await showErrorStep(widget, error);
168
+ emitter.emit('error', error, widget.id);
169
+ widget.params.errorCallback?.(error);
170
+ }
171
+
113
172
  export async function runChallenge(widget, emitter) {
114
173
  const mode = widget.params.mode || 'serverless';
115
174
 
@@ -190,10 +249,7 @@ async function runServerless(widget, emitter) {
190
249
  emitResult(widget, emitter, result);
191
250
  } catch (error) {
192
251
  cleanupLocal();
193
- console.log('Error during challenge:', error);
194
- widget.showResult('fail', 'Verification failed');
195
- emitter.emit('error', error, widget.id);
196
- params.errorCallback?.(error);
252
+ await handleChallengeError(widget, emitter, error);
197
253
  }
198
254
  }
199
255
 
@@ -324,11 +380,8 @@ async function runServer(widget, emitter) {
324
380
 
325
381
  cleanupVM(transport);
326
382
  } catch (error) {
327
- console.log('Error during challenge:', error);
328
383
  cleanupVM();
329
- widget.showResult('fail', 'Verification failed');
330
- emitter.emit('error', error, widget.id);
331
- params.errorCallback?.(error);
384
+ await handleChallengeError(widget, emitter, error);
332
385
  }
333
386
  }
334
387
 
@@ -8,6 +8,7 @@ export declare const FACEAPI_MODEL_CDN: string;
8
8
  export declare const DEFAULT_MIN_AGE: number;
9
9
  export declare const FAIL_FLOOR: number;
10
10
  export declare const MAX_RETRIES: number;
11
+ export declare const ERROR_STEP_SECONDS: number;
11
12
  export declare const BURST_FRAMES: number;
12
13
  export declare const BURST_INTERVAL_MS: number;
13
14
  export declare const POSITION_CHECK_MS: number;
package/src/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const VERSION = '1.0.0';
2
2
 
3
- export const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/' + '@mediapipe/tasks-vision@0.10.17';
3
+ export const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.17';
4
4
 
5
5
  export const MEDIAPIPE_WASM = `${MEDIAPIPE_CDN}/wasm`;
6
6
 
@@ -12,12 +12,13 @@ export const MEDIAPIPE_MODEL =
12
12
  'face_landmarker.task';
13
13
 
14
14
  export const FACEAPI_CDN =
15
- 'https://cdn.jsdelivr.net/npm/' + 'face-api.js@0.22.2/dist/face-api.min.js';
15
+ 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
16
16
 
17
17
  export const FACEAPI_MODEL_CDN =
18
- 'https://cdn.jsdelivr.net/gh/' + 'justadudewhohacks/face-api.js@master/weights';
18
+ 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights';
19
19
 
20
20
  export const MAX_RETRIES = 3;
21
+ export const ERROR_STEP_SECONDS = 5;
21
22
  export const BURST_FRAMES = 5;
22
23
  export const BURST_INTERVAL_MS = 200;
23
24
  export const POSITION_CHECK_MS = 100;
package/src/ui.d.ts CHANGED
@@ -17,6 +17,10 @@ export declare function heroTemplate(
17
17
 
18
18
  export declare function challengeTemplate(): string;
19
19
 
20
+ export declare function errorStepTemplate(
21
+ message: string
22
+ ): string;
23
+
20
24
  export declare function resultTemplate(
21
25
  outcome: "fail" | "retry",
22
26
  message: string
package/src/ui.js CHANGED
@@ -893,6 +893,52 @@ export const STYLES = `
893
893
  .oa-result-fail { color: var(--oa-danger); }
894
894
  .oa-result-retry { color: var(--oa-warn); }
895
895
 
896
+ .oa-error-step {
897
+ display: flex;
898
+ flex-direction: column;
899
+ align-items: center;
900
+ gap: 0.7rem;
901
+ padding: 2rem 1.2rem;
902
+ background: var(--oa-surface);
903
+ border: 1px solid var(--oa-border);
904
+ border-radius: var(--oa-radius);
905
+ margin: 10px;
906
+ animation: oa-fade-in 0.4s ease;
907
+ text-align: center;
908
+ }
909
+
910
+ .oa-error-step-icon {
911
+ width: 3rem;
912
+ height: 3rem;
913
+ display: flex;
914
+ align-items: center;
915
+ justify-content: center;
916
+ border-radius: 999px;
917
+ background: rgba(239, 68, 68, 0.12);
918
+ color: var(--oa-danger);
919
+ font-size: 1.7rem;
920
+ line-height: 1;
921
+ }
922
+
923
+ .oa-error-step-title {
924
+ font-size: 1rem;
925
+ font-weight: 700;
926
+ color: var(--oa-text);
927
+ }
928
+
929
+ .oa-error-step-message {
930
+ font-size: 0.84rem;
931
+ line-height: 1.5;
932
+ color: var(--oa-text-muted);
933
+ max-width: 260px;
934
+ }
935
+
936
+ .oa-error-step-countdown {
937
+ font-size: 0.75rem;
938
+ font-weight: 600;
939
+ color: var(--oa-danger);
940
+ }
941
+
896
942
  .oa-hidden { display: none !important; }
897
943
 
898
944
  /* ── Animations ──────────────────────────────── */
@@ -1019,6 +1065,17 @@ export function challengeTemplate() {
1019
1065
  `;
1020
1066
  }
1021
1067
 
1068
+ export function errorStepTemplate(message) {
1069
+ return `
1070
+ <div class="oa-error-step">
1071
+ <div class="oa-error-step-icon">✕</div>
1072
+ <div class="oa-error-step-title">Verification stopped</div>
1073
+ <div class="oa-error-step-message">${message}</div>
1074
+ <div class="oa-error-step-countdown"></div>
1075
+ </div>
1076
+ `;
1077
+ }
1078
+
1022
1079
  export function resultTemplate(outcome, message) {
1023
1080
  const icons = {
1024
1081
  fail: '✕',
package/src/widget.d.ts CHANGED
@@ -46,6 +46,7 @@ export declare class Widget {
46
46
  message: string
47
47
  ): void;
48
48
  showError(message: string): void;
49
+ setErrorCountdown(seconds: number): void;
49
50
  clearError(): void;
50
51
  setState(state: string): void;
51
52
  getToken(): string | null;
package/src/widget.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  checkboxTemplate,
4
4
  heroTemplate,
5
5
  challengeTemplate,
6
+ errorStepTemplate,
6
7
  resultTemplate,
7
8
  resolveTheme,
8
9
  watchTheme,
@@ -476,8 +477,20 @@ export class Widget {
476
477
  }
477
478
  }
478
479
 
479
- showError() {
480
- this.setState('retry');
480
+ showError(message) {
481
+ if (!this.popupElements?.body) return;
482
+
483
+ this.popupElements.body.innerHTML = errorStepTemplate(message);
484
+ this.popupElements.errorCountdown =
485
+ this.popupElements.body.querySelector('.oa-error-step-countdown');
486
+ this.hideActions();
487
+ }
488
+
489
+ setErrorCountdown(seconds) {
490
+ if (!this.popupElements?.errorCountdown) return;
491
+
492
+ const unit = seconds === 1 ? 'second' : 'seconds';
493
+ this.popupElements.errorCountdown.textContent = `Closing in ${seconds} ${unit}…`;
481
494
  }
482
495
 
483
496
  clearError() {