Added braille spinner to verify Query & Verify button; restructured verify results into a single card flow with OTS stamp/upgraded cards and a final summary card

This commit is contained in:
Laan Tungir
2026-07-29 09:00:26 -04:00
parent b46d081211
commit 33e06adc4a
4 changed files with 246 additions and 78 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nostr_quantum_preparation",
"version": "0.1.0",
"version": "0.1.1",
"description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.",
"main": "index.js",
"scripts": {
+183 -37
View File
@@ -50,17 +50,55 @@
const resultList = document.getElementById('pqResultList');
const eventJsonWrap = document.getElementById('pqEventJsonWrap');
const eventJsonEl = document.getElementById('pqEventJson');
const otsBadge = document.getElementById('pqOtsBadge');
const otsUpgradeWrap = document.getElementById('pqOtsUpgradeWrap');
const otsInfo = document.getElementById('pqOtsInfo');
const otsUpgradeBtn = document.getElementById('pqOtsUpgradeBtn');
const otsRepublishBtn = document.getElementById('pqOtsRepublishBtn');
const otsUpgradeStatus = document.getElementById('pqOtsUpgradeStatus');
const summaryWrap = document.getElementById('pqSummaryWrap');
const summaryCard = document.getElementById('pqSummaryCard');
// OTS cards are created dynamically and appended into the shared result
// list so they flow as part of the same card list as the signature checks.
let otsStampCard = null;
let otsUpgradedCard = null;
let currentEvent = null;
let currentOtsBytes = null;
let currentExpectedAuthor = null;
/* Braille spinner: shown inside a button while its action is running
(e.g. Query & Verify). Advances through the standard braille spinner
frames every 80ms, matching the spinners on the preparation page. */
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const buttonSpinnerIntervals = new Map();
function setButtonSpinner(btn) {
if (!btn) return;
clearButtonSpinner(btn);
let span = btn.querySelector('.pq-btn-spinner');
if (!span) {
span = document.createElement('span');
span.className = 'pq-btn-spinner';
btn.insertBefore(span, btn.firstChild);
}
let frame = 0;
span.textContent = SPINNER_FRAMES[0];
buttonSpinnerIntervals.set(btn, setInterval(() => {
frame = (frame + 1) % SPINNER_FRAMES.length;
span.textContent = SPINNER_FRAMES[frame];
}, 80));
}
function clearButtonSpinner(btn) {
if (!btn) return;
if (buttonSpinnerIntervals.has(btn)) {
clearInterval(buttonSpinnerIntervals.get(btn));
buttonSpinnerIntervals.delete(btn);
}
const span = btn.querySelector('.pq-btn-spinner');
if (span) span.remove();
}
// F-H3: Build status DOM safely — type is internally controlled, message uses textContent
function setStatus(element, type, message) {
const div = document.createElement('div');
@@ -77,13 +115,70 @@
resultList.appendChild(item);
}
// F-H3: Helper to set OTS badge (static HTML, safe) and info (textContent, safe)
function setOtsBadge(className, text) {
otsBadge.innerHTML = `<span class="pq-ots-badge ${className}"></span>`;
otsBadge.querySelector('span').textContent = text;
// OTS state tracked for the summary card.
let otsStampState = { valid: null, label: '', detail: '' };
let otsUpgradedState = { valid: null, label: '', detail: '' };
// Render an OTS card (stamp or upgraded). The card is created on demand
// and appended to the shared result list so it flows with the signature
// checks. `which` is 'stamp' or 'upgraded'; `state` is one of
// 'valid' | 'invalid' | 'pending'.
function setOtsCard(which, state, head, body) {
const card = which === 'upgraded' ? otsUpgradedCard : otsStampCard;
if (!card) {
// Create the card and append it to the result list.
const el = document.createElement('div');
el.className = 'pq-result-item pq-result-detail';
resultList.appendChild(el);
if (which === 'upgraded') otsUpgradedCard = el; else otsStampCard = el;
}
const target = which === 'upgraded' ? otsUpgradedCard : otsStampCard;
target.style.display = 'block';
target.classList.remove('pq-result-valid', 'pq-result-invalid', 'pq-result-pending');
if (state === 'valid') target.classList.add('pq-result-valid');
else if (state === 'invalid') target.classList.add('pq-result-invalid');
else if (state === 'pending') target.classList.add('pq-result-pending');
// F-H3: build DOM safely — head and body use textContent
target.innerHTML = '';
const headEl = document.createElement('div');
headEl.className = 'pq-result-head';
headEl.textContent = head;
const bodyEl = document.createElement('div');
bodyEl.className = 'pq-result-body';
bodyEl.textContent = body;
target.appendChild(headEl);
target.appendChild(bodyEl);
}
function setOtsInfo(text) {
otsInfo.textContent = text;
function hideOtsCard(which) {
const card = which === 'upgraded' ? otsUpgradedCard : otsStampCard;
if (card) { card.remove(); }
if (which === 'upgraded') otsUpgradedCard = null; else otsStampCard = null;
}
// Render the final summary card based on signature validity and OTS state.
function renderSummary(allValid) {
const stampOk = otsStampState.valid === true;
const upgradedOk = otsUpgradedState.valid === true;
const anyOtsConfirmed = stampOk || upgradedOk;
summaryWrap.style.display = 'block';
summaryCard.classList.remove('pq-result-valid', 'pq-result-invalid', 'pq-result-pending');
if (allValid && anyOtsConfirmed) {
summaryCard.classList.add('pq-result-valid');
const anchor = upgradedOk
? (otsUpgradedState.label || 'the upgraded proof')
: (otsStampState.label || 'the original proof');
summaryCard.textContent = `All signatures are verified and valid, and the event is stamped on the Bitcoin blockchain (via ${anchor}).`;
} else if (allValid && otsStampState.valid === 'pending') {
summaryCard.classList.add('pq-result-pending');
summaryCard.textContent = 'All signatures are verified and valid. The OpenTimestamps proof is pending — not yet stamped on the Bitcoin blockchain. You can upgrade it below.';
} else if (allValid) {
summaryCard.classList.add('pq-result-invalid');
summaryCard.textContent = 'All signatures are verified and valid, but no Bitcoin timestamp attestation was found.';
} else {
summaryCard.classList.add('pq-result-invalid');
summaryCard.textContent = 'Some checks failed verification. See the results above for details.';
}
}
/* ================================================================
@@ -174,9 +269,13 @@
resultsWrap.style.display = 'none';
eventJsonWrap.style.display = 'none';
otsUpgradeWrap.style.display = 'none';
otsBadge.innerHTML = '';
hideOtsCard('stamp');
hideOtsCard('upgraded');
summaryWrap.style.display = 'none';
otsStampState = { valid: null, label: '', detail: '' };
otsUpgradedState = { valid: null, label: '', detail: '' };
// Display full proof carrier JSON
// Display full proof carrier JSON at the top of the page
eventJsonEl.textContent = JSON.stringify(event, null, 2);
eventJsonWrap.style.display = 'block';
@@ -237,9 +336,10 @@
}
// 3. Inspect OTS proof tag and verify it matches the sha256 tag.
// First do a quick structural check for immediate UI feedback, then
// run full cryptographic verification (async — fetches Bitcoin block
// headers and validates the Merkle path).
// The OTS result is rendered as a dedicated card (pqOtsStampCard)
// alongside the signature results. If the proof is pending, an
// upgrade card (pqOtsUpgradedCard) is reserved for a later upgraded
// proof. A final summary card is rendered at the end.
const otsTag = event.tags.find(t => t[0] === 'ots');
const sha256Tag = event.tags.find(t => t[0] === 'sha256');
if (otsTag && otsTag[1]) {
@@ -254,17 +354,27 @@
: `WARNING: sha256 tag (${sha256Tag[1].substring(0, 16)}...) does not match computed hash (${fullHash.substring(0, 16)}...)`)
: 'No sha256 tag found';
// Quick structural display first
otsUpgradeWrap.style.display = 'block';
if (!validFile) {
setOtsBadge('pq-ots-badge-none', 'OTS: Invalid format');
setOtsInfo(`OTS tag present but does not appear to be a valid detached .ots file (${currentOtsBytes.length} bytes). ${hashMatchNote}.`);
otsUpgradeWrap.style.display = 'none';
} else if (hasBitcoinAttestation) {
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verifying...');
setOtsInfo(`OpenTimestamps proof contains a Bitcoin attestation. Performing full cryptographic verification (fetching block header)... Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
otsUpgradeWrap.style.display = 'block';
// Invalid format — no upgrade possible
otsStampState = { valid: false, label: '', detail: '' };
setOtsCard('stamp', 'invalid', 'OTS Stamp: Invalid format',
`OTS tag present but does not appear to be a valid detached .ots file (${currentOtsBytes.length} bytes). ${hashMatchNote}.`);
hideOtsCard('upgraded');
otsUpgradeBtn.style.display = 'none';
otsRepublishBtn.style.display = 'none';
renderSummary(allValid);
} else if (hasBitcoinAttestation) {
// Contains a Bitcoin attestation — run full async verification.
// Show a "verifying" stamp card first, then update it.
otsStampState = { valid: null, label: '', detail: '' };
setOtsCard('stamp', 'pending', 'OTS Stamp: Verifying...',
`OpenTimestamps proof contains a Bitcoin attestation. Performing full cryptographic verification (fetching block header)... Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
hideOtsCard('upgraded');
otsUpgradeBtn.style.display = 'none';
otsRepublishBtn.style.display = 'none';
renderSummary(allValid);
// Run full async verification: parse the proof, bind the target
// digest to the sha256 tag, walk the Merkle path, and check the
@@ -278,36 +388,56 @@
: result.trustMode === 'single-explorer-checked'
? 'single-explorer-checked (trusted API)'
: result.trustMode || 'unknown';
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)');
setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
otsStampState = {
valid: true,
label: `Bitcoin block ${att.height}`,
detail: `OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`
};
setOtsCard('stamp', 'valid', 'OTS Stamp: Verified (Bitcoin)', otsStampState.detail);
otsUpgradeBtn.style.display = 'none';
renderSummary(allValid);
} else {
setOtsBadge('pq-ots-badge-none', 'OTS: Verification failed');
const errDetail = result.errors.length > 0 ? ` Errors: ${result.errors.join('; ')}` : '';
setOtsInfo(`OpenTimestamps proof could NOT be cryptographically verified. ${hashMatchNote}.${errDetail}`);
otsStampState = { valid: false, label: '', detail: '' };
setOtsCard('stamp', 'invalid', 'OTS Stamp: Verification failed',
`OpenTimestamps proof could NOT be cryptographically verified. ${hashMatchNote}.${errDetail}`);
otsUpgradeBtn.style.display = 'inline-block';
renderSummary(allValid);
}
}).catch(err => {
setOtsBadge('pq-ots-badge-none', 'OTS: Verification error');
setOtsInfo(`OpenTimestamps verification error: ${err.message}. ${hashMatchNote}.`);
otsStampState = { valid: false, label: '', detail: '' };
setOtsCard('stamp', 'invalid', 'OTS Stamp: Verification error',
`OpenTimestamps verification error: ${err.message}. ${hashMatchNote}.`);
otsUpgradeBtn.style.display = 'inline-block';
renderSummary(allValid);
});
} else {
setOtsBadge('pq-ots-badge-pending', 'OTS: Pending');
setOtsInfo(`OpenTimestamps proof is pending (no Bitcoin attestation yet). Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}. You can try upgrading it below.`);
otsUpgradeWrap.style.display = 'block';
// Pending — no Bitcoin attestation yet. Reserve the upgraded card
// for when the user upgrades the proof below.
otsStampState = { valid: 'pending', label: '', detail: '' };
setOtsCard('stamp', 'pending', 'OTS Stamp: Pending',
`OpenTimestamps proof is pending (no Bitcoin attestation yet). Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}. You can try upgrading it below.`);
hideOtsCard('upgraded');
otsUpgradeBtn.style.display = 'inline-block';
otsRepublishBtn.style.display = 'none';
renderSummary(allValid);
}
} catch (e) {
setOtsBadge('pq-ots-badge-none', 'OTS: Error');
setOtsInfo(`Failed to parse OTS proof: ${e.message}`);
otsUpgradeWrap.style.display = 'none';
otsStampState = { valid: false, label: '', detail: '' };
setOtsCard('stamp', 'invalid', 'OTS Stamp: Error',
`Failed to parse OTS proof: ${e.message}`);
hideOtsCard('upgraded');
otsUpgradeBtn.style.display = 'none';
otsRepublishBtn.style.display = 'none';
renderSummary(allValid);
}
} else {
setOtsBadge('pq-ots-badge-none', 'OTS: None');
setOtsInfo('No OpenTimestamps proof tag found in this event.');
otsStampState = { valid: false, label: '', detail: '' };
setOtsCard('stamp', 'invalid', 'OTS Stamp: None',
'No OpenTimestamps proof tag found in this event.');
hideOtsCard('upgraded');
otsUpgradeWrap.style.display = 'none';
renderSummary(allValid);
}
resultsWrap.style.display = 'block';
@@ -322,6 +452,7 @@
queryBtn.addEventListener('click', async () => {
queryBtn.disabled = true;
setButtonSpinner(queryBtn);
setStatus(queryStatus, 'info', 'Querying relays...');
const pubkeyInput = document.getElementById('pqPubkeyInput').value.trim();
@@ -331,6 +462,7 @@
if (!pubkeyHex) {
setStatus(queryStatus, 'error', 'Invalid pubkey. Enter a 64-char hex pubkey or an npub.');
queryBtn.disabled = false;
clearButtonSpinner(queryBtn);
return;
}
@@ -338,6 +470,7 @@
if (relayUrls.length === 0) {
setStatus(queryStatus, 'error', 'Enter at least one relay URL.');
queryBtn.disabled = false;
clearButtonSpinner(queryBtn);
return;
}
@@ -373,6 +506,7 @@
if (allCandidates.length === 0) {
setStatus(queryStatus, 'error', `No proof carrier events found for this pubkey on any of the ${relayUrls.length} relay(s)${lastError ? ' (last error: ' + lastError + ')' : ''}.`);
queryBtn.disabled = false;
clearButtonSpinner(queryBtn);
return;
}
@@ -397,6 +531,7 @@
if (!kind1Event) {
setStatus(queryStatus, 'error', 'Could not parse kind 1 announcement from any candidate.');
queryBtn.disabled = false;
clearButtonSpinner(queryBtn);
return;
}
@@ -427,6 +562,7 @@
}
queryBtn.disabled = false;
clearButtonSpinner(queryBtn);
});
/* ================================================================
@@ -523,10 +659,20 @@
setStatus(otsUpgradeStatus, 'success', `Bitcoin attestation verified! Block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof upgraded (${currentOtsBytes.length} bytes). You can publish a new kind 9999 event carrying the confirmed proof below; it will reference the original via an upgrade_of tag.`);
otsUpgradeBtn.style.display = 'none';
otsRepublishBtn.style.display = 'inline-block';
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)');
setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'}. Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.`);
// Render the upgraded proof as a second OTS card and refresh summary.
otsUpgradedState = {
valid: true,
label: `Bitcoin block ${att ? att.height : '?'}`,
detail: `OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.`
};
setOtsCard('upgraded', 'valid', 'OTS Upgraded: Verified (Bitcoin)', otsUpgradedState.detail);
renderSummary(true);
} else {
setStatus(otsUpgradeStatus, 'info', `Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`);
otsUpgradedState = { valid: 'pending', label: '', detail: '' };
setOtsCard('upgraded', 'pending', 'OTS Upgraded: Still pending',
`Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`);
renderSummary(true);
}
} catch (error) {
setStatus(otsUpgradeStatus, 'error', `Upgrade failed: ${error.message}`);
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.1.0",
"VERSION_NUMBER": "0.1.0",
"BUILD_DATE": "2026-07-28T10:01:19.383Z"
"VERSION": "v0.1.1",
"VERSION_NUMBER": "0.1.1",
"BUILD_DATE": "2026-07-29T13:00:25.910Z"
}
+59 -37
View File
@@ -69,6 +69,22 @@
.pq-button:hover { opacity: 0.7; }
.pq-button:disabled { opacity: 0.3; cursor: not-allowed; }
/* Braille spinner shown inside a button while its action is running.
Frames are advanced by JS (setButtonSpinner); this class just styles
the inline spinner glyph. */
.pq-btn-spinner {
display: inline-block;
color: var(--accent-color);
font-size: 16px;
line-height: 1;
margin-right: 6px;
vertical-align: middle;
}
/* Keep the spinner fully visible even while the button is dimmed
(disabled) during a running action. */
.pq-button:disabled .pq-btn-spinner { opacity: 1; }
.pq-button-row {
display: flex;
gap: 10px;
@@ -164,6 +180,31 @@
border-left: 4px solid #cc0000;
}
.pq-result-pending {
border-left: 4px solid #f0ad4e;
}
/* Multi-line OTS / summary cards wrap their detail text. */
.pq-result-item.pq-result-detail {
display: block;
line-height: 1.6;
white-space: normal;
}
.pq-result-detail .pq-result-head {
font-weight: bold;
margin-bottom: 4px;
}
.pq-result-detail .pq-result-body {
font-size: 13px;
font-weight: normal;
}
.pq-summary-card {
font-size: 16px;
font-weight: bold;
padding: 15px 18px;
}
.pq-event-preview {
background: var(--secondary-color);
color: var(--primary-color);
@@ -211,30 +252,6 @@
.pq-tab-panel { display: none; }
.pq-tab-panel.pq-tab-panel-active { display: block; }
.pq-ots-badge {
display: inline-block;
padding: 3px 10px;
border-radius: var(--border-radius);
font-size: 12px;
font-weight: bold;
margin-left: 8px;
}
.pq-ots-badge-pending {
background: #f0ad4e;
color: #fff;
}
.pq-ots-badge-confirmed {
background: #00aa00;
color: #fff;
}
.pq-ots-badge-none {
background: var(--muted-color);
color: var(--secondary-color);
}
</style>
</head>
@@ -314,24 +331,24 @@
<div id="pqSignInStatus"></div>
</div>
<!-- Shared results area -->
<div id="pqResultsWrap" style="display: none;">
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;">
<strong>Verification Results:</strong>
<span id="pqOtsBadge"></span>
</div>
<div class="pq-result-list" id="pqResultList"></div>
</div>
<!-- Full event JSON (shown at the top once an event is loaded) -->
<div id="pqEventJsonWrap" style="display: none; margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Full event JSON:</strong></div>
<div class="pq-event-preview" id="pqEventJson"></div>
</div>
<!-- OTS upgrade section -->
<div id="pqOtsUpgradeWrap" style="display: none; margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>OpenTimestamps:</strong></div>
<div id="pqOtsInfo" class="pq-status pq-status-info"></div>
<!-- Shared results area: signature + PQ checks + OTS cards, all in
one continuous card list. The OTS stamp/upgraded cards are
appended into pqResultList by JS so the flow is unbroken. -->
<div id="pqResultsWrap" style="display: none;">
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;">
<strong>Verification Results:</strong>
</div>
<div class="pq-result-list" id="pqResultList"></div>
</div>
<!-- OTS upgrade actions (cards live in pqResultList above) -->
<div id="pqOtsUpgradeWrap" style="display: none; margin-top: 10px;">
<div class="pq-button-row">
<button class="pq-button" id="pqOtsUpgradeBtn">Upgrade OTS Proof</button>
<button class="pq-button" id="pqOtsRepublishBtn" style="display:none;">Publish Upgraded Event</button>
@@ -339,6 +356,11 @@
<div id="pqOtsUpgradeStatus"></div>
</div>
<!-- Summary card (shown once verification completes) -->
<div id="pqSummaryWrap" style="display: none; margin-top: 15px;">
<div class="pq-result-item pq-summary-card" id="pqSummaryCard"></div>
</div>
<hr style="border: var(--border); margin: 25px 0 15px;" />
<div class="pq-info-text" style="color: var(--accent-color); font-size: 13px;">