@tn3w/openage 1.0.5 → 1.0.6
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/README.md +22 -6
- package/dist/openage.esm.js +144 -18
- package/dist/openage.esm.js.map +1 -1
- package/dist/openage.min.js +1 -1
- package/dist/openage.min.js.map +1 -1
- package/dist/openage.umd.js +144 -18
- package/dist/openage.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/challenge.js +25 -6
- package/src/constants.js +1 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +17 -0
- package/src/ui.js +14 -0
- package/src/widget.d.ts +1 -0
- package/src/widget.js +87 -10
package/README.md
CHANGED
|
@@ -16,11 +16,11 @@ button binding.
|
|
|
16
16
|
|
|
17
17
|
## At a Glance
|
|
18
18
|
|
|
19
|
-
| Browser-side | Server-backed | UI
|
|
20
|
-
| ----------------------- | -------------------------- |
|
|
21
|
-
| On-device face analysis | Optional WASM verification |
|
|
22
|
-
| No raw camera upload | Signed sessions and tokens | Normal, compact, invisible
|
|
23
|
-
| Serverless soft gates | Hosted or custom backend | Auto, light, dark
|
|
19
|
+
| Browser-side | Server-backed | UI |
|
|
20
|
+
| ----------------------- | -------------------------- | -------------------------------- |
|
|
21
|
+
| On-device face analysis | Optional WASM verification | Widget popup, inline embed, bind |
|
|
22
|
+
| No raw camera upload | Signed sessions and tokens | Normal, compact, invisible |
|
|
23
|
+
| Serverless soft gates | Hosted or custom backend | Auto, light, dark |
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
@@ -54,12 +54,27 @@ import OpenAge from '@tn3w/openage';
|
|
|
54
54
|
|
|
55
55
|
OpenAge.render('#gate', {
|
|
56
56
|
mode: 'serverless',
|
|
57
|
+
layout: 'inline',
|
|
57
58
|
minAge: 18,
|
|
58
59
|
callback: (token) => console.log(token),
|
|
59
60
|
errorCallback: (error) => console.error(error),
|
|
60
61
|
});
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
### Inline Embed
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
OpenAge.render('#gate', {
|
|
68
|
+
mode: 'serverless',
|
|
69
|
+
layout: 'inline',
|
|
70
|
+
minAge: 18,
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`layout: 'inline'` removes the checkbox shell and renders the verification
|
|
75
|
+
panel directly in the container. The first verification step starts
|
|
76
|
+
immediately after loading.
|
|
77
|
+
|
|
63
78
|
### Bound Flow
|
|
64
79
|
|
|
65
80
|
```js
|
|
@@ -104,6 +119,7 @@ the popup automatically after 5 seconds.
|
|
|
104
119
|
| Param | Values |
|
|
105
120
|
| --------- | --------------------------------- |
|
|
106
121
|
| `mode` | `serverless`, `sitekey`, `custom` |
|
|
122
|
+
| `layout` | `widget`, `inline` |
|
|
107
123
|
| `theme` | `light`, `dark`, `auto` |
|
|
108
124
|
| `size` | `normal`, `compact`, `invisible` |
|
|
109
125
|
| `minAge` | number, default `18` |
|
|
@@ -122,7 +138,7 @@ python server.py
|
|
|
122
138
|
```
|
|
123
139
|
|
|
124
140
|
The repository also includes `demo/`, a minimal GitHub Pages build that loads
|
|
125
|
-
the jsDelivr bundle for `@tn3w/openage` in embedded `serverless` mode.
|
|
141
|
+
the jsDelivr bundle for `@tn3w/openage` in inline embedded `serverless` mode.
|
|
126
142
|
|
|
127
143
|
## Development
|
|
128
144
|
|
package/dist/openage.esm.js
CHANGED
|
@@ -541,6 +541,20 @@ const STYLES = `
|
|
|
541
541
|
flex-direction: column;
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
.oa-inline-shell {
|
|
545
|
+
background: var(--oa-bg);
|
|
546
|
+
border: 1px solid var(--oa-border);
|
|
547
|
+
border-radius: var(--oa-radius);
|
|
548
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.18),
|
|
549
|
+
0 0 0 1px rgba(0,0,0,0.06);
|
|
550
|
+
width: min(100%, 360px);
|
|
551
|
+
max-width: 100%;
|
|
552
|
+
overflow: hidden;
|
|
553
|
+
display: flex;
|
|
554
|
+
flex-direction: column;
|
|
555
|
+
margin: 0 auto;
|
|
556
|
+
}
|
|
557
|
+
|
|
544
558
|
.oa-modal-overlay {
|
|
545
559
|
position: fixed;
|
|
546
560
|
inset: 0;
|
|
@@ -1108,8 +1122,7 @@ const MEDIAPIPE_MODEL =
|
|
|
1108
1122
|
'face_landmarker/face_landmarker/float16/1/' +
|
|
1109
1123
|
'face_landmarker.task';
|
|
1110
1124
|
|
|
1111
|
-
const FACEAPI_CDN =
|
|
1112
|
-
'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
|
|
1125
|
+
const FACEAPI_CDN = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
|
|
1113
1126
|
|
|
1114
1127
|
const FACEAPI_MODEL_CDN =
|
|
1115
1128
|
'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights';
|
|
@@ -1146,6 +1159,7 @@ class Widget {
|
|
|
1146
1159
|
this.popup = null;
|
|
1147
1160
|
this.shadow = null;
|
|
1148
1161
|
this.elements = {};
|
|
1162
|
+
this.popupElements = null;
|
|
1149
1163
|
this.onChallenge = null;
|
|
1150
1164
|
this.onStartClick = null;
|
|
1151
1165
|
this.popupFrame = 0;
|
|
@@ -1158,6 +1172,7 @@ class Widget {
|
|
|
1158
1172
|
const host = document.createElement('div');
|
|
1159
1173
|
host.id = this.id;
|
|
1160
1174
|
this.shadow = host.attachShadow({ mode: 'open' });
|
|
1175
|
+
this.host = host;
|
|
1161
1176
|
|
|
1162
1177
|
const style = document.createElement('style');
|
|
1163
1178
|
style.textContent = STYLES;
|
|
@@ -1170,7 +1185,12 @@ class Widget {
|
|
|
1170
1185
|
if (this.params.size === 'invisible') {
|
|
1171
1186
|
host.style.display = 'none';
|
|
1172
1187
|
this.container.appendChild(host);
|
|
1173
|
-
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (this.isInlineLayout()) {
|
|
1192
|
+
this.renderInlineShell();
|
|
1193
|
+
this.container.appendChild(host);
|
|
1174
1194
|
return;
|
|
1175
1195
|
}
|
|
1176
1196
|
|
|
@@ -1206,7 +1226,32 @@ class Widget {
|
|
|
1206
1226
|
this.elements.errorSlot = this.shadow.querySelector('.oa-error-slot');
|
|
1207
1227
|
|
|
1208
1228
|
this.container.appendChild(host);
|
|
1209
|
-
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
isInlineLayout() {
|
|
1232
|
+
return this.params.layout === 'inline';
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
renderInlineShell() {
|
|
1236
|
+
const inlineShell = document.createElement('div');
|
|
1237
|
+
inlineShell.className = 'oa-inline-shell';
|
|
1238
|
+
inlineShell.innerHTML = this.buildPopupContent({ closeable: false });
|
|
1239
|
+
this.shadow.appendChild(inlineShell);
|
|
1240
|
+
|
|
1241
|
+
this.popup = {
|
|
1242
|
+
host: this.host,
|
|
1243
|
+
root: inlineShell,
|
|
1244
|
+
inline: true,
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
this.bindPopupEvents(inlineShell);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
resetInlineShell() {
|
|
1251
|
+
if (!this.popup?.root) return;
|
|
1252
|
+
|
|
1253
|
+
this.popup.root.innerHTML = this.buildPopupContent({ closeable: false });
|
|
1254
|
+
this.bindPopupEvents(this.popup.root);
|
|
1210
1255
|
}
|
|
1211
1256
|
|
|
1212
1257
|
startChallenge() {
|
|
@@ -1239,6 +1284,14 @@ class Widget {
|
|
|
1239
1284
|
}
|
|
1240
1285
|
|
|
1241
1286
|
openPopup() {
|
|
1287
|
+
if (this.isInlineLayout()) {
|
|
1288
|
+
if (!this.popup) {
|
|
1289
|
+
this.renderInlineShell();
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return this.getVideo();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1242
1295
|
if (this.popup) return this.getVideo();
|
|
1243
1296
|
|
|
1244
1297
|
const anchor = this.getPopupAnchor();
|
|
@@ -1416,7 +1469,7 @@ class Widget {
|
|
|
1416
1469
|
this.popup.root.style.pointerEvents = 'auto';
|
|
1417
1470
|
}
|
|
1418
1471
|
|
|
1419
|
-
buildPopupContent() {
|
|
1472
|
+
buildPopupContent({ closeable = true } = {}) {
|
|
1420
1473
|
return `
|
|
1421
1474
|
<div class="oa-header">
|
|
1422
1475
|
<div class="oa-title">
|
|
@@ -1427,10 +1480,14 @@ class Widget {
|
|
|
1427
1480
|
</a>
|
|
1428
1481
|
<span class="oa-badge">on-device</span>
|
|
1429
1482
|
</div>
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1483
|
+
${
|
|
1484
|
+
closeable
|
|
1485
|
+
? `<button class="oa-close-btn"
|
|
1486
|
+
aria-label="Close">
|
|
1487
|
+
${CLOSE_SVG}
|
|
1488
|
+
</button>`
|
|
1489
|
+
: ''
|
|
1490
|
+
}
|
|
1434
1491
|
</div>
|
|
1435
1492
|
<div class="oa-body">
|
|
1436
1493
|
${heroTemplate('Initializing…')}
|
|
@@ -1443,7 +1500,7 @@ class Widget {
|
|
|
1443
1500
|
`;
|
|
1444
1501
|
}
|
|
1445
1502
|
|
|
1446
|
-
bindPopupEvents(root
|
|
1503
|
+
bindPopupEvents(root) {
|
|
1447
1504
|
const closeBtn = root.querySelector('.oa-close-btn');
|
|
1448
1505
|
if (closeBtn) {
|
|
1449
1506
|
closeBtn.addEventListener('click', () => {
|
|
@@ -1561,6 +1618,22 @@ class Widget {
|
|
|
1561
1618
|
}
|
|
1562
1619
|
|
|
1563
1620
|
showResult(outcome, message) {
|
|
1621
|
+
if (this.isInlineLayout()) {
|
|
1622
|
+
if (!this.popupElements?.body) return;
|
|
1623
|
+
|
|
1624
|
+
this.popupElements.body.innerHTML = resultTemplate(outcome, message);
|
|
1625
|
+
this.hideActions();
|
|
1626
|
+
|
|
1627
|
+
if (outcome === 'pass') {
|
|
1628
|
+
this.setState('verified');
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
this.setState(outcome === 'fail' ? 'failed' : 'retry');
|
|
1633
|
+
this.showActions('Try Again');
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1564
1637
|
if (outcome === 'pass') {
|
|
1565
1638
|
this.closePopup();
|
|
1566
1639
|
this.setState('verified');
|
|
@@ -1600,8 +1673,9 @@ class Widget {
|
|
|
1600
1673
|
if (!this.popupElements?.body) return;
|
|
1601
1674
|
|
|
1602
1675
|
this.popupElements.body.innerHTML = errorStepTemplate(message);
|
|
1603
|
-
this.popupElements.errorCountdown =
|
|
1604
|
-
|
|
1676
|
+
this.popupElements.errorCountdown = this.popupElements.body.querySelector(
|
|
1677
|
+
'.oa-error-step-countdown'
|
|
1678
|
+
);
|
|
1605
1679
|
this.hideActions();
|
|
1606
1680
|
}
|
|
1607
1681
|
|
|
@@ -1620,6 +1694,22 @@ class Widget {
|
|
|
1620
1694
|
|
|
1621
1695
|
closePopup() {
|
|
1622
1696
|
if (!this.popup) return;
|
|
1697
|
+
|
|
1698
|
+
if (this.popup.inline) {
|
|
1699
|
+
this.popup.cleanup?.();
|
|
1700
|
+
if (this.popupFrame) {
|
|
1701
|
+
cancelAnimationFrame(this.popupFrame);
|
|
1702
|
+
this.popupFrame = 0;
|
|
1703
|
+
}
|
|
1704
|
+
this.resetInlineShell();
|
|
1705
|
+
|
|
1706
|
+
if (this.state === 'loading') {
|
|
1707
|
+
this.setState('idle');
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1623
1713
|
this.popup.cleanup?.();
|
|
1624
1714
|
this.popup.themeCleanup?.();
|
|
1625
1715
|
this.popup.host.remove();
|
|
@@ -2819,9 +2909,18 @@ function resolveChallengeErrorMessage(error) {
|
|
|
2819
2909
|
return 'Verification failed. Please try again.';
|
|
2820
2910
|
}
|
|
2821
2911
|
|
|
2912
|
+
function isInlineLayout(widget) {
|
|
2913
|
+
return widget?.params?.layout === 'inline';
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2822
2916
|
async function showErrorStep(widget, error) {
|
|
2823
2917
|
const message = resolveChallengeErrorMessage(error);
|
|
2824
2918
|
|
|
2919
|
+
if (isInlineLayout(widget)) {
|
|
2920
|
+
widget.showResult?.('retry', message);
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2825
2924
|
if (!widget.popup) {
|
|
2826
2925
|
widget.openPopup?.();
|
|
2827
2926
|
}
|
|
@@ -2874,10 +2973,14 @@ async function runServerless(widget, emitter) {
|
|
|
2874
2973
|
await Promise.all([loadVision(), initAgeEstimator()]);
|
|
2875
2974
|
|
|
2876
2975
|
modelBuffer = await loadModel();
|
|
2877
|
-
widget.showReady();
|
|
2878
2976
|
|
|
2879
|
-
|
|
2880
|
-
|
|
2977
|
+
if (isInlineLayout(widget)) {
|
|
2978
|
+
await startCameraFlow(widget, modelBuffer);
|
|
2979
|
+
} else {
|
|
2980
|
+
widget.showReady();
|
|
2981
|
+
await waitForStart(widget);
|
|
2982
|
+
await startCameraFlow(widget, modelBuffer);
|
|
2983
|
+
}
|
|
2881
2984
|
|
|
2882
2985
|
const transport = createTransport('serverless', params);
|
|
2883
2986
|
|
|
@@ -2966,10 +3069,16 @@ async function runServer(widget, emitter) {
|
|
|
2966
3069
|
captureFrame: () => (captureFrame() ? 'true' : 'null'),
|
|
2967
3070
|
});
|
|
2968
3071
|
|
|
2969
|
-
|
|
2970
|
-
|
|
3072
|
+
let video;
|
|
3073
|
+
|
|
3074
|
+
if (isInlineLayout(widget)) {
|
|
3075
|
+
video = widget.showCamera();
|
|
3076
|
+
} else {
|
|
3077
|
+
widget.showReady();
|
|
3078
|
+
await waitForStart(widget);
|
|
3079
|
+
video = widget.showCamera();
|
|
3080
|
+
}
|
|
2971
3081
|
|
|
2972
|
-
const video = widget.showCamera();
|
|
2973
3082
|
widget.setVideoStatus('Requesting camera…');
|
|
2974
3083
|
await startCamera(video);
|
|
2975
3084
|
exposeMirrorVideo(video);
|
|
@@ -3272,8 +3381,19 @@ class EventEmitter {
|
|
|
3272
3381
|
const emitter = new EventEmitter();
|
|
3273
3382
|
const widgets = new Map();
|
|
3274
3383
|
|
|
3384
|
+
function normalizeLayout(layout) {
|
|
3385
|
+
if (layout === 'inline' || layout === 'embed' || layout === 'embedded') {
|
|
3386
|
+
return 'inline';
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
return 'widget';
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3275
3392
|
function normalizeParams(params) {
|
|
3276
3393
|
const globalConfig = typeof window !== 'undefined' ? window.openage || {} : {};
|
|
3394
|
+
const layout = normalizeLayout(
|
|
3395
|
+
params.layout ?? params.display ?? globalConfig.layout ?? globalConfig.display
|
|
3396
|
+
);
|
|
3277
3397
|
|
|
3278
3398
|
return {
|
|
3279
3399
|
mode: 'serverless',
|
|
@@ -3282,6 +3402,7 @@ function normalizeParams(params) {
|
|
|
3282
3402
|
minAge: 18,
|
|
3283
3403
|
...globalConfig,
|
|
3284
3404
|
...params,
|
|
3405
|
+
layout,
|
|
3285
3406
|
};
|
|
3286
3407
|
}
|
|
3287
3408
|
|
|
@@ -3290,6 +3411,10 @@ function startWidget(widget) {
|
|
|
3290
3411
|
emitter.emit('opened', widget.id);
|
|
3291
3412
|
runChallenge(widget, emitter);
|
|
3292
3413
|
};
|
|
3414
|
+
|
|
3415
|
+
if (widget.params.layout === 'inline') {
|
|
3416
|
+
widget.startChallenge();
|
|
3417
|
+
}
|
|
3293
3418
|
}
|
|
3294
3419
|
|
|
3295
3420
|
function render(container, params = {}) {
|
|
@@ -3438,6 +3563,7 @@ function autoRender() {
|
|
|
3438
3563
|
size: element.dataset.size,
|
|
3439
3564
|
action: element.dataset.action,
|
|
3440
3565
|
mode: element.dataset.mode,
|
|
3566
|
+
layout: element.dataset.layout || element.dataset.display,
|
|
3441
3567
|
server: element.dataset.server,
|
|
3442
3568
|
};
|
|
3443
3569
|
|