From a48002df6102233eea52ff521e755411773b75b2 Mon Sep 17 00:00:00 2001 From: Zac Yang Date: Wed, 26 Aug 2026 03:17:12 -0400 Subject: [PATCH 1/2] Make the hidden attribute effective in the console .login-shell sets display:grid, which outranks the user-agent [hidden] rule, so the login overlay stayed on screen after a successful login. Co-authored-by: Cursor --- control-server/public/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/control-server/public/styles.css b/control-server/public/styles.css index 178840d..2b1bc92 100644 --- a/control-server/public/styles.css +++ b/control-server/public/styles.css @@ -1,5 +1,6 @@ :root { --ink:#171811; --paper:#e9e3d2; --acid:#d8ff3e; --rust:#bb3e24; --muted:#817d70; --line:#3a3b31; } * { box-sizing:border-box; } +[hidden] { display:none !important; } body { margin:0; min-height:100vh; background:var(--ink); color:var(--paper); font-family:"IBM Plex Mono","Courier New",monospace; } .grain { position:fixed; inset:0; opacity:.12; pointer-events:none; background-image:repeating-linear-gradient(0deg,transparent,transparent 3px,#fff 4px); mix-blend-mode:overlay; } .eyebrow { margin:0 0 .5rem; color:var(--acid); font-size:.72rem; letter-spacing:.2em; text-transform:uppercase; } From e307539c818f0cfc92830e11f0e185f74424a98c Mon Sep 17 00:00:00 2001 From: Zac Yang Date: Wed, 26 Aug 2026 03:17:12 -0400 Subject: [PATCH 2/2] Stop echoing the password in hash-password.mjs readline.question leaves the plaintext in scrollback and shell history. Read raw keystrokes instead and require a TTY. Co-authored-by: Cursor --- bin/hash-password.mjs | 49 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/bin/hash-password.mjs b/bin/hash-password.mjs index 1ddc488..9b0f068 100644 --- a/bin/hash-password.mjs +++ b/bin/hash-password.mjs @@ -1,17 +1,48 @@ #!/usr/bin/env node import crypto from "node:crypto"; -import readline from "node:readline"; const username = process.argv[2]; if (!username || !/^[a-zA-Z0-9._-]{1,64}$/.test(username)) { console.error("usage: bin/hash-password.mjs USERNAME"); process.exit(2); } -const terminal = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true }); -terminal.question("Password: ", (password) => { - terminal.close(); - if (password.length < 12) { console.error("password must be at least 12 characters"); process.exit(2); } - const salt = crypto.randomBytes(16); - const hash = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }); - console.log(JSON.stringify({ username, passwordHash: `scrypt$16384$8$1$${salt.toString("base64url")}$${hash.toString("base64url")}` })); -}); + +// Echoing the password would leave it in scrollback, shell history, and any +// captured terminal log, so the prompt reads raw keystrokes instead. +function readPassword(prompt) { + return new Promise((resolve, reject) => { + const input = process.stdin; + if (!input.isTTY) { + reject(new Error("stdin must be a terminal")); + return; + } + process.stderr.write(prompt); + input.setRawMode(true); + input.resume(); + input.setEncoding("utf8"); + let value = ""; + const settle = (error, result) => { + input.removeListener("data", onData); + input.setRawMode(false); + input.pause(); + process.stderr.write("\n"); + if (error) reject(error); + else resolve(result); + }; + const onData = (chunk) => { + for (const character of chunk) { + if (character === "\r" || character === "\n" || character === "\u0004") return settle(null, value); + if (character === "\u0003") return settle(new Error("cancelled")); + if (character === "\u007f" || character === "\b") value = value.slice(0, -1); + else if (character >= " ") value += character; + } + }; + input.on("data", onData); + }); +} + +const password = await readPassword("Password: "); +if (password.length < 12) { console.error("password must be at least 12 characters"); process.exit(2); } +const salt = crypto.randomBytes(16); +const hash = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }); +console.log(JSON.stringify({ username, passwordHash: `scrypt$16384$8$1$${salt.toString("base64url")}$${hash.toString("base64url")}` }));