fix: move hashcash PoW from build artifact to JSX source

The hashcash proof-of-work implementation was incorrectly added to the
build artifact web/dist/app.js instead of the source file web/src/app.jsx.
Running web/build.sh would overwrite all hashcash changes.

Changes:
- Add checkLeadingZeros() and mintHashcash() functions to app.jsx
- Integrate hashcash into LoginScreen: fetch hashcash_bits from /server,
  compute stamp via Web Crypto API before session creation, show
  'Computing proof-of-work...' feedback
- Remove web/dist/ from git tracking (build artifacts)
- Add web/dist/ to .gitignore
This commit is contained in:
clawbot
2026-03-10 02:58:48 -07:00
parent fe937b5ebb
commit 50e7a93287

View File

@@ -8,6 +8,56 @@ const MEMBER_REFRESH_INTERVAL = 10000;
const ACTION_PREFIX = "\x01ACTION "; const ACTION_PREFIX = "\x01ACTION ";
const ACTION_SUFFIX = "\x01"; const ACTION_SUFFIX = "\x01";
// Hashcash proof-of-work helpers using Web Crypto API.
function checkLeadingZeros(hashBytes, bits) {
let count = 0;
for (let i = 0; i < hashBytes.length; i++) {
if (hashBytes[i] === 0) {
count += 8;
continue;
}
let b = hashBytes[i];
while ((b & 0x80) === 0) {
count++;
b <<= 1;
}
break;
}
return count >= bits;
}
async function mintHashcash(bits, resource) {
const encoder = new TextEncoder();
const now = new Date();
const date =
String(now.getUTCFullYear()).slice(2) +
String(now.getUTCMonth() + 1).padStart(2, "0") +
String(now.getUTCDate()).padStart(2, "0");
const prefix = `1:${bits}:${date}:${resource}::`;
let nonce = Math.floor(Math.random() * 0x100000);
const batchSize = 1024;
for (;;) {
const stamps = [];
const hashPromises = [];
for (let i = 0; i < batchSize; i++) {
const stamp = prefix + (nonce + i).toString(16);
stamps.push(stamp);
hashPromises.push(
crypto.subtle.digest("SHA-256", encoder.encode(stamp)),
);
}
const hashes = await Promise.all(hashPromises);
for (let i = 0; i < hashes.length; i++) {
if (checkLeadingZeros(new Uint8Array(hashes[i]), bits)) {
return stamps[i];
}
}
nonce += batchSize;
}
}
function api(path, opts = {}) { function api(path, opts = {}) {
const token = localStorage.getItem("neoirc_token"); const token = localStorage.getItem("neoirc_token");
const headers = { const headers = {
@@ -60,12 +110,16 @@ function LoginScreen({ onLogin }) {
const [motd, setMotd] = useState(""); const [motd, setMotd] = useState("");
const [serverName, setServerName] = useState("NeoIRC"); const [serverName, setServerName] = useState("NeoIRC");
const inputRef = useRef(); const inputRef = useRef();
const hashcashBitsRef = useRef(0);
const hashcashResourceRef = useRef("neoirc");
useEffect(() => { useEffect(() => {
api("/server") api("/server")
.then((s) => { .then((s) => {
if (s.name) setServerName(s.name); if (s.name) setServerName(s.name);
if (s.motd) setMotd(s.motd); if (s.motd) setMotd(s.motd);
hashcashBitsRef.current = s.hashcash_bits || 0;
if (s.name) hashcashResourceRef.current = s.name;
}) })
.catch(() => {}); .catch(() => {});
const saved = localStorage.getItem("neoirc_token"); const saved = localStorage.getItem("neoirc_token");
@@ -81,9 +135,20 @@ function LoginScreen({ onLogin }) {
e.preventDefault(); e.preventDefault();
setError(""); setError("");
try { try {
const extraHeaders = {};
if (hashcashBitsRef.current > 0) {
setError("Computing proof-of-work...");
const stamp = await mintHashcash(
hashcashBitsRef.current,
hashcashResourceRef.current,
);
extraHeaders["X-Hashcash"] = stamp;
setError("");
}
const res = await api("/session", { const res = await api("/session", {
method: "POST", method: "POST",
body: JSON.stringify({ nick: nick.trim() }), body: JSON.stringify({ nick: nick.trim() }),
headers: extraHeaders,
}); });
localStorage.setItem("neoirc_token", res.token); localStorage.setItem("neoirc_token", res.token);
onLogin(res.nick); onLogin(res.nick);