sirius-press/plugins/sirius-press-auth/assets/login.js
Silent Mode 2287138149 fix(sirius-press): wallet sign-in did not work in an actual browser
Two bugs, both invisible to every test written so far, because those tests
post to wp-login.php over HTTP and a browser does not.

**The submit was blocked.** WordPress marks its username and password inputs
`required`. The wallet path deliberately leaves both empty — the signature is
the credential — so `form.requestSubmit()` ran constraint validation, refused,
and pointed a "Please fill out this field" bubble at an input the visitor is
not supposed to touch, with a valid signature already sitting in the form.
Nothing happened and nothing explained why. The wallet submit now turns
validation off for that submission only; password sign-in keeps it.

**`hidden` did not hide.** The attribute works through a UA rule that any
author rule with a `display` outranks, and WordPress ships exactly such a
rule — `.wp-core-ui .button { display: inline-block }`. So the two buttons
this plugin ships hidden were on screen regardless. That inverted the whole
progressive-enhancement story: "Use the browser wallet" was offered on every
browser including those without one, and a visitor with JavaScript disabled
would have been shown a sign-in button that could never do anything, instead
of the paste-a-signature box that works without scripts.

Found by opening the login page in a browser and clicking the button, which
is the one thing 178 passing checks had not done.
2026-09-22 20:06:27 +02:00

211 lines
7.7 KiB
JavaScript

// Wiring for the sign-in and registration screens.
//
// This file contains no cryptography — wallet.js does all of that. What it
// does is decide which of the three ways to sign is on offer and get out of
// the way of the other two:
//
// 1. A wallet the browser already has (Theseus exposes one). Best case: the
// key never comes near this page.
// 2. A recovery phrase typed here, used once, and wiped.
// 3. A signature produced somewhere else entirely and pasted in. This one
// works with JavaScript switched off, which is why the markup ships with
// the paste box present and the buttons hidden until this script decides
// they are usable.
//
// The progressive-enhancement direction matters: buttons start hidden and are
// revealed, rather than starting visible and being disabled. A script that
// fails to load leaves a page that still works.
(() => {
"use strict";
const CONFIG = window.SIRIUS_PRESS || {};
const strings = CONFIG.strings || {};
function setStatus(block, text, kind = "") {
const el = block.querySelector(".sirius-wallet__status");
if (!el) return;
el.textContent = text || "";
el.className = "sirius-wallet__status" + (kind ? " is-" + kind : "");
}
/** Submit the form the block sits in, once a signature is in place. */
function submitForm(block) {
const form = block.closest("form");
if (!form) return;
// WordPress marks its username and password inputs `required`, and the
// wallet path deliberately leaves both empty — the signature is the
// credential. Constraint validation would otherwise refuse the submit and
// point a "Please fill out this field" bubble at an input the visitor is
// not supposed to touch, with the signature already sitting in the form.
//
// Only reached from the wallet buttons, so password sign-in keeps its
// validation.
form.noValidate = true;
if (typeof form.requestSubmit === "function") {
form.requestSubmit();
} else {
form.submit();
}
}
function setSignature(block, signature, address) {
const field = block.querySelector(".sirius-wallet__signature");
if (field) field.value = signature;
// The address the signature claims. The server compares it against the
// one it recovers, which is the only way a signature over the wrong text
// becomes an error instead of a different identity.
const claim = block.querySelector(".sirius-wallet__address");
if (claim && address) claim.value = address;
}
/** On the paste path the visitor supplies the address themselves. */
function adoptTypedAddress(block) {
const typed = block.querySelector(".sirius-wallet__address-input");
const claim = block.querySelector(".sirius-wallet__address");
if (typed && claim && typed.value.trim() && !claim.value) {
claim.value = typed.value.trim();
}
}
async function signWithPhrase(block) {
const input = block.querySelector(".sirius-wallet__phrase-input");
const message = block.querySelector(".sirius-wallet__message").value;
const phrase = input ? input.value : "";
const check = window.SiriusWallet.validatePhrase(phrase);
if (!check.ok) {
setStatus(block, check.error, "error");
return;
}
setStatus(block, strings.signing || "Signing…");
let wallet;
try {
wallet = await window.SiriusWallet.fromPhrase(phrase, {
prefix: CONFIG.prefix,
path: CONFIG.path,
});
setSignature(block, await wallet.sign(message), wallet.address);
} catch (err) {
setStatus(block, err.message || String(err), "error");
return;
} finally {
// The phrase has done its one job. Clear it from the field and from the
// wallet object before anything else on the page runs.
if (input) input.value = "";
if (wallet && wallet.forget) wallet.forget();
}
submitForm(block);
}
async function signWithExternal(block) {
const external = window.SiriusWallet.external();
if (!external) {
setStatus(block, strings.noWallet || "No wallet found in this browser.", "error");
return;
}
const message = block.querySelector(".sirius-wallet__message").value;
setStatus(block, strings.signing || "Signing…");
try {
setSignature(block, await external.sign(message), await external.address());
} catch (err) {
// A user declining the wallet's approval dialog is a normal outcome, not
// an error worth shouting about.
const text = err && err.message ? err.message : String(err);
setStatus(block, /reject/i.test(text) ? "" : text, /reject/i.test(text) ? "" : "error");
return;
}
submitForm(block);
}
function enhance(block) {
// Whatever happens next, a hand-typed address must reach the server.
const form = block.closest("form");
if (form) form.addEventListener("submit", () => adoptTypedAddress(block));
const signButton = block.querySelector(".sirius-wallet__sign");
const externalButton = block.querySelector(".sirius-wallet__external");
const phraseArea = block.querySelector(".sirius-wallet__phrase");
if (!window.SiriusWallet || !window.crypto || !window.crypto.subtle) {
// No usable crypto — for instance an http:// origin, where WebCrypto is
// unavailable. Say so, and leave the paste path alone: it still works.
setStatus(
block,
"This browser cannot sign here (a secure https connection is required). Paste a signature from your own wallet instead.",
"error",
);
const manual = block.querySelector(".sirius-wallet__manual");
if (manual) manual.open = true;
return;
}
if (phraseArea) phraseArea.hidden = false;
if (signButton) {
signButton.hidden = false;
signButton.addEventListener("click", () => signWithPhrase(block));
}
if (externalButton && window.SiriusWallet.external()) {
externalButton.hidden = false;
externalButton.addEventListener("click", () => signWithExternal(block));
}
// Enter in the phrase box should sign, not submit an unsigned form.
const input = block.querySelector(".sirius-wallet__phrase-input");
if (input) {
input.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
signWithPhrase(block);
}
});
}
}
/** The "which address is this phrase?" helper on the recovery page. */
function enhanceDerive() {
const button = document.getElementById("sirius_derive_go");
const input = document.getElementById("sirius_derive_phrase");
const out = document.querySelector(".sirius-derive__out");
if (!button || !input || !out) return;
button.addEventListener("click", async () => {
const check = window.SiriusWallet.validatePhrase(input.value);
if (!check.ok) {
out.textContent = check.error;
out.className = "sirius-derive__out is-error";
return;
}
out.textContent = strings.signing || "Working…";
out.className = "sirius-derive__out";
try {
const wallet = await window.SiriusWallet.fromPhrase(input.value, {
prefix: CONFIG.prefix,
path: CONFIG.path,
});
out.textContent = wallet.address;
out.className = "sirius-derive__out is-address";
wallet.forget();
} catch (err) {
out.textContent = err.message || String(err);
out.className = "sirius-derive__out is-error";
} finally {
input.value = "";
}
});
}
function start() {
document.querySelectorAll(".sirius-wallet").forEach(enhance);
enhanceDerive();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start);
} else {
start();
}
})();