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:
+1
-1
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user