fix(wasm-www): mask password prompts with CSS, never input.type=password

Reported live: iOS Safari kept showing its AutoFill quick-suggestion bar
(passwords/cards/contacts icons) above the keyboard even during ordinary
command typing, well after any password prompt had passed. Root cause:
the command input toggled between type="text" and type="password" on
the SAME persistent <input> element (driven by the telnet SGA/ECHO
negotiation -- see onEcho()). WebKit's AutoFill heuristic sticks to an
element as a credential field for the rest of the page's life once it's
ever been type="password", regardless of the type reverting afterward.

This mud never wants the browser's credential manager involved at all
(autocomplete=off already says so) -- the masking need is purely visual.
Replaced the type toggle with a `.masked` CSS class applying
-webkit-text-security:disc (WebKit/Blink; not Firefox, but that only
affects whether typed characters are legible, never functionality, and
iOS/Chrome cover the overwhelming majority of this page's mobile
traffic). input.type now never changes from "text"; updated the one
other reader of it (the Enter-key handler's local-echo suppression) to
check the session's serverEchoes flag directly instead.

Verified against a real packed site: type stays "text" through the
.masked toggle in both directions, the -webkit-text-security:disc
computed style applies/clears correctly, and a masked field visibly
renders bullet dots for typed text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyQCUoTo1Z93Py9aVFHQi1
This commit is contained in:
Yucong Sun 2026-07-29 01:10:10 -07:00
parent 5cc89de8ac
commit eaa1a6ee13

View file

@ -185,6 +185,19 @@
flex: 1; background: transparent; border: 0; outline: 0; color: var(--fg);
font: inherit; padding: 10px 16px 10px 8px;
}
/* Password-style masking WITHOUT ever setting type="password": once an
<input> has been type="password" even briefly, iOS/Safari's AutoFill
heuristic sticks to it as a credential field for the rest of the
page's life (the passwords/cards/contacts QuickType bar keeps showing
above the keyboard even after switching back to type="text" --
reported live). This mud never wants the browser's credential
manager involved at all (autocomplete=off already says so) -- the
masking is purely visual, so do it with CSS instead and never touch
`type`. -webkit-text-security is WebKit/Blink-only (not in Firefox);
acceptable here since it only affects whether typed characters are
LEGIBLE, never functionality, and iOS/Chrome cover the overwhelming
majority of mobile traffic this page sees. */
#cmd.masked { -webkit-text-security: disc; }
.modal-overlay {
position: fixed; inset: 0; background: rgba(4, 6, 10, 0.72);
display: flex; align-items: center; justify-content: center;
@ -833,7 +846,8 @@ function makeSession() {
s.telnet.onText = (t) => s.term.write(t);
s.telnet.onEcho = (on) => {
s.serverEchoes = on;
if (view === s && !s.charMode) input.type = on ? 'password' : 'text';
// CSS-only masking, never input.type -- see the #cmd.masked rule.
if (view === s && !s.charMode) input.classList.toggle('masked', on);
};
s.telnet.onCharMode = (on) => {
if (on === s.charMode) return;
@ -898,7 +912,7 @@ function applyInputState(s) {
status.textContent = s.disconnected ? 'disconnected'
: s.connId >= 0 ? 'connected (conn ' + s.connId + ')'
: 'connecting…';
input.type = s.serverEchoes ? 'password' : 'text';
input.classList.toggle('masked', s.serverEchoes);
input.disabled = s.disconnected || s.connId < 0;
refit(s);
if (!input.disabled && view === s) input.focus();
@ -1007,7 +1021,7 @@ input.addEventListener('keydown', (ev) => {
const s = activeGame;
const line = input.value;
input.value = '';
if (input.type !== 'password') s.term.write(line + '\r\n');
if (!s.serverEchoes) s.term.write(line + '\r\n');
s.sendData(Array.from(utf8enc.encode(line + '\r\n')));
});