diff --git a/bun.lock b/bun.lock index cdf7d437..a220ab93 100644 --- a/bun.lock +++ b/bun.lock @@ -235,7 +235,7 @@ }, "packages/auth": { "name": "@wrnexus/auth", - "version": "0.8.23", + "version": "0.8.24", "dependencies": { "@wrnexus/authz": "workspace:*", "@wrnexus/captcha": "workspace:*", diff --git a/packages/auth/assets/client/auth.js b/packages/auth/assets/client/auth.js index 1ee18894..0643987e 100644 --- a/packages/auth/assets/client/auth.js +++ b/packages/auth/assets/client/auth.js @@ -424,11 +424,16 @@ form?.querySelectorAll("input, button").forEach((control) => { control.disabled = true; }); - setMessage( - status, - "Your sign-in request has expired. Sign in again to get a fresh verification request.", - true, - ); + // A form response's finalizer or hydration may re-enable buttons. Hide the + // entire stale form instead of leaving a second apparently usable action. + element.dataset.mfaExpired = "true"; + if (form) form.hidden = true; + setMessage(status, ""); + form?.querySelectorAll("[data-error]").forEach((error) => { + error.textContent = ""; + }); + const expiredMessage = element.querySelector("[data-auth-mfa-expired]"); + if (expiredMessage) expiredMessage.hidden = false; const restart = element.querySelector("[data-auth-mfa-restart]"); if (restart) restart.hidden = false; }; diff --git a/packages/auth/components/TwoFactorChallenge.wrn b/packages/auth/components/TwoFactorChallenge.wrn index 51529f4f..24626bf4 100644 --- a/packages/auth/components/TwoFactorChallenge.wrn +++ b/packages/auth/components/TwoFactorChallenge.wrn @@ -51,6 +51,7 @@ component TwoFactorChallenge { - `; + `; window.eval(readFileSync(new URL("../assets/client/auth.js", import.meta.url), "utf8")); (window as any).WRNexusAuth.mount(window.document); expect(window.document.querySelector("button")!.disabled).toBe(true); expect(window.document.querySelector("[data-auth-mfa-restart]")!.hidden).toBe( false, ); - expect(window.document.querySelector("[data-auth-mfa-status]")!.textContent).toContain( - "Sign in again", + expect(window.document.querySelector("[data-auth-mfa-expired]")!.hidden).toBe( + false, ); + expect(window.document.querySelector("form")!.hidden).toBe(true); + expect(window.document.querySelector("[data-auth-mfa-status]")!.textContent).toBe(""); expect(window.sessionStorage.getItem("wrnexus.auth.mfa")).toBeNull(); } finally { await window.happyDOM.close(); @@ -121,6 +123,12 @@ test("MFA radios preserve allowed methods and restore continuation without reviv detail: { code: "mfa-transaction-expired" }, }), ); + // Validation finalizers may restore button.disabled after dispatching error. + form.querySelector("button")!.disabled = false; + expect(form.hidden).toBe(true); + expect( + window.document.querySelector("[data-auth-mfa-challenge]")!.dataset.mfaExpired, + ).toBe("true"); expect(window.sessionStorage.getItem("wrnexus.auth.mfa")).toBeNull(); expect(window.document.querySelector("[data-auth-mfa-restart]")!.hidden).toBe( false,