@authhero/widget 0.28.1 → 0.29.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.
@@ -1019,7 +1019,18 @@ const AuthheroWidget = class {
1019
1019
  e.preventDefault();
1020
1020
  if (!this._screen || this.loading)
1021
1021
  return;
1022
- const submitData = overrideData || this.formData;
1022
+ let submitData = { ...this.formData, ...(overrideData || {}) };
1023
+ // Merge hidden input values from DOM (may have been set programmatically
1024
+ // by inline scripts, e.g. passkey management buttons)
1025
+ const form = this.el.shadowRoot?.querySelector("form");
1026
+ if (form) {
1027
+ const hiddenInputs = form.querySelectorAll('input[type="hidden"]');
1028
+ hiddenInputs.forEach((input) => {
1029
+ if (input.name && input.value) {
1030
+ submitData[input.name] = input.value;
1031
+ }
1032
+ });
1033
+ }
1023
1034
  // Always emit the submit event
1024
1035
  this.formSubmit.emit({
1025
1036
  screen: this._screen,
@@ -1095,6 +1106,10 @@ const AuthheroWidget = class {
1095
1106
  this.state = result.state;
1096
1107
  this.persistState();
1097
1108
  }
1109
+ // Perform WebAuthn ceremony if present (structured data, not script)
1110
+ if (result.ceremony) {
1111
+ this.performWebAuthnCeremony(result.ceremony);
1112
+ }
1098
1113
  // Focus first input on new screen
1099
1114
  this.focusFirstInput();
1100
1115
  }
@@ -1128,6 +1143,192 @@ const AuthheroWidget = class {
1128
1143
  this.loading = false;
1129
1144
  }
1130
1145
  };
1146
+ /**
1147
+ * Override form.submit() so that scripts (e.g. WebAuthn ceremony) that call
1148
+ * form.submit() go through the widget's JSON fetch pipeline instead of a
1149
+ * native form-urlencoded POST.
1150
+ */
1151
+ overrideFormSubmit() {
1152
+ const shadowRoot = this.el.shadowRoot;
1153
+ if (!shadowRoot)
1154
+ return;
1155
+ const form = shadowRoot.querySelector("form");
1156
+ if (!form)
1157
+ return;
1158
+ form.submit = () => {
1159
+ const formData = new FormData(form);
1160
+ const data = {};
1161
+ formData.forEach((value, key) => {
1162
+ if (typeof value === "string") {
1163
+ data[key] = value;
1164
+ }
1165
+ });
1166
+ const syntheticEvent = { preventDefault: () => { } };
1167
+ this.handleSubmit(syntheticEvent, data);
1168
+ };
1169
+ }
1170
+ /**
1171
+ * Validate and execute a structured WebAuthn ceremony returned by the server.
1172
+ * Instead of injecting arbitrary script content, this parses the ceremony JSON,
1173
+ * validates the expected fields, and calls the WebAuthn API natively.
1174
+ */
1175
+ performWebAuthnCeremony(ceremony) {
1176
+ if (!this.isValidWebAuthnCeremony(ceremony)) {
1177
+ console.error("Invalid WebAuthn ceremony payload", ceremony);
1178
+ return;
1179
+ }
1180
+ requestAnimationFrame(() => {
1181
+ this.overrideFormSubmit();
1182
+ this.executeWebAuthnRegistration(ceremony);
1183
+ });
1184
+ }
1185
+ /**
1186
+ * Schema validation for WebAuthn ceremony payloads.
1187
+ * Checks required fields and types before invoking browser APIs.
1188
+ */
1189
+ isValidWebAuthnCeremony(data) {
1190
+ if (typeof data !== "object" || data === null)
1191
+ return false;
1192
+ const obj = data;
1193
+ if (obj.type !== "webauthn-registration")
1194
+ return false;
1195
+ if (typeof obj.successAction !== "string")
1196
+ return false;
1197
+ const opts = obj.options;
1198
+ if (typeof opts !== "object" || opts === null)
1199
+ return false;
1200
+ const o = opts;
1201
+ if (typeof o.challenge !== "string")
1202
+ return false;
1203
+ const rp = o.rp;
1204
+ if (typeof rp !== "object" || rp === null)
1205
+ return false;
1206
+ if (typeof rp.id !== "string" ||
1207
+ typeof rp.name !== "string")
1208
+ return false;
1209
+ const user = o.user;
1210
+ if (typeof user !== "object" || user === null)
1211
+ return false;
1212
+ const u = user;
1213
+ if (typeof u.id !== "string" ||
1214
+ typeof u.name !== "string" ||
1215
+ typeof u.displayName !== "string")
1216
+ return false;
1217
+ if (!Array.isArray(o.pubKeyCredParams))
1218
+ return false;
1219
+ return true;
1220
+ }
1221
+ /**
1222
+ * Perform the WebAuthn navigator.credentials.create() ceremony and submit
1223
+ * the credential result via the form.
1224
+ */
1225
+ async executeWebAuthnRegistration(ceremony) {
1226
+ const opts = ceremony.options;
1227
+ const b64uToBuf = (s) => {
1228
+ s = s.replace(/-/g, "+").replace(/_/g, "/");
1229
+ while (s.length % 4)
1230
+ s += "=";
1231
+ const b = atob(s);
1232
+ const a = new Uint8Array(b.length);
1233
+ for (let i = 0; i < b.length; i++)
1234
+ a[i] = b.charCodeAt(i);
1235
+ return a.buffer;
1236
+ };
1237
+ const bufToB64u = (b) => {
1238
+ const a = new Uint8Array(b);
1239
+ let s = "";
1240
+ for (let i = 0; i < a.length; i++)
1241
+ s += String.fromCharCode(a[i]);
1242
+ return btoa(s)
1243
+ .replace(/\+/g, "-")
1244
+ .replace(/\//g, "_")
1245
+ .replace(/=+$/, "");
1246
+ };
1247
+ const findForm = () => {
1248
+ const shadowRoot = this.el?.shadowRoot;
1249
+ if (shadowRoot) {
1250
+ const f = shadowRoot.querySelector("form");
1251
+ if (f)
1252
+ return f;
1253
+ }
1254
+ return document.querySelector("form");
1255
+ };
1256
+ try {
1257
+ const publicKey = {
1258
+ challenge: b64uToBuf(opts.challenge),
1259
+ rp: { id: opts.rp.id, name: opts.rp.name },
1260
+ user: {
1261
+ id: b64uToBuf(opts.user.id),
1262
+ name: opts.user.name,
1263
+ displayName: opts.user.displayName,
1264
+ },
1265
+ pubKeyCredParams: opts.pubKeyCredParams.map((p) => ({
1266
+ alg: p.alg,
1267
+ type: p.type,
1268
+ })),
1269
+ timeout: opts.timeout,
1270
+ attestation: (opts.attestation || "none"),
1271
+ authenticatorSelection: opts.authenticatorSelection
1272
+ ? {
1273
+ residentKey: (opts.authenticatorSelection.residentKey ||
1274
+ "preferred"),
1275
+ userVerification: (opts.authenticatorSelection
1276
+ .userVerification ||
1277
+ "preferred"),
1278
+ }
1279
+ : undefined,
1280
+ };
1281
+ if (opts.excludeCredentials?.length) {
1282
+ publicKey.excludeCredentials = opts.excludeCredentials.map((c) => ({
1283
+ id: b64uToBuf(c.id),
1284
+ type: c.type,
1285
+ transports: (c.transports || []),
1286
+ }));
1287
+ }
1288
+ const cred = (await navigator.credentials.create({
1289
+ publicKey,
1290
+ }));
1291
+ const response = cred.response;
1292
+ const resp = {
1293
+ id: cred.id,
1294
+ rawId: bufToB64u(cred.rawId),
1295
+ type: cred.type,
1296
+ response: {
1297
+ attestationObject: bufToB64u(response.attestationObject),
1298
+ clientDataJSON: bufToB64u(response.clientDataJSON),
1299
+ },
1300
+ clientExtensionResults: cred.getClientExtensionResults(),
1301
+ authenticatorAttachment: cred.authenticatorAttachment || undefined,
1302
+ };
1303
+ if (typeof response.getTransports === "function") {
1304
+ resp.response.transports =
1305
+ response.getTransports();
1306
+ }
1307
+ const form = findForm();
1308
+ if (form) {
1309
+ const cf = form.querySelector('[name="credential-field"]') ||
1310
+ form.querySelector("#credential-field");
1311
+ const af = form.querySelector('[name="action-field"]') ||
1312
+ form.querySelector("#action-field");
1313
+ if (cf)
1314
+ cf.value = JSON.stringify(resp);
1315
+ if (af)
1316
+ af.value = ceremony.successAction;
1317
+ form.submit();
1318
+ }
1319
+ }
1320
+ catch (e) {
1321
+ console.error("WebAuthn registration error:", e);
1322
+ const form = findForm();
1323
+ if (form) {
1324
+ const af = form.querySelector('[name="action-field"]') ||
1325
+ form.querySelector("#action-field");
1326
+ if (af)
1327
+ af.value = "error";
1328
+ form.submit();
1329
+ }
1330
+ }
1331
+ }
1131
1332
  handleButtonClick = (detail) => {
1132
1333
  // If this is a submit button click, trigger form submission
1133
1334
  if (detail.type === "submit") {
@@ -1330,9 +1531,11 @@ const AuthheroWidget = class {
1330
1531
  // Use the local screen variable for all rendering
1331
1532
  const screenErrors = screen.messages?.filter((m) => m.type === "error") || [];
1332
1533
  const screenSuccesses = screen.messages?.filter((m) => m.type === "success") || [];
1333
- const components = [...(screen.components ?? [])]
1534
+ const allComponents = [...(screen.components ?? [])];
1535
+ const components = allComponents
1334
1536
  .filter((c) => c.visible !== false)
1335
1537
  .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1538
+ const hiddenComponents = allComponents.filter((c) => c.visible === false);
1336
1539
  // Separate social, divider, and field components for layout ordering
1337
1540
  const socialComponents = components.filter((c) => this.isSocialComponent(c));
1338
1541
  const fieldComponents = components.filter((c) => !this.isSocialComponent(c) && !this.isDividerComponent(c));
@@ -1367,7 +1570,7 @@ const AuthheroWidget = class {
1367
1570
  };
1368
1571
  // Get logo URL from theme.widget (takes precedence) or branding
1369
1572
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
1370
- return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, part: "form" }, h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (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 &&
1573
+ return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, part: "form" }, hiddenComponents.map((c) => (h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (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 &&
1371
1574
  fieldComponents.length > 0 &&
1372
1575
  hasDivider && (h("div", { class: "divider", part: "divider" }, h("span", { class: "divider-text" }, dividerText))), h("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (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 && (h("div", { class: "links", part: "links" }, screen.links.map((link) => (h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (h("span", null, link.text, " ", h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
1373
1576
  id: link.id,