feat(pqsign): WASM signer for browser-side post-quantum identity

WebCrypto has Ed25519 but no ML-DSA, so the post-quantum half is
compiled from the same pkg/pqid the server verifies with. One
implementation of the scheme in the project means a client and server
cannot disagree about signing.

The Ed25519 half is stored as its 32-byte seed rather than the expanded
key, since the seed cannot encode an inconsistent pair, and the public
key is derived rather than stored so a client cannot present one that
does not match what it signs with.

Verified end to end in a JS runtime: 1984-byte public key, 3373-byte
signature, derived key matches, malformed input returns an error rather
than crashing the module. 3.4MB, 0.9MB gzipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-08-06 04:10:51 +00:00
parent 8af6fd585e
commit b0f07f63ff
11 changed files with 1170 additions and 5 deletions

View File

@@ -297,26 +297,54 @@ function onSnapshot(s) {
mult.className = myBet === 'out' ? 'multiplier won' : 'multiplier crashed';
$('state').textContent =
`crashed — next round in ${Math.max(0, s.next_phase_in_seconds).toFixed(0)}s`;
action.textContent = 'Next round';
action.textContent = 'Bet again';
action.className = 'primary big';
action.disabled = true;
action.disabled = false;
// Record the round once, on the transition into settled.
if (!wasSettled && s.crash_point) {
stats.crashes.push(crash);
if (myBet === 'in') {
stats.losses++;
hint.textContent = 'Rode it too far.';
hint.className = 'hint bad';
// Near-miss psychology: show how close they were
const autoTarget = parseFloat($('auto-target').value || '0');
if (autoTarget > 1 && crash > 1.0 && crash < autoTarget) {
hint.innerHTML = `Almost! Crashed at ${crash.toFixed(2)}× — you were ${((autoTarget - crash) * 100).toFixed(0)}% away from ${autoTarget.toFixed(2)}×.`;
hint.className = 'hint near-miss';
} else {
hint.textContent = crash < 1.5 ? 'Brutal — early crash.' : crash < 3 ? 'Rode it too far.' : 'So close to a monster.';
hint.className = 'hint bad';
}
stats.streak = 0;
buzz(120);
} else if (myBet === 'out') {
stats.wins++;
buzz([30, 40, 30]);
stats.streak = (stats.streak || 0) + 1;
const payout = Math.round(stake * crash / 1000);
hint.textContent = stats.streak >= 5
? `🔥 ${stats.streak} IN A ROW! +${sats(payout * 1000)} sats`
: stats.streak >= 3
? `On fire! ${stats.streak} wins straight. +${sats(payout * 1000)} sats`
: `Won +${sats(payout * 1000)} sats at ${crash.toFixed(2)}×`;
hint.className = 'hint good';
if (stats.streak >= 3) buzz([20, 30, 20, 30, 40]);
else buzz([30, 40, 30]);
// Auto-increment stake on hot streak
if (stats.streak >= 3 && stake < 25000) {
const newStake = stake * 2;
setStake(newStake);
hint.textContent += ' • Stake doubled!';
}
}
if (!stats.best || crash > stats.best) stats.best = crash;
saveStats();
renderStrip();
refreshBalance();
}
// Pre-fill for instant re-bet: auto-bet on next round
if (!wasSettled && myBet === 'out') {
action.classList.add('pulse');
}
break;
}
}