fix(rpc): let the PASSWORD_REQUIRED sentinel through the error sanitizer
No node in the fleet could mint a Trusted federation invite or promote a
peer to Trusted from the dashboard. The cause is one case comparison.
Granting Trusted requires the node password; when none is supplied the
handler bails with "PASSWORD_REQUIRED: node password required to grant
Trusted" — a sentinel the frontend machine-reads (isPasswordRequired
checks includes('PASSWORD_REQUIRED')) to know it should open the password
prompt. sanitize_error_message allowlists "Password", but starts_with is
case-sensitive, so the ALL-CAPS sentinel failed the allowlist and
collapsed into "Operation failed. Check server logs for details." The
frontend never saw the sentinel, the prompt never opened, and the flow
died with a generic error on every node.
Reproduced via direct RPC: federation.invite {trust_level:"trusted"}
without a password returned the generic message; the same call WITH the
password minted a code fine — the machinery was healthy, only the signal
was being eaten.
"Tor address not available. Tor may not be running." — the invite
handler's other user-actionable precondition — was masked the same way
and is allowlisted too.
Regression tests pin both passing through verbatim, and
internal_errors_stay_generic still passes, so the generic masking of real
internals is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7ec4bdb22d
commit
e5a8fce198
@@ -58,6 +58,18 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
|
|||||||
"must be",
|
"must be",
|
||||||
"cannot",
|
"cannot",
|
||||||
"Password",
|
"Password",
|
||||||
|
// The federation escalation sentinel. "Password" above does NOT cover
|
||||||
|
// it — starts_with is case-sensitive and the sentinel is ALL-CAPS —
|
||||||
|
// so the frontend's isPasswordRequired() never saw it and the
|
||||||
|
// password prompt could never open. Net effect: no node could mint a
|
||||||
|
// Trusted invite or promote a peer from the trust dropdown, fleet-wide
|
||||||
|
// (2026-08-09). The sentinel is machine-read by the UI; it must pass
|
||||||
|
// through verbatim.
|
||||||
|
"PASSWORD_REQUIRED",
|
||||||
|
// "Tor address not available. Tor may not be running." — the invite
|
||||||
|
// handler's precondition, entirely user-actionable, was likewise
|
||||||
|
// collapsing into the generic message.
|
||||||
|
"Tor address not available",
|
||||||
"Session",
|
"Session",
|
||||||
"Failed to pull",
|
"Failed to pull",
|
||||||
"Failed to start",
|
"Failed to start",
|
||||||
@@ -171,6 +183,24 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
|
|||||||
mod sanitize_tests {
|
mod sanitize_tests {
|
||||||
use super::sanitize_error_message;
|
use super::sanitize_error_message;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn password_required_sentinel_passes_through_verbatim() {
|
||||||
|
// The UI machine-reads this sentinel (isPasswordRequired checks
|
||||||
|
// `includes('PASSWORD_REQUIRED')`) to know it should open the password
|
||||||
|
// prompt. The "Password" prefix does not cover it — starts_with is
|
||||||
|
// case-sensitive — and masking it made Trusted invites and trust
|
||||||
|
// promotion impossible on EVERY node (2026-08-09): the prompt simply
|
||||||
|
// never opened.
|
||||||
|
let msg = "PASSWORD_REQUIRED: node password required to grant Trusted";
|
||||||
|
assert_eq!(sanitize_error_message(msg), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tor_unavailable_precondition_passes_through() {
|
||||||
|
let msg = "Tor address not available. Tor may not be running.";
|
||||||
|
assert_eq!(sanitize_error_message(msg), msg);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn seed_reveal_errors_pass_through() {
|
fn seed_reveal_errors_pass_through() {
|
||||||
// Every user-actionable seed.reveal failure must reach the user —
|
// Every user-actionable seed.reveal failure must reach the user —
|
||||||
|
|||||||
Reference in New Issue
Block a user