@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.
@@ -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
- this.executeWebAuthnRegistration(ceremony);
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
- const rp = o.rp;
1206
- if (typeof rp !== "object" || rp === null)
1207
- return false;
1208
- if (typeof rp.id !== "string" ||
1209
- typeof rp.name !== "string")
1210
- return false;
1211
- const user = o.user;
1212
- if (typeof user !== "object" || user === null)
1213
- return false;
1214
- const u = user;
1215
- if (typeof u.id !== "string" ||
1216
- typeof u.name !== "string" ||
1217
- typeof u.displayName !== "string")
1218
- return false;
1219
- if (!Array.isArray(o.pubKeyCredParams))
1220
- return false;
1221
- return true;
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,