@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.
- package/dist/authhero-widget/authhero-widget.esm.js +1 -1
- package/dist/authhero-widget/index.esm.js +1 -1
- package/dist/authhero-widget/p-5428e2e1.entry.js +1 -0
- package/dist/cjs/authhero-widget.cjs.entry.js +206 -3
- package/dist/cjs/index.cjs.js +24 -6
- package/dist/collection/components/authhero-node/authhero-node.js +1 -1
- package/dist/collection/components/authhero-widget/authhero-widget.js +208 -5
- package/dist/components/authhero-widget.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/esm/authhero-widget.entry.js +206 -3
- package/dist/esm/index.js +24 -6
- package/dist/types/components/authhero-widget/authhero-widget.d.ts +22 -0
- package/hydrate/index.js +206 -3
- package/hydrate/index.mjs +206 -3
- package/package.json +2 -2
- package/dist/authhero-widget/p-b9ae0275.entry.js +0 -1
|
@@ -1019,7 +1019,18 @@ const AuthheroWidget = class {
|
|
|
1019
1019
|
e.preventDefault();
|
|
1020
1020
|
if (!this._screen || this.loading)
|
|
1021
1021
|
return;
|
|
1022
|
-
|
|
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
|
|
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,
|