Redesign migration UI: consolidate seed buttons, plain-text PQ key checklist, remove signature cards, per-relay publish checkboxes, rewrite OTS step with auto-polling, remove inverted black backgrounds, add header padding and copy kind 11112 event

This commit is contained in:
Laan Tungir
2026-07-18 17:43:27 -04:00
parent 2dbfc16f98
commit 0ef2d6be72
3 changed files with 243 additions and 192 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "post_quantum_nostr",
"version": "0.0.2",
"version": "0.0.3",
"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": {
+239 -188
View File
@@ -11,6 +11,14 @@
<link rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;," />
<style>
#divHeaderText {
font-size: calc(max(var(--header-height), var(--header-min-height)) * 0.5) !important;
}
#divHeaderFlexRight {
padding-right: 20px;
}
#divBody {
flex-direction: column !important;
flex-wrap: nowrap !important;
@@ -128,8 +136,9 @@
.pq-warning strong { color: var(--accent-color); }
.pq-seed-display {
background: var(--primary-color);
color: var(--secondary-color);
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 20px;
margin: 15px 0;
@@ -210,7 +219,8 @@
.pq-status-error { background: var(--accent-color); color: var(--secondary-color); }
.pq-event-preview {
background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius);
background: var(--secondary-color); color: var(--primary-color); border: var(--border);
border-radius: var(--border-radius);
padding: 15px; font-size: 11px; white-space: pre-wrap; word-break: break-all;
max-height: 400px; overflow-y: auto; text-align: left;
}
@@ -325,10 +335,6 @@
<li>Publish the event to relays</li>
</ol>
</div>
<div class="pq-warning">
<strong>Important:</strong> Your seed phrase is generated and handled entirely in your browser.
It is never sent to any server. Write it down on paper — it's your quantum-safe backup.
</div>
<button class="pq-button" id="pqStartBtn">Start Migration</button>
</div>
@@ -341,27 +347,24 @@
The code is open source and auditable.
</div>
<!-- Toggle: Generate vs Bring your own -->
<!-- Mode toggle + copy row (always visible) -->
<div class="pq-button-row" style="margin-bottom: 15px;">
<button class="pq-button pq-button-secondary pq-seed-toggle pq-seed-toggle-active" id="pqSeedToggleGenerate">Generate New Seed</button>
<button class="pq-button pq-button-secondary pq-seed-toggle" id="pqSeedToggleOwn">I Have a Seed Phrase</button>
<button class="pq-button pq-button-secondary" id="pqCopySeedBtn">Copy</button>
</div>
<!-- Generate mode -->
<div id="pqSeedGeneratePanel">
<div class="pq-seed-display" id="pqSeedDisplay"></div>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary" id="pqCopySeedBtn">Copy</button>
<button class="pq-button pq-button-secondary" id="pqRegenerateBtn">Generate New Seed</button>
</div>
<div class="pq-info-text" style="margin-top: 15px; font-size: 13px;">
<strong>Optional:</strong> Add extra entropy by moving your mouse and typing in the box below.
This mixes your input with the browser's random number generator, making the seed unpredictable
even if the browser's RNG is compromised. You can skip this — a seed has already been generated for you above.
</div>
<div id="pqEntropyBox" style="background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius); padding: 15px; font-size: 13px; min-height: 60px; margin: 10px 0; cursor: text; text-align: left;" tabindex="0">
<div id="pqEntropyBox" style="background: var(--secondary-color); color: var(--primary-color); border: var(--border); border-radius: var(--border-radius); padding: 15px; font-size: 13px; min-height: 60px; margin: 10px 0; cursor: text; text-align: left;" tabindex="0">
<span id="pqEntropyHint">Optional: click here and type random characters, move your mouse to add entropy...</span>
<div style="margin-top: 8px; height: 6px; background: var(--secondary-color); border-radius: 3px; overflow: hidden;">
<div style="margin-top: 8px; height: 6px; background: var(--muted-color); border-radius: 3px; overflow: hidden;">
<div id="pqEntropyBar" style="height: 100%; width: 0%; background: var(--accent-color); transition: width 0.2s;"></div>
</div>
</div>
@@ -403,47 +406,37 @@
<div class="pq-progress-bar">
<div class="pq-progress-fill" id="pqDeriveProgress"></div>
</div>
<div class="pq-key-list" id="pqKeyList">
<div class="pq-key-item" id="pqKeyMlDsa44">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">ML-DSA-44 (Dilithium)</div>
<div class="pq-key-meta">FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1</div>
<div class="pq-key-pubkey" id="pqKeyMlDsa44Pub"></div>
</div>
<div class="pq-checklist" id="pqKeyList" style="margin-bottom: 15px;">
<div class="pq-checklist-item" id="pqKeyMlDsa44">
<span class="pq-checklist-box"></span>
<span>ML-DSA-44 (Dilithium) — FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1
<div class="pq-key-pubkey" id="pqKeyMlDsa44Pub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-key-item" id="pqKeyMlDsa65">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">ML-DSA-65 (Dilithium)</div>
<div class="pq-key-meta">FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2</div>
<div class="pq-key-pubkey" id="pqKeyMlDsa65Pub"></div>
</div>
<div class="pq-checklist-item" id="pqKeyMlDsa65">
<span class="pq-checklist-box"></span>
<span>ML-DSA-65 (Dilithium) — FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2
<div class="pq-key-pubkey" id="pqKeyMlDsa65Pub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-key-item" id="pqKeySlhDsa">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">SLH-DSA-128s (SPHINCS+)</div>
<div class="pq-key-meta">FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4</div>
<div class="pq-key-pubkey" id="pqKeySlhDsaPub"></div>
</div>
<div class="pq-checklist-item" id="pqKeySlhDsa">
<span class="pq-checklist-box"></span>
<span>SLH-DSA-128s (SPHINCS+) — FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4
<div class="pq-key-pubkey" id="pqKeySlhDsaPub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-key-item" id="pqKeyFalcon">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Falcon-512</div>
<div class="pq-key-meta">FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6</div>
<div class="pq-key-pubkey" id="pqKeyFalconPub"></div>
<div class="pq-checklist-item" id="pqKeyFalcon">
<span class="pq-checklist-box"></span>
<span>Falcon-512 — FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6
<div class="pq-key-pubkey" id="pqKeyFalconPub" style="margin-top: 2px;"></div>
<div style="font-size: 11px; color: var(--accent-color); margin-top: 4px;">⚠ Draft standard — may need re-issuing if FIPS 206 changes</div>
</div>
</span>
</div>
<div class="pq-key-item" id="pqKeyMlKem">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">ML-KEM-768 (Kyber)</div>
<div class="pq-key-meta">FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8</div>
<div class="pq-key-pubkey" id="pqKeyMlKemPub"></div>
</div>
<div class="pq-checklist-item" id="pqKeyMlKem">
<span class="pq-checklist-box"></span>
<span>ML-KEM-768 (Kyber) — FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8
<div class="pq-key-pubkey" id="pqKeyMlKemPub" style="margin-top: 2px;"></div>
</span>
</div>
</div>
<div id="pqDeriveStatus"></div>
@@ -460,50 +453,6 @@
full kind 1 event (including its secp256k1 signature) is submitted to OpenTimestamps.
</div>
<div id="pqSignStatus"></div>
<div class="pq-key-list" id="pqSignChecklist">
<div class="pq-key-item" id="pqSignPqMlDsa44">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">PQ signature: ML-DSA-44</div>
<div class="pq-key-meta">Done in browser (pure JS)</div>
</div>
</div>
<div class="pq-key-item" id="pqSignPqMlDsa65">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">PQ signature: ML-DSA-65</div>
<div class="pq-key-meta">Done in browser (pure JS)</div>
</div>
</div>
<div class="pq-key-item" id="pqSignPqSlhDsa">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">PQ signature: SLH-DSA-128s</div>
<div class="pq-key-meta">Done in browser (pure JS)</div>
</div>
</div>
<div class="pq-key-item" id="pqSignPqFalcon">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">PQ signature: Falcon-512</div>
<div class="pq-key-meta">Done in browser (pure JS)</div>
</div>
</div>
<div class="pq-key-item" id="pqSignSecp">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">secp256k1 signature: kind 1 announcement</div>
<div class="pq-key-meta">Requires approval from your signer</div>
</div>
</div>
<div class="pq-key-item" id="pqSignSecpWrapper">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">secp256k1 signature: kind 11112 wrapper</div>
<div class="pq-key-meta">Requires approval from your signer</div>
</div>
</div>
</div>
<button class="pq-button" id="pqSignBtn">Sign Event</button>
<div id="pqEventPreviewWrap" class="pq-hidden" style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;">Event preview:</div>
@@ -527,12 +476,17 @@
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;"><strong>Kind 11112 wrapper event:</strong></div>
<div id="pqEventIdDisplay" style="margin: 5px 0; font-size: 11px; color: var(--primary-color); word-break: break-all;"></div>
<div class="pq-event-preview" id="pqSuccessEventPreview"></div>
<div class="pq-button-row" style="margin-top: 10px;">
<button class="pq-button pq-button-secondary" id="pqCopyKind11112Btn">Copy Kind 11112 Event</button>
</div>
<div style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Relays to publish to:</strong></div>
<input type="text" class="pq-relay-input" id="pqRelaysInput"
value="wss://laantungir.net/relay,wss://relay.damus.io,wss://nos.lol,wss://relay.primal.net"
placeholder="wss://relay1.com,wss://relay2.com" />
<div id="pqRelayList" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px;"></div>
<div class="pq-button-row">
<input type="text" class="pq-relay-input" id="pqRelayAddInput" style="flex: 1; margin: 0;" placeholder="wss://relay.example.com" />
<button class="pq-button pq-button-secondary" id="pqRelayAddBtn" style="flex: 0 0 auto; width: auto; padding: 12px 20px;">Add Relay</button>
</div>
</div>
<div id="pqPublishStatus"></div>
@@ -549,47 +503,41 @@
<div id="pqOtsStep" class="pq-card pq-hidden">
<div class="pq-card-title">OpenTimestamps Attestation</div>
<div class="pq-info-text">
The NIP-QR event is being timestamped on the Bitcoin blockchain via OpenTimestamps.
This proves the event existed at this point in time, preventing a future quantum attacker
from backdating a fraudulent event. Bitcoin confirmation typically takes 10-30 minutes.
Your event hash has already been submitted to OpenTimestamps in the previous step, and a
<strong>pending</strong> timestamp proof is embedded in the kind 11112 event you just published.
OpenTimestamps anchors that hash into the Bitcoin blockchain, proving your event existed at this
point in time — preventing a future quantum attacker from backdating a fraudulent event.
</div>
<div class="pq-button-row" id="pqOtsSubmitButtonRow">
<button class="pq-button" id="pqOtsStartBtn">Submit to OpenTimestamps</button>
<div class="pq-info-text">
Bitcoin confirmation typically takes <strong>10-30 minutes</strong>. You can leave this page open
to watch the progress below — the polling log updates every minute. Once the Bitcoin attestation is
confirmed, this page will automatically re-sign and republish the kind 11112 event with the upgraded
proof. You're free to close the page; the workflow resumes on your next visit.
</div>
<div id="pqOtsStatus"></div>
<!-- Polling log window -->
<div class="pq-info-text" style="margin-top: 10px; margin-bottom: 5px;">Polling log:</div>
<div id="pqOtsLog" style="background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius); padding: 10px; font-size: 11px; font-family: monospace; height: 120px; overflow-y: auto; text-align: left; margin-bottom: 15px;"></div>
<div id="pqOtsLog" style="background: var(--secondary-color); color: var(--primary-color); border: var(--border); border-radius: var(--border-radius); padding: 10px; font-size: 11px; font-family: monospace; height: 120px; overflow-y: auto; text-align: left; margin-bottom: 15px;"></div>
<div class="pq-key-list">
<div class="pq-key-item" id="pqOtsSubmit">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Submit hash to OpenTimestamps</div>
<div class="pq-key-meta">Creates a complete pending detached .ots proof</div>
</div>
<div class="pq-checklist" style="margin-bottom: 15px;">
<div class="pq-checklist-item pq-done" id="pqOtsSubmit">
<span class="pq-checklist-box"></span>
<span>Submit hash to OpenTimestamps — pending .ots proof created</span>
</div>
<div class="pq-key-item" id="pqOtsPendingPublish">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Pending proof embedded in kind 11112 event</div>
<div class="pq-key-meta">The pending .ots proof is embedded as a tag in the initial event</div>
</div>
<div class="pq-checklist-item pq-done" id="pqOtsPendingPublish">
<span class="pq-checklist-box"></span>
<span>Pending proof embedded in kind 11112 event</span>
</div>
<div class="pq-key-item" id="pqOtsConfirm">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Wait for Bitcoin confirmation</div>
<div class="pq-key-meta" id="pqOtsConfirmDetail">Polling every 60 seconds...</div>
</div>
<div class="pq-checklist-item pq-active" id="pqOtsConfirm">
<span class="pq-checklist-box"></span>
<span>Wait for Bitcoin confirmation
<div class="pq-key-meta" id="pqOtsConfirmDetail" style="margin-top: 2px;">Polling every 60 seconds...</div>
</span>
</div>
<div class="pq-key-item" id="pqOtsConfirmedPublish">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Republish kind 11112 with confirmed proof</div>
<div class="pq-key-meta">Republishes the event with the upgraded Bitcoin-attested proof</div>
</div>
<div class="pq-checklist-item" id="pqOtsConfirmedPublish">
<span class="pq-checklist-box"></span>
<span>Republish kind 11112 with confirmed proof — automatic on confirmation</span>
</div>
</div>
<div id="pqOtsProofWrap" class="pq-hidden" style="margin-top: 15px;">
@@ -766,7 +714,7 @@
console.log('[ots] Found saved OTS workflow from previous session, event:', pendingOts.eventId);
pendingOtsBytes = pendingOts.ots;
pqEvent = { id: pendingOts.eventId, kind: pendingOts.targetKind || NIP_QR_KIND };
if (pendingOts.relayUrls) document.getElementById('pqRelaysInput').value = pendingOts.relayUrls;
if (pendingOts.relayUrls) setRelayUrls(pendingOts.relayUrls);
// F-H3: Build the OTS resume notice with safe DOM construction
const otsNotice = document.createElement('div');
@@ -802,8 +750,16 @@
function setKeyIcon(elementId, icon) {
const el = document.getElementById(elementId);
const iconEl = el.querySelector('.pq-key-icon');
iconEl.textContent = icon;
if (!el) return;
// Supports both the old .pq-key-icon elements and the new checklist
// .pq-checklist-box elements (used in Step 3 and Step 6).
const iconEl = el.querySelector('.pq-key-icon') || el.querySelector('.pq-checklist-box');
if (iconEl) iconEl.textContent = icon;
if (icon && el.classList.contains('pq-checklist-item')) {
el.classList.add('pq-done');
} else if (!icon && el.classList.contains('pq-checklist-item')) {
el.classList.remove('pq-done');
}
}
/* ================================================================
@@ -1077,10 +1033,6 @@
setStatus(pqSignStatus, 'info', ' Building kind 1 announcement event...');
const kind1Template = buildKind1Announcement(currentPubkey, blockHeight, pqKeys);
setKeyIcon('pqSignPqMlDsa44', 'Done');
setKeyIcon('pqSignPqMlDsa65', 'Done');
setKeyIcon('pqSignPqSlhDsa', 'Done');
setKeyIcon('pqSignPqFalcon', 'Done');
setStatus(pqSignStatus, 'info', 'PQ signatures complete. Requesting secp256k1 signature for kind 1 announcement...');
const kind1Signed = await window.nostr.signEvent(kind1Template);
@@ -1124,8 +1076,6 @@
}
const kind11112Signed = await window.nostr.signEvent(kind11112Template);
setKeyIcon('pqSignSecp', 'Done');
setKeyIcon('pqSignSecpWrapper', 'Done');
pqEvent = {
id: kind11112Signed.id,
@@ -1156,7 +1106,7 @@
document.getElementById('pqKind1Preview').textContent = JSON.stringify(kind1Event, null, 2);
pqEventIdDisplay.textContent = `Event ID: ${pqEvent.id}`;
document.getElementById('pqSuccessEventPreview').textContent = eventJsonStr;
setTimeout(() => showView('pqPublishStep'), 1000);
setTimeout(() => { renderRelayList(); showView('pqPublishStep'); }, 1000);
} catch (error) {
console.error('[post-quantum] Sign failed:', error);
@@ -1196,6 +1146,112 @@
}));
}
/* ================================================================
RELAY LIST MANAGEMENT
================================================================ */
const DEFAULT_RELAYS = [
'wss://laantungir.net/relay',
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.primal.net'
];
let pqRelays = DEFAULT_RELAYS.slice();
// Per-relay publish status: { url: { kind1: bool, kind11112: bool } }
const pqRelayStatus = {};
function makeStatusCheckbox(label) {
const wrap = document.createElement('label');
wrap.style.cssText = 'display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted-color); cursor: default; flex: 0 0 auto;';
const box = document.createElement('span');
box.className = 'pq-checklist-box';
box.textContent = '';
box.style.cssText = 'width: 16px; height: 16px; border: 2px solid var(--muted-color); border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: bold;';
const lbl = document.createElement('span');
lbl.textContent = label;
wrap.appendChild(box);
wrap.appendChild(lbl);
return { wrap, box };
}
function setRelayStatus(url, kind, ok) {
if (!pqRelayStatus[url]) pqRelayStatus[url] = { kind1: false, kind11112: false };
pqRelayStatus[url][kind] = ok;
const row = document.querySelector(`.pq-relay-row[data-relay="${CSS.escape(url)}"]`);
if (!row) return;
const box = row.querySelector(`.pq-relay-status-${kind}`);
if (box) {
box.textContent = ok ? '✓' : '';
box.style.background = ok ? 'var(--accent-color)' : 'transparent';
box.style.color = ok ? 'var(--secondary-color)' : 'var(--primary-color)';
box.style.borderColor = ok ? 'var(--accent-color)' : 'var(--muted-color)';
}
}
function renderRelayList() {
const listEl = document.getElementById('pqRelayList');
if (!listEl) return;
listEl.innerHTML = '';
pqRelays.forEach((url, idx) => {
const row = document.createElement('div');
row.className = 'pq-relay-row';
row.dataset.relay = url;
row.style.cssText = 'display: flex; align-items: center; gap: 10px; background: var(--secondary-color); border: var(--border); border-radius: var(--border-radius); padding: 10px 14px;';
const urlSpan = document.createElement('span');
urlSpan.textContent = url;
urlSpan.style.cssText = 'flex: 1; font-size: 14px; color: var(--primary-color); word-break: break-all;';
row.appendChild(urlSpan);
const k1 = makeStatusCheckbox('k1');
k1.box.classList.add('pq-relay-status-kind1');
row.appendChild(k1.wrap);
const k11112 = makeStatusCheckbox('k11112');
k11112.box.classList.add('pq-relay-status-kind11112');
row.appendChild(k11112.wrap);
const removeBtn = document.createElement('button');
removeBtn.className = 'pq-button pq-button-secondary';
removeBtn.textContent = 'Remove';
removeBtn.style.cssText = 'flex: 0 0 auto; width: auto; padding: 8px 16px; font-size: 13px;';
removeBtn.addEventListener('click', () => {
pqRelays.splice(idx, 1);
delete pqRelayStatus[url];
renderRelayList();
});
row.appendChild(removeBtn);
listEl.appendChild(row);
// Restore any existing status
const st = pqRelayStatus[url];
if (st) {
if (st.kind1) setRelayStatus(url, 'kind1', true);
if (st.kind11112) setRelayStatus(url, 'kind11112', true);
}
});
}
function getRelayUrls() {
return pqRelays.slice();
}
function setRelayUrls(urls) {
// Accept either a comma-separated string or an array
if (typeof urls === 'string') {
pqRelays = urls.split(',').map(s => s.trim()).filter(Boolean);
} else if (Array.isArray(urls)) {
pqRelays = urls.slice();
}
if (pqRelays.length === 0) pqRelays = DEFAULT_RELAYS.slice();
renderRelayList();
}
function getRelayUrlsString() {
return pqRelays.join(',');
}
/* ================================================================
EVENT LISTENERS
================================================================ */
@@ -1255,19 +1311,6 @@
}
});
// "Generate New Seed" — generates a fresh seed using pure CSPRNG (no user entropy)
document.getElementById('pqRegenerateBtn').addEventListener('click', () => {
userEntropyChunks = [];
generateAndShowSeed();
// Reset entropy UI
const entropyBar = document.getElementById('pqEntropyBar');
const entropyHint = document.getElementById('pqEntropyHint');
const regenWithEntropyBtn = document.getElementById('pqRegenerateWithEntropyBtn');
if (entropyBar) entropyBar.style.width = '0%';
if (entropyHint) entropyHint.textContent = 'Optional: click here and type random characters, move your mouse to add entropy...';
if (regenWithEntropyBtn) { regenWithEntropyBtn.classList.add('pq-hidden'); regenWithEntropyBtn.disabled = true; }
});
// "Generate with Extra Entropy" — generates a fresh seed mixing CSPRNG + user entropy
document.getElementById('pqRegenerateWithEntropyBtn').addEventListener('click', () => { generateAndShowSeed(); });
@@ -1283,10 +1326,11 @@
setStepActive(5);
publishStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing kind 1 announcement to relays...</div>';
try {
const relayUrls = document.getElementById('pqRelaysInput').value.split(',').map(s => s.trim()).filter(s => s);
const relayUrls = getRelayUrls();
// Publish kind 1 announcement first
const results1 = await publishToRelays(kind1Event, relayUrls);
results1.forEach(r => setRelayStatus(r.relay, 'kind1', r.success));
const success1 = results1.filter(r => r.success).length;
const fail1 = results1.filter(r => !r.success).length;
otsLog(`Kind 1 announcement published: ${success1} ok, ${fail1} failed.`);
@@ -1294,6 +1338,7 @@
// Publish kind 11112 wrapper
publishStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing kind 11112 wrapper to relays...</div>';
const results2 = await publishToRelays(pqEvent, relayUrls);
results2.forEach(r => setRelayStatus(r.relay, 'kind11112', r.success));
const success2 = results2.filter(r => r.success).length;
const fail2 = results2.filter(r => !r.success).length;
otsLog(`Kind 11112 wrapper published: ${success2} ok, ${fail2} failed.`);
@@ -1311,6 +1356,40 @@
document.getElementById('pqDoneBtn').addEventListener('click', () => { showAuthedView(); });
// Add Relay button
document.getElementById('pqRelayAddBtn').addEventListener('click', () => {
const input = document.getElementById('pqRelayAddInput');
const url = input.value.trim();
if (!url) return;
if (!/^wss?:\/\//.test(url)) {
setStatus(document.getElementById('pqPublishStatus'), 'error', 'Relay URL must start with wss:// or ws://');
return;
}
if (pqRelays.includes(url)) {
setStatus(document.getElementById('pqPublishStatus'), 'error', 'That relay is already in the list.');
return;
}
pqRelays.push(url);
input.value = '';
renderRelayList();
document.getElementById('pqPublishStatus').innerHTML = '';
});
// Allow pressing Enter in the add-relay input to add the relay
document.getElementById('pqRelayAddInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); document.getElementById('pqRelayAddBtn').click(); }
});
// Copy Kind 11112 Event button
document.getElementById('pqCopyKind11112Btn').addEventListener('click', () => {
if (!pqEvent) return;
navigator.clipboard.writeText(JSON.stringify(pqEvent, null, 2)).then(() => {
const btn = document.getElementById('pqCopyKind11112Btn');
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy Kind 11112 Event'; }, 2000);
});
});
// Sign out button
const pqSignOutBtn = document.getElementById('pqSignOutBtn');
pqSignOutBtn.addEventListener('click', async () => {
@@ -1334,6 +1413,7 @@
userEntropyChunks = [];
pendingOtsBytes = null;
currentBlockHeight = 0;
pqRelays = DEFAULT_RELAYS.slice();
// Clear auth/session storage, then restore only the OTS workflow record.
try {
localStorage.clear();
@@ -1370,32 +1450,16 @@
}
function prepareOtsStep() {
showView('pqOtsStep');
setStepActive(6);
const otsStatus = document.getElementById('pqOtsStatus');
const otsLogEl = document.getElementById('pqOtsLog');
const startBtn = document.getElementById('pqOtsStartBtn');
if (otsLogEl) otsLogEl.innerHTML = '';
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Review this step, then submit the NIP-QR event hash to OpenTimestamps.</div>';
startBtn.disabled = false;
startBtn.textContent = 'Submit to OpenTimestamps';
document.getElementById('pqOtsSubmitButtonRow').classList.remove('pq-hidden');
setKeyIcon('pqOtsSubmit', '');
setKeyIcon('pqOtsPendingPublish', '');
setKeyIcon('pqOtsConfirm', '');
setKeyIcon('pqOtsConfirmedPublish', '');
otsLog('Waiting for user approval to submit the event hash.');
// The OTS submission already happened in step 4. Auto-start the polling flow.
startOtsFlow();
}
async function startOtsFlow() {
showView('pqOtsStep');
setStepActive(6);
const otsStatus = document.getElementById('pqOtsStatus');
const startBtn = document.getElementById('pqOtsStartBtn');
startBtn.disabled = true;
startBtn.textContent = 'Processing...';
setKeyIcon('pqOtsSubmit', '');
setKeyIcon('pqOtsPendingPublish', '');
const otsLogEl = document.getElementById('pqOtsLog');
if (otsLogEl) otsLogEl.innerHTML = '';
setKeyIcon('pqOtsConfirm', '');
setKeyIcon('pqOtsConfirmedPublish', '');
@@ -1409,7 +1473,6 @@
if (existing && existing.eventId === pqEvent.id) {
pendingOtsBytes = existing.ots;
otsLog(`Loaded saved OTS workflow for event ${pqEvent.id.substring(0, 16)}...`);
setKeyIcon('pqOtsSubmit', 'Done');
if (existing.pendingPublished) setKeyIcon('pqOtsPendingPublish', 'Done');
if (existing.confirmedPublished) {
setKeyIcon('pqOtsConfirm', 'Done');
@@ -1418,13 +1481,11 @@
setStepActive(7);
otsStatus.innerHTML = '<div class="pq-status pq-status-success">Confirmed timestamp proof was already published.</div>';
showOtsProof(pendingOtsBytes);
startBtn.textContent = 'Completed';
return;
}
showOtsProof(pendingOtsBytes);
setKeyIcon('pqOtsPendingPublish', 'Done');
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 11112 event. Waiting for Bitcoin confirmation...</div>';
startBtn.textContent = 'Submitted';
startOtsPolling();
return;
}
@@ -1437,18 +1498,16 @@
pendingOtsBytes = base64ToBytes(otsTag[1]);
if (isDetachedOtsFile(pendingOtsBytes)) {
otsLog(`Found pending OTS proof embedded in event (${pendingOtsBytes.length} bytes).`);
setKeyIcon('pqOtsSubmit', 'Done');
showOtsProof(pendingOtsBytes);
savePendingOts(pqEvent.id, pendingOtsBytes, {
ownerPubkey: currentPubkey,
targetKind: pqEvent.kind,
relayUrls: document.getElementById('pqRelaysInput').value,
relayUrls: getRelayUrlsString(),
pendingPublished: true,
confirmedPublished: false
});
setKeyIcon('pqOtsPendingPublish', 'Done');
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 11112 event. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
startBtn.textContent = 'Submitted';
startOtsPolling();
return;
}
@@ -1463,8 +1522,6 @@
if (!sha256Tag || !sha256Tag[1]) {
otsLog('ERROR: No sha256 tag found in event. Cannot timestamp.');
otsStatus.innerHTML = '<div class="pq-status pq-status-error">Event is missing the sha256 tag.</div>';
startBtn.disabled = false;
startBtn.textContent = 'Retry';
return;
}
const fullHash = sha256Tag[1];
@@ -1475,7 +1532,6 @@
const otsBytes = await timestampEvent(fullHash);
pendingOtsBytes = otsBytes;
otsLog(`Created pending detached .ots file (${otsBytes.length} bytes).`);
setKeyIcon('pqOtsSubmit', 'Done');
showOtsProof(otsBytes);
// Republish the event with the OTS proof embedded
@@ -1485,19 +1541,16 @@
savePendingOts(pqEvent.id, otsBytes, {
ownerPubkey: currentPubkey,
targetKind: pqEvent.kind,
relayUrls: document.getElementById('pqRelaysInput').value,
relayUrls: getRelayUrlsString(),
pendingPublished: true,
confirmedPublished: false
});
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof embedded. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
startBtn.textContent = 'Submitted';
startOtsPolling();
} catch (error) {
console.error('[ots] Submission failed:', error);
otsLog(`ERROR: ${error.message}`);
setStatus(otsStatus, 'error', `OpenTimestamps workflow failed: ${error.message}`);
startBtn.disabled = false;
startBtn.textContent = 'Retry OpenTimestamps Submission';
}
}
@@ -1572,7 +1625,7 @@
try {
otsLog('Building upgraded kind 11112 event with confirmed OTS proof...');
const savedWorkflow = loadPendingOts();
const configuredRelays = document.getElementById('pqRelaysInput').value || (savedWorkflow && savedWorkflow.relayUrls) || 'wss://laantungir.net/relay';
const configuredRelays = getRelayUrlsString() || (savedWorkflow && savedWorkflow.relayUrls) || 'wss://laantungir.net/relay';
// Build the upgraded event: copy all tags from the original, replace the ots tag
const upgradedTemplate = buildUpgradedEvent(pqEvent, upgradedOtsBytes);
@@ -1610,8 +1663,6 @@
}
// OTS event listeners
document.getElementById('pqOtsStartBtn').addEventListener('click', () => { startOtsFlow(); });
document.getElementById('pqOtsDownloadBtn').addEventListener('click', () => {
if (pendingOtsBytes) {
const blob = new Blob([pendingOtsBytes], { type: 'application/octet-stream' });
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.0.2",
"VERSION_NUMBER": "0.0.2",
"BUILD_DATE": "2026-07-17T23:14:56.946Z"
"VERSION": "v0.0.3",
"VERSION_NUMBER": "0.0.3",
"BUILD_DATE": "2026-07-18T21:43:27.753Z"
}