@authhero/widget 0.29.1 → 0.30.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.
- package/dist/authhero-widget/authhero-widget.esm.js +1 -1
- package/dist/authhero-widget/index.esm.js +1 -1
- package/dist/authhero-widget/p-53f4e14a.entry.js +1 -0
- package/dist/authhero-widget/p-e91b632f.entry.js +1 -0
- package/dist/cjs/authhero-node.cjs.entry.js +39 -38
- package/dist/cjs/authhero-widget.cjs.entry.js +237 -21
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/collection/components/authhero-node/authhero-node.js +39 -38
- package/dist/collection/components/authhero-widget/authhero-widget.js +237 -21
- package/dist/components/authhero-node.js +1 -1
- package/dist/components/authhero-widget.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/p-DqexL9yF.js +1 -0
- package/dist/esm/authhero-node.entry.js +39 -38
- package/dist/esm/authhero-widget.entry.js +237 -21
- package/dist/esm/index.js +1 -1
- package/dist/types/components/authhero-node/authhero-node.d.ts +8 -0
- package/dist/types/components/authhero-widget/authhero-widget.d.ts +16 -0
- package/hydrate/index.js +276 -59
- package/hydrate/index.mjs +276 -59
- package/package.json +2 -2
- package/dist/authhero-widget/p-5428e2e1.entry.js +0 -1
- package/dist/authhero-widget/p-8514f73f.entry.js +0 -1
- package/dist/components/p-CncHQfQk.js +0 -1
|
@@ -633,6 +633,11 @@ const AuthheroWidget = class {
|
|
|
633
633
|
* Form data collected from inputs.
|
|
634
634
|
*/
|
|
635
635
|
formData = {};
|
|
636
|
+
/**
|
|
637
|
+
* AbortController for an in-flight conditional mediation request.
|
|
638
|
+
* Aborted on screen change or component disconnect.
|
|
639
|
+
*/
|
|
640
|
+
conditionalMediationAbort;
|
|
636
641
|
/**
|
|
637
642
|
* Emitted when the form is submitted.
|
|
638
643
|
* The consuming application should handle the submission unless autoSubmit is true.
|
|
@@ -667,6 +672,9 @@ const AuthheroWidget = class {
|
|
|
667
672
|
*/
|
|
668
673
|
screenChange;
|
|
669
674
|
watchScreen(newValue) {
|
|
675
|
+
// Abort any in-flight conditional mediation when screen changes
|
|
676
|
+
this.conditionalMediationAbort?.abort();
|
|
677
|
+
this.conditionalMediationAbort = undefined;
|
|
670
678
|
if (typeof newValue === "string") {
|
|
671
679
|
try {
|
|
672
680
|
this._screen = JSON.parse(newValue);
|
|
@@ -900,6 +908,8 @@ const AuthheroWidget = class {
|
|
|
900
908
|
}
|
|
901
909
|
disconnectedCallback() {
|
|
902
910
|
window.removeEventListener("popstate", this.handlePopState);
|
|
911
|
+
this.conditionalMediationAbort?.abort();
|
|
912
|
+
this.conditionalMediationAbort = undefined;
|
|
903
913
|
}
|
|
904
914
|
async componentWillLoad() {
|
|
905
915
|
// Parse initial props - this prevents unnecessary state changes during hydration that cause flashes
|
|
@@ -988,6 +998,10 @@ const AuthheroWidget = class {
|
|
|
988
998
|
this.updateDataScreenAttribute();
|
|
989
999
|
this.persistState();
|
|
990
1000
|
this.focusFirstInput();
|
|
1001
|
+
// Start WebAuthn ceremony if returned with the screen (e.g. conditional mediation)
|
|
1002
|
+
if (data.ceremony) {
|
|
1003
|
+
this.performWebAuthnCeremony(data.ceremony);
|
|
1004
|
+
}
|
|
991
1005
|
return true;
|
|
992
1006
|
}
|
|
993
1007
|
}
|
|
@@ -1179,9 +1193,19 @@ const AuthheroWidget = class {
|
|
|
1179
1193
|
console.error("Invalid WebAuthn ceremony payload", ceremony);
|
|
1180
1194
|
return;
|
|
1181
1195
|
}
|
|
1196
|
+
if (ceremony.type === "webauthn-authentication-conditional") {
|
|
1197
|
+
// Conditional mediation runs in the background, no requestAnimationFrame needed
|
|
1198
|
+
this.executeWebAuthnConditionalMediation(ceremony);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1182
1201
|
requestAnimationFrame(() => {
|
|
1183
1202
|
this.overrideFormSubmit();
|
|
1184
|
-
|
|
1203
|
+
if (ceremony.type === "webauthn-authentication") {
|
|
1204
|
+
this.executeWebAuthnAuthentication(ceremony);
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
this.executeWebAuthnRegistration(ceremony);
|
|
1208
|
+
}
|
|
1185
1209
|
});
|
|
1186
1210
|
}
|
|
1187
1211
|
/**
|
|
@@ -1192,8 +1216,6 @@ const AuthheroWidget = class {
|
|
|
1192
1216
|
if (typeof data !== "object" || data === null)
|
|
1193
1217
|
return false;
|
|
1194
1218
|
const obj = data;
|
|
1195
|
-
if (obj.type !== "webauthn-registration")
|
|
1196
|
-
return false;
|
|
1197
1219
|
if (typeof obj.successAction !== "string")
|
|
1198
1220
|
return false;
|
|
1199
1221
|
const opts = obj.options;
|
|
@@ -1202,23 +1224,30 @@ const AuthheroWidget = class {
|
|
|
1202
1224
|
const o = opts;
|
|
1203
1225
|
if (typeof o.challenge !== "string")
|
|
1204
1226
|
return false;
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
typeof rp.
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
typeof u.
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1227
|
+
if (obj.type === "webauthn-registration") {
|
|
1228
|
+
const rp = o.rp;
|
|
1229
|
+
if (typeof rp !== "object" || rp === null)
|
|
1230
|
+
return false;
|
|
1231
|
+
if (typeof rp.id !== "string" ||
|
|
1232
|
+
typeof rp.name !== "string")
|
|
1233
|
+
return false;
|
|
1234
|
+
const user = o.user;
|
|
1235
|
+
if (typeof user !== "object" || user === null)
|
|
1236
|
+
return false;
|
|
1237
|
+
const u = user;
|
|
1238
|
+
if (typeof u.id !== "string" ||
|
|
1239
|
+
typeof u.name !== "string" ||
|
|
1240
|
+
typeof u.displayName !== "string")
|
|
1241
|
+
return false;
|
|
1242
|
+
if (!Array.isArray(o.pubKeyCredParams))
|
|
1243
|
+
return false;
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
if (obj.type === "webauthn-authentication" ||
|
|
1247
|
+
obj.type === "webauthn-authentication-conditional") {
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
return false;
|
|
1222
1251
|
}
|
|
1223
1252
|
/**
|
|
1224
1253
|
* Perform the WebAuthn navigator.credentials.create() ceremony and submit
|
|
@@ -1331,6 +1360,193 @@ const AuthheroWidget = class {
|
|
|
1331
1360
|
}
|
|
1332
1361
|
}
|
|
1333
1362
|
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Perform the WebAuthn navigator.credentials.get() ceremony (explicit modal)
|
|
1365
|
+
* and submit the credential result via the form.
|
|
1366
|
+
*/
|
|
1367
|
+
async executeWebAuthnAuthentication(ceremony) {
|
|
1368
|
+
const opts = ceremony.options;
|
|
1369
|
+
const b64uToBuf = (s) => {
|
|
1370
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
1371
|
+
while (s.length % 4)
|
|
1372
|
+
s += "=";
|
|
1373
|
+
const b = atob(s);
|
|
1374
|
+
const a = new Uint8Array(b.length);
|
|
1375
|
+
for (let i = 0; i < b.length; i++)
|
|
1376
|
+
a[i] = b.charCodeAt(i);
|
|
1377
|
+
return a.buffer;
|
|
1378
|
+
};
|
|
1379
|
+
const bufToB64u = (b) => {
|
|
1380
|
+
const a = new Uint8Array(b);
|
|
1381
|
+
let s = "";
|
|
1382
|
+
for (let i = 0; i < a.length; i++)
|
|
1383
|
+
s += String.fromCharCode(a[i]);
|
|
1384
|
+
return btoa(s)
|
|
1385
|
+
.replace(/\+/g, "-")
|
|
1386
|
+
.replace(/\//g, "_")
|
|
1387
|
+
.replace(/=+$/, "");
|
|
1388
|
+
};
|
|
1389
|
+
const findForm = () => {
|
|
1390
|
+
const shadowRoot = this.el?.shadowRoot;
|
|
1391
|
+
if (shadowRoot) {
|
|
1392
|
+
const f = shadowRoot.querySelector("form");
|
|
1393
|
+
if (f)
|
|
1394
|
+
return f;
|
|
1395
|
+
}
|
|
1396
|
+
return document.querySelector("form");
|
|
1397
|
+
};
|
|
1398
|
+
try {
|
|
1399
|
+
const publicKey = {
|
|
1400
|
+
challenge: b64uToBuf(opts.challenge),
|
|
1401
|
+
rpId: opts.rpId,
|
|
1402
|
+
timeout: opts.timeout,
|
|
1403
|
+
userVerification: opts.userVerification || "preferred",
|
|
1404
|
+
};
|
|
1405
|
+
if (opts.allowCredentials?.length) {
|
|
1406
|
+
publicKey.allowCredentials = opts.allowCredentials.map((c) => ({
|
|
1407
|
+
id: b64uToBuf(c.id),
|
|
1408
|
+
type: c.type,
|
|
1409
|
+
transports: (c.transports || []),
|
|
1410
|
+
}));
|
|
1411
|
+
}
|
|
1412
|
+
const cred = (await navigator.credentials.get({
|
|
1413
|
+
publicKey,
|
|
1414
|
+
}));
|
|
1415
|
+
const response = cred.response;
|
|
1416
|
+
const resp = {
|
|
1417
|
+
id: cred.id,
|
|
1418
|
+
rawId: bufToB64u(cred.rawId),
|
|
1419
|
+
type: cred.type,
|
|
1420
|
+
response: {
|
|
1421
|
+
authenticatorData: bufToB64u(response.authenticatorData),
|
|
1422
|
+
clientDataJSON: bufToB64u(response.clientDataJSON),
|
|
1423
|
+
signature: bufToB64u(response.signature),
|
|
1424
|
+
},
|
|
1425
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
1426
|
+
authenticatorAttachment: cred.authenticatorAttachment || undefined,
|
|
1427
|
+
};
|
|
1428
|
+
if (response.userHandle) {
|
|
1429
|
+
resp.response.userHandle = bufToB64u(response.userHandle);
|
|
1430
|
+
}
|
|
1431
|
+
const form = findForm();
|
|
1432
|
+
if (form) {
|
|
1433
|
+
const cf = form.querySelector('[name="credential-field"]') ||
|
|
1434
|
+
form.querySelector("#credential-field");
|
|
1435
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
1436
|
+
form.querySelector("#action-field");
|
|
1437
|
+
if (cf)
|
|
1438
|
+
cf.value = JSON.stringify(resp);
|
|
1439
|
+
if (af)
|
|
1440
|
+
af.value = ceremony.successAction;
|
|
1441
|
+
form.submit();
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
catch (e) {
|
|
1445
|
+
console.error("WebAuthn authentication error:", e);
|
|
1446
|
+
const form = findForm();
|
|
1447
|
+
if (form) {
|
|
1448
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
1449
|
+
form.querySelector("#action-field");
|
|
1450
|
+
if (af)
|
|
1451
|
+
af.value = "error";
|
|
1452
|
+
form.submit();
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Execute WebAuthn conditional mediation (autofill-assisted passkeys).
|
|
1458
|
+
* Runs in the background — the browser shows passkey suggestions in the
|
|
1459
|
+
* username field's autofill dropdown. Silently ignored if unsupported.
|
|
1460
|
+
*/
|
|
1461
|
+
async executeWebAuthnConditionalMediation(ceremony) {
|
|
1462
|
+
// Feature detection
|
|
1463
|
+
if (!window.PublicKeyCredential ||
|
|
1464
|
+
!PublicKeyCredential.isConditionalMediationAvailable) {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const available = await PublicKeyCredential.isConditionalMediationAvailable();
|
|
1468
|
+
if (!available)
|
|
1469
|
+
return;
|
|
1470
|
+
// Abort any previous conditional mediation request
|
|
1471
|
+
this.conditionalMediationAbort?.abort();
|
|
1472
|
+
const abortController = new AbortController();
|
|
1473
|
+
this.conditionalMediationAbort = abortController;
|
|
1474
|
+
const opts = ceremony.options;
|
|
1475
|
+
const b64uToBuf = (s) => {
|
|
1476
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
1477
|
+
while (s.length % 4)
|
|
1478
|
+
s += "=";
|
|
1479
|
+
const b = atob(s);
|
|
1480
|
+
const a = new Uint8Array(b.length);
|
|
1481
|
+
for (let i = 0; i < b.length; i++)
|
|
1482
|
+
a[i] = b.charCodeAt(i);
|
|
1483
|
+
return a.buffer;
|
|
1484
|
+
};
|
|
1485
|
+
const bufToB64u = (b) => {
|
|
1486
|
+
const a = new Uint8Array(b);
|
|
1487
|
+
let s = "";
|
|
1488
|
+
for (let i = 0; i < a.length; i++)
|
|
1489
|
+
s += String.fromCharCode(a[i]);
|
|
1490
|
+
return btoa(s)
|
|
1491
|
+
.replace(/\+/g, "-")
|
|
1492
|
+
.replace(/\//g, "_")
|
|
1493
|
+
.replace(/=+$/, "");
|
|
1494
|
+
};
|
|
1495
|
+
try {
|
|
1496
|
+
const cred = (await navigator.credentials.get({
|
|
1497
|
+
mediation: "conditional",
|
|
1498
|
+
signal: abortController.signal,
|
|
1499
|
+
publicKey: {
|
|
1500
|
+
challenge: b64uToBuf(opts.challenge),
|
|
1501
|
+
rpId: opts.rpId,
|
|
1502
|
+
timeout: opts.timeout,
|
|
1503
|
+
userVerification: opts.userVerification ||
|
|
1504
|
+
"preferred",
|
|
1505
|
+
},
|
|
1506
|
+
}));
|
|
1507
|
+
const response = cred.response;
|
|
1508
|
+
const resp = {
|
|
1509
|
+
id: cred.id,
|
|
1510
|
+
rawId: bufToB64u(cred.rawId),
|
|
1511
|
+
type: cred.type,
|
|
1512
|
+
response: {
|
|
1513
|
+
authenticatorData: bufToB64u(response.authenticatorData),
|
|
1514
|
+
clientDataJSON: bufToB64u(response.clientDataJSON),
|
|
1515
|
+
signature: bufToB64u(response.signature),
|
|
1516
|
+
},
|
|
1517
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
1518
|
+
authenticatorAttachment: cred.authenticatorAttachment || undefined,
|
|
1519
|
+
};
|
|
1520
|
+
if (response.userHandle) {
|
|
1521
|
+
resp.response.userHandle = bufToB64u(response.userHandle);
|
|
1522
|
+
}
|
|
1523
|
+
// Submit via the widget's form handling
|
|
1524
|
+
this.formData["credential-field"] = JSON.stringify(resp);
|
|
1525
|
+
this.formData["action-field"] = ceremony.successAction;
|
|
1526
|
+
// Ensure form submit override is set up, then submit
|
|
1527
|
+
this.overrideFormSubmit();
|
|
1528
|
+
const shadowRoot = this.el?.shadowRoot;
|
|
1529
|
+
const form = shadowRoot?.querySelector("form");
|
|
1530
|
+
if (form) {
|
|
1531
|
+
// Set the hidden input values directly
|
|
1532
|
+
const cf = form.querySelector('[name="credential-field"]') ||
|
|
1533
|
+
form.querySelector("#credential-field");
|
|
1534
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
1535
|
+
form.querySelector("#action-field");
|
|
1536
|
+
if (cf)
|
|
1537
|
+
cf.value = JSON.stringify(resp);
|
|
1538
|
+
if (af)
|
|
1539
|
+
af.value = ceremony.successAction;
|
|
1540
|
+
form.submit();
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
catch (e) {
|
|
1544
|
+
// Silently ignore AbortError and NotAllowedError
|
|
1545
|
+
if (e?.name === "AbortError" || e?.name === "NotAllowedError")
|
|
1546
|
+
return;
|
|
1547
|
+
console.error("Conditional mediation error:", e);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1334
1550
|
handleButtonClick = (detail) => {
|
|
1335
1551
|
// If this is a submit button click, trigger form submission
|
|
1336
1552
|
if (detail.type === "submit") {
|
|
@@ -1572,7 +1788,7 @@ const AuthheroWidget = class {
|
|
|
1572
1788
|
};
|
|
1573
1789
|
// Get logo URL from theme.widget (takes precedence) or branding
|
|
1574
1790
|
const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
|
|
1575
|
-
return (index.h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, index.h("header", { class: "widget-header", part: "header" }, logoUrl && (index.h("div", { class: "logo-wrapper", part: "logo-wrapper" }, index.h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (index.h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (index.h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), index.h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (index.h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (index.h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), index.h("form", { onSubmit: this.handleSubmit, part: "form" }, hiddenComponents.map((c) => (index.h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), index.h("div", { class: "form-content" }, socialComponents.length > 0 && (index.h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (index.h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
|
|
1791
|
+
return (index.h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, index.h("header", { class: "widget-header", part: "header" }, logoUrl && (index.h("div", { class: "logo-wrapper", part: "logo-wrapper" }, index.h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (index.h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (index.h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), index.h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (index.h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (index.h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), index.h("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (index.h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), index.h("div", { class: "form-content" }, socialComponents.length > 0 && (index.h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (index.h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
|
|
1576
1792
|
fieldComponents.length > 0 &&
|
|
1577
1793
|
hasDivider && (index.h("div", { class: "divider", part: "divider" }, index.h("span", { class: "divider-text" }, dividerText))), index.h("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (index.h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading })))))), screen.links && screen.links.length > 0 && (index.h("div", { class: "links", part: "links" }, screen.links.map((link) => (index.h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (index.h("span", null, link.text, " ", index.h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
|
|
1578
1794
|
id: link.id,
|