Files
nostr_quantum_preparation/www/index.html
T

1862 lines
82 KiB
HTML

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src wss: https:; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; object-src 'none'; base-uri 'none';" />
<title>Post-Quantum Nostr</title>
<link rel="stylesheet" href="./css/client.css" />
<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;
align-items: center !important;
justify-content: flex-start !important;
align-content: flex-start !important;
}
.pq-container {
max-width: 700px;
width: 100%;
padding: 10px;
}
/* CHECKLIST */
.pq-checklist {
background: var(--secondary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 20px 25px;
margin-bottom: 20px;
}
.pq-checklist-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: var(--primary-color);
}
.pq-checklist-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
font-size: 14px;
color: var(--muted-color);
border-bottom: 1px solid var(--border-color);
}
.pq-checklist-item:last-child { border-bottom: none; }
.pq-checklist-item.pq-done {
color: var(--primary-color);
}
.pq-checklist-box {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 2px solid var(--muted-color);
border-radius: 4px;
flex-shrink: 0;
font-size: 12px;
font-weight: bold;
}
.pq-checklist-item.pq-done .pq-checklist-box {
background: var(--accent-color);
border-color: var(--accent-color);
color: var(--secondary-color);
}
.pq-checklist-item.pq-active {
color: var(--primary-color);
font-weight: bold;
}
.pq-checklist-item.pq-active .pq-checklist-box {
border-color: var(--accent-color);
}
.pq-checklist-link {
color: var(--accent-color);
text-decoration: underline;
}
/* CARDS */
.pq-card {
background: var(--secondary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 25px;
margin-bottom: 20px;
}
.pq-card-title {
font-size: 18px;
font-weight: bold;
color: var(--primary-color);
margin-bottom: 15px;
}
.pq-info-text {
font-size: 14px;
color: var(--primary-color);
line-height: 1.6;
margin-bottom: 15px;
}
.pq-info-text strong { color: var(--primary-color); }
.pq-warning {
background: var(--primary-color);
color: var(--secondary-color);
border-radius: var(--border-radius);
padding: 12px 15px;
margin: 15px 0;
font-size: 13px;
line-height: 1.5;
}
.pq-warning strong { color: var(--accent-color); }
.pq-seed-display {
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 20px;
margin: 15px 0;
font-size: 16px;
line-height: 1.8;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 20px;
word-break: break-word;
}
.pq-seed-word { display: flex; align-items: center; gap: 8px; }
.pq-seed-number { color: var(--muted-color); font-size: 12px; min-width: 20px; }
.pq-button {
background: var(--primary-color);
color: var(--secondary-color);
border: var(--border-width) solid var(--primary-color);
border-radius: var(--border-radius);
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: opacity 0.2s;
width: 100%;
}
.pq-button:hover { opacity: 0.7; }
.pq-button:disabled { opacity: 0.3; cursor: not-allowed; }
.pq-button-secondary {
background: transparent;
color: var(--primary-color);
border: var(--border-width) solid var(--primary-color);
}
.pq-button-row { display: flex; gap: 10px; margin: 10px 0; }
.pq-button-row .pq-button { flex: 1; }
.pq-checkbox-row {
display: flex; align-items: center; gap: 8px;
margin: 15px 0; font-size: 14px; cursor: pointer; color: var(--primary-color);
}
.pq-checkbox-row input {
width: 18px; height: 18px; cursor: pointer;
appearance: none; -webkit-appearance: none;
border: var(--border-width) solid var(--primary-color);
background: transparent;
display: inline-grid;
place-content: center;
box-sizing: border-box;
}
.pq-checkbox-row input::before {
content: "";
width: 100%; height: 100%;
transform: scale(0);
transition: transform 0.1s ease-in-out;
background: #d00;
border-radius: 1px;
}
.pq-checkbox-row input:checked::before {
transform: scale(1);
}
.pq-checkbox-row input:checked {
border-color: #d00;
background: #d00;
}
.pq-seed-toggle {
flex: 1;
opacity: 0.5;
}
.pq-seed-toggle-active {
opacity: 1;
border-color: var(--accent-color);
color: var(--accent-color);
}
.pq-progress-bar {
background: var(--muted-color); border-radius: 4px; height: 8px; margin: 15px 0; overflow: hidden;
}
.pq-progress-fill { background: var(--accent-color); height: 100%; width: 0%; transition: width 0.3s; }
.pq-key-list { display: flex; flex-direction: column; gap: 10px; margin: 15px 0; }
.pq-key-item {
display: flex; align-items: flex-start; gap: 12px; padding: 12px;
background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius);
}
.pq-key-icon { font-size: 20px; flex-shrink: 0; }
.pq-key-details { flex: 1; min-width: 0; }
.pq-key-name { font-size: 14px; font-weight: bold; margin-bottom: 2px; }
.pq-key-meta { font-size: 12px; color: var(--primary-color); margin-bottom: 4px; }
.pq-key-pubkey { font-size: 11px; color: var(--primary-color); word-break: break-all; }
.pq-status {
padding: 10px 15px; border-radius: var(--border-radius); margin: 10px 0; font-size: 14px; text-align: left;
}
.pq-status-info { background: var(--secondary-color); border: var(--border); }
.pq-status-success { background: var(--accent-color); color: var(--secondary-color); }
.pq-status-error { background: var(--accent-color); color: var(--secondary-color); }
.pq-event-preview {
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;
}
.pq-npub-display {
font-size: 12px; word-break: break-all;
background: var(--primary-color); color: var(--secondary-color);
padding: 8px 12px; border-radius: var(--border-radius); margin: 10px 0;
}
.pq-relay-input {
width: 100%; background: var(--secondary-color); border: var(--border);
border-radius: var(--border-radius); padding: 10px 14px; font-size: 14px;
color: var(--primary-color); margin: 8px 0;
}
.pq-relay-input:focus { outline: none; border-color: var(--accent-color); }
.pq-hidden { display: none !important; }
ol { margin: 10px 0 10px 20px; line-height: 1.8; }
</style>
</head>
<body>
<div id="divHeader">
<div id="divHeaderFlexLeft"></div>
<div id="divHeaderFlexCenter">
<div class="divHeaderText" id="divHeaderText">Post-Quantum Nostr</div>
</div>
<div id="divHeaderFlexRight">
<button class="pq-button pq-button-secondary pq-hidden" id="pqSignOutBtn" style="width: auto; padding: 6px 16px; font-size: 13px;">Sign Out</button>
</div>
</div>
<div id="divBody">
<div class="pq-container">
<!-- ================================================================
PERSISTENT CHECKLIST (always visible at top)
================================================================ -->
<div class="pq-checklist">
<div class="pq-checklist-title">Migration Steps</div>
<div class="pq-checklist-item" id="step-1">
<span class="pq-checklist-box"></span>
<span>1. Sign in with your Nostr identity</span>
</div>
<div class="pq-checklist-item" id="step-2">
<span class="pq-checklist-box"></span>
<span>2. Generate a seed phrase</span>
</div>
<div class="pq-checklist-item" id="step-3">
<span class="pq-checklist-box"></span>
<span>3. Derive post-quantum keys from seed</span>
</div>
<div class="pq-checklist-item" id="step-4">
<span class="pq-checklist-box"></span>
<span>4. Sign the NIP-QR migration event</span>
</div>
<div class="pq-checklist-item" id="step-5">
<span class="pq-checklist-box"></span>
<span>5. Publish events to relays</span>
</div>
<div class="pq-checklist-item" id="step-6">
<span class="pq-checklist-box"></span>
<span>6. Timestamp the event with OpenTimestamps</span>
</div>
<div class="pq-checklist-item" id="step-7">
<span class="pq-checklist-box"></span>
<span>7. <a href="./verify.html" class="pq-checklist-link">Verify the event signatures</a></span>
</div>
</div>
<!-- ================================================================
STEP CONTENT (shown/hidden below checklist)
================================================================ -->
<!-- STEP 0: NOT AUTHENTICATED -->
<div id="pqNotAuthed" class="pq-card">
<div class="pq-card-title">Post-Quantum Nostr</div>
<div class="pq-info-text" style="font-size: 16px; color: var(--primary-color);">
Create an experimental pre-quantum PQ key commitment
</div>
<div class="pq-info-text">
Sign in with your Nostr signer to begin. Your npub won't change, your followers stay,
and your social graph is preserved. We'll generate a seed phrase and link
it to your current identity.
</div>
<div class="pq-info-text" style="font-size: 13px; color: var(--accent-color);">
<strong>Research prototype.</strong> Do not enter a valuable existing mnemonic. This tool
does not make your Nostr identity post-quantum secure on its own; it creates a commitment
that future PQ protocols can build on.
</div>
<div class="pq-info-text" id="noSignerWarning" style="color: var(--accent-color); display: none;">
No Nostr signer detected. Install a NIP-07 browser extension (like nos2x or Alby) and reload.
</div>
<div id="pqSignInError"></div>
<button class="pq-button" id="pqSignInBtn">Sign In to Continue</button>
</div>
<!-- STEP 1: AUTHENTICATED - INTRO -->
<div id="pqAuthed" class="pq-card pq-hidden">
<div class="pq-card-title">Post-Quantum Migration</div>
<div class="pq-info-text">
<strong>Connected as:</strong>
<div class="pq-npub-display" id="pqNpub"></div>
</div>
<div class="pq-info-text">
<strong>Current Bitcoin block height:</strong>
<div class="pq-npub-display" id="pqBlockHeight">Loading...</div>
</div>
<div class="pq-info-text">
This tool will:
<ol>
<li>Generate a new <strong>seed phrase</strong> (your recovery backup)</li>
<li>Derive <strong>post-quantum keys</strong> from that seed via BIP32 paths (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512, ML-KEM-768)</li>
<li>Sign a <strong>NIP-QR migration event</strong> linking your identity to the PQ keys</li>
<li>Publish the event to relays</li>
</ol>
</div>
<button class="pq-button" id="pqStartBtn">Start Migration</button>
</div>
<!-- STEP 2: SEED PHRASE GENERATION -->
<div id="pqSeedStep" class="pq-card pq-hidden">
<div class="pq-card-title">Your Seed Phrase</div>
<div class="pq-info-text">
Your seed phrase is your <strong>recovery root</strong>. All post-quantum keys
are derived from it. It is processed entirely in your browser and never sent to any server.
The code is open source and auditable.
</div>
<div class="pq-info-text" style="font-size: 13px; color: var(--accent-color);">
<strong>Back this up offline.</strong> Anyone with this phrase can derive your PQ private keys.
24 words (256 bits) is recommended; 12 words (128 bits) is offered for testing only and is
weaker under a quantum brute-force model.
</div>
<!-- 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">
<!-- Word-count selector -->
<div class="pq-info-text" style="margin-bottom: 10px; font-size: 13px;">
<label style="margin-right: 12px;"><input type="radio" name="pqWordCount" value="256" id="pqWordCount24" checked> 24 words (recommended)</label>
<label><input type="radio" name="pqWordCount" value="128" id="pqWordCount12"> 12 words (testing only)</label>
</div>
<div class="pq-seed-display" id="pqSeedDisplay"></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(--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(--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>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary pq-hidden" id="pqRegenerateWithEntropyBtn" disabled>Generate with Extra Entropy</button>
</div>
<label class="pq-checkbox-row">
<input type="checkbox" id="pqSeedConfirmed" />
I have written down my seed phrase
</label>
<button class="pq-button" id="pqSeedContinueBtn" disabled>Continue</button>
</div>
<!-- Bring your own mode -->
<div id="pqSeedOwnPanel" style="display: none;">
<div class="pq-info-text">
Paste your 12 or 24-word BIP39 seed phrase below. It will be validated and used to
derive your post-quantum keys. Make sure you generated it offline or with a trusted tool.
</div>
<textarea class="pq-textarea" id="pqSeedInput" style="min-height: 80px; width: 100%; background: var(--secondary-color); border: var(--border); border-radius: var(--border-radius); padding: 12px; font-size: 14px; color: var(--primary-color); font-family: monospace; margin: 10px 0;" placeholder="abandon ability able about above absent absorb abstract absurd abuse access accident..."></textarea>
<div id="pqSeedValidation" style="font-size: 13px; margin: 5px 0;"></div>
<button class="pq-button" id="pqSeedOwnContinueBtn" disabled>Continue</button>
</div>
</div>
<!-- STEP 3: DERIVING PQ KEYS -->
<div id="pqDeriveStep" class="pq-card pq-hidden">
<div class="pq-card-title">Deriving Post-Quantum Keys</div>
<div class="pq-info-text">
Deriving post-quantum keys from your seed phrase via BIP32 paths.
All keys are generated deterministically — the same seed will always produce the same keys.
</div>
<div class="pq-progress-bar">
<div class="pq-progress-fill" id="pqDeriveProgress"></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-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-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-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>
</span>
</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>
<button class="pq-button pq-hidden" id="pqDeriveContinueBtn">Continue to Signing</button>
</div>
<!-- STEP 4: SIGNING -->
<div id="pqSignStep" class="pq-card pq-hidden">
<div class="pq-card-title">Sign Migration Event</div>
<div class="pq-info-text">
We'll now create two events: a kind 1 announcement (public, visible in Nostr feeds) and
a kind 9999 proof carrier (carries the OpenTimestamps proof). Each PQ key signs the attestation
statement, and your signer signs both events with your secp256k1 key. The SHA-256 of the
full kind 1 event (including its secp256k1 signature) is submitted to OpenTimestamps.
</div>
<div id="pqSignStatus"></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>
<div class="pq-event-preview" id="pqEventPreview"></div>
</div>
</div>
<!-- STEP 5: REVIEW & PUBLISH -->
<div id="pqPublishStep" class="pq-card pq-hidden">
<div class="pq-card-title">Review & Publish Events</div>
<div class="pq-info-text">
Two events have been signed: a kind 1 announcement (public, visible in Nostr feeds) and
a kind 9999 proof carrier (carries the OpenTimestamps proof). Review them below, then publish
both to relays when ready.
</div>
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Kind 1 announcement event:</strong></div>
<div id="pqKind1IdDisplay" style="margin: 5px 0; font-size: 11px; color: var(--primary-color); word-break: break-all;"></div>
<div class="pq-event-preview" id="pqKind1Preview" style="max-height: 200px;"></div>
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;"><strong>Kind 9999 proof carrier 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="pqCopyProofCarrierBtn">Copy Proof Carrier Event</button>
</div>
<div style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Relays to publish to:</strong></div>
<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>
<div class="pq-button-row" style="margin-top: 15px;">
<button class="pq-button" id="pqPublishBtn">Publish Both Events to Relays</button>
</div>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary pq-hidden" id="pqOtsContinueBtn">Continue to OpenTimestamps</button>
</div>
</div>
<!-- STEP 6: OPENTIMESTAMPS -->
<div id="pqOtsStep" class="pq-card pq-hidden">
<div class="pq-card-title">OpenTimestamps Attestation - Leave This Page Open</div>
<div class="pq-info-text">
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 9999 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-info-text">
Bitcoin confirmation typically takes <strong>10-30 minutes</strong>. You can
<strong style="text-decoration: underline;">leave this page open</strong>
to watch the progress below — the polling log updates every minute. Once the Bitcoin attestation is
confirmed, this page will automatically sign and publish a <strong>new</strong> kind 9999 event carrying
the upgraded (confirmed) proof, referencing the original via an <code>upgrade_of</code> tag. The original
event remains on relays permanently. 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(--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-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-checklist-item pq-done" id="pqOtsPendingPublish">
<span class="pq-checklist-box"></span>
<span>Pending proof embedded in kind 9999 event</span>
</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-checklist-item" id="pqOtsConfirmedPublish">
<span class="pq-checklist-box"></span>
<span>Publish new kind 9999 with confirmed proof — automatic on confirmation</span>
</div>
</div>
<div id="pqOtsProofWrap" class="pq-hidden" style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;">OTS proof file (base64):</div>
<div class="pq-event-preview" id="pqOtsProof" style="max-height: 150px;"></div>
<div class="pq-button-row" style="margin-top: 10px;">
<button class="pq-button pq-button-secondary" id="pqOtsDownloadBtn">Download .ots file</button>
</div>
</div>
</div>
</div>
</div>
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox"></div>
<div id="divFooterRight" class="divFooterBox"></div>
</div>
<!-- SCRIPTS -->
<script src="/nostr-login-lite/nostr.bundle.js"></script>
<script src="/nostr-login-lite/nostr-lite.js"></script>
<!-- Version display: fetch version.json and update title + header -->
<script>
fetch('./js/version.json')
.then(r => r.json())
.then(v => {
const versionText = v.VERSION || '';
document.title = 'Post-Quantum Nostr ' + versionText;
const header = document.getElementById('divHeaderText');
if (header) header.textContent = 'Post-Quantum Nostr ' + versionText;
})
.catch(() => { /* version.json not found — keep default title */ });
</script>
<script type="module">
import {
generateSeedPhrase,
generateSeedPhraseWithEntropy,
mnemonicToSeed,
isValidMnemonic,
deriveSecp256k1FromSeed,
derivePQKeysFromSeed,
buildKind1Announcement,
buildProofCarrier,
computeEventId,
hashFullEvent,
verifyNIPQRContent,
verifyNostrEvent,
bytesToBase64,
base64ToBytes,
bytesToHex,
PQ_KEY_INFO,
NIP_QR_KIND,
buildUpgradedEvent,
selectCanonicalProofCarrier,
timestampEvent,
upgradeOts,
isOtsConfirmed,
verifyOtsProof,
savePendingOts,
loadPendingOts,
clearPendingOts,
isDetachedOtsFile
} from './pq-crypto.bundle.js';
/* ================================================================
GLOBAL STATE
================================================================ */
let currentPubkey = null;
let currentBlockHeight = 0;
let pqMnemonic = null;
let pqSeed = null;
let pqKeys = null;
let pqSecpKeys = null;
let pqEvent = null; // kind 9999 proof carrier event
let kind1Event = null; // kind 1 announcement event
/* ================================================================
DOM REFERENCES
================================================================ */
const pqNotAuthed = document.getElementById('pqNotAuthed');
const pqAuthed = document.getElementById('pqAuthed');
const pqNpub = document.getElementById('pqNpub');
const pqSeedStep = document.getElementById('pqSeedStep');
const pqSeedDisplay = document.getElementById('pqSeedDisplay');
const pqSeedConfirmed = document.getElementById('pqSeedConfirmed');
const pqSeedContinueBtn = document.getElementById('pqSeedContinueBtn');
const pqDeriveStep = document.getElementById('pqDeriveStep');
const pqDeriveProgress = document.getElementById('pqDeriveProgress');
const pqDeriveStatus = document.getElementById('pqDeriveStatus');
const pqDeriveContinueBtn = document.getElementById('pqDeriveContinueBtn');
const pqSignStep = document.getElementById('pqSignStep');
const pqSignStatus = document.getElementById('pqSignStatus');
const pqSignBtn = document.getElementById('pqSignBtn');
const pqEventPreviewWrap = document.getElementById('pqEventPreviewWrap');
const pqEventPreview = document.getElementById('pqEventPreview');
const pqPublishStep = document.getElementById('pqPublishStep');
const pqEventIdDisplay = document.getElementById('pqEventIdDisplay');
const noSignerWarning = document.getElementById('noSignerWarning');
const pqSignInError = document.getElementById('pqSignInError');
/* ================================================================
CHECKLIST MANAGEMENT
================================================================ */
const stepElements = {
1: document.getElementById('step-1'),
2: document.getElementById('step-2'),
3: document.getElementById('step-3'),
4: document.getElementById('step-4'),
5: document.getElementById('step-5'),
6: document.getElementById('step-6'),
7: document.getElementById('step-7')
};
function setStepDone(stepNum) {
const el = stepElements[stepNum];
if (el) {
el.classList.add('pq-done');
el.classList.remove('pq-active');
el.querySelector('.pq-checklist-box').textContent = '';
}
}
function setStepActive(stepNum) {
const el = stepElements[stepNum];
if (el) {
el.classList.add('pq-active');
el.classList.remove('pq-done');
}
}
function setStepPending(stepNum) {
const el = stepElements[stepNum];
if (el) {
el.classList.remove('pq-active', 'pq-done');
el.querySelector('.pq-checklist-box').textContent = '';
}
}
/* ================================================================
VIEW MANAGEMENT
================================================================ */
const pqOtsStep = document.getElementById('pqOtsStep');
function showView(elementId) {
[pqNotAuthed, pqAuthed, pqSeedStep, pqDeriveStep, pqSignStep, pqPublishStep, pqOtsStep].forEach(el => {
el.classList.add('pq-hidden');
});
const el = document.getElementById(elementId);
if (el) el.classList.remove('pq-hidden');
}
async function showAuthedView() {
pqNpub.textContent = currentPubkey;
showView('pqAuthed');
setStepDone(1);
setStepActive(2);
// Show sign out button
document.getElementById('pqSignOutBtn').classList.remove('pq-hidden');
const blockHeightEl = document.getElementById('pqBlockHeight');
blockHeightEl.textContent = 'Loading...';
const height = await fetchBlockHeight();
blockHeightEl.textContent = height > 0 ? String(height) : 'Unavailable';
// Offer a persisted OTS workflow only to the same signed-in identity.
const pendingOts = loadPendingOts();
document.querySelectorAll('.pq-ots-resume-notice').forEach(el => el.remove());
if (pendingOts && pendingOts.ownerPubkey && pendingOts.ownerPubkey === currentPubkey) {
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) setRelayUrls(pendingOts.relayUrls);
// F-H3: Build the OTS resume notice with safe DOM construction
const otsNotice = document.createElement('div');
otsNotice.className = 'pq-status pq-status-info pq-ots-resume-notice';
otsNotice.style.marginTop = '15px';
const stateLabel = pendingOts.confirmedPublished ? 'completed' : (pendingOts.pendingPublished ? 'pending Bitcoin confirmation' : 'not yet published');
otsNotice.textContent = `You have an OpenTimestamps workflow from a previous session (${stateLabel}; event: ${pendingOts.eventId.substring(0, 16)}...). `;
const resumeLink = document.createElement('a');
resumeLink.href = '#';
resumeLink.id = 'pqResumeOtsLink';
resumeLink.style.color = 'var(--accent-color)';
resumeLink.style.cursor = 'pointer';
resumeLink.textContent = 'View / resume timestamping';
otsNotice.appendChild(resumeLink);
pqAuthed.appendChild(otsNotice);
document.getElementById('pqResumeOtsLink').addEventListener('click', async (e) => {
e.preventDefault();
for (let i = 1; i <= 5; i++) setStepDone(i);
setStepActive(6);
// G56-02: Fetch ALL proof carrier events from relays and select
// the canonical one (earliest valid Bitcoin anchor)
if (!pqEvent.tags || !pqEvent.content) {
otsLog('Resuming OTS workflow: fetching proof carrier events from relays...');
const relayUrls = getRelayUrls();
let allCandidates = [];
await Promise.all(relayUrls.map(async (url) => {
try {
const events = await queryRelayForProofCarriers(currentPubkey, url);
if (events && events.length > 0) allCandidates.push(...events);
} catch (e) { /* ignore */ }
}));
// Deduplicate by event ID
const seenIds = new Set();
allCandidates = allCandidates.filter(e => {
if (!e || !e.id || seenIds.has(e.id)) return false;
seenIds.add(e.id);
return true;
});
if (allCandidates.length > 0) {
// Try to find the one matching our persisted event ID
const matching = allCandidates.find(e => e.id === pqEvent.id);
if (matching) {
pqEvent = matching;
otsLog(`Resuming OTS workflow: fetched event ${pqEvent.id.substring(0, 16)}... from relays (${allCandidates.length} total candidates).`);
} else {
// Use the first candidate as fallback
pqEvent = allCandidates[0];
otsLog(`Resuming OTS workflow: persisted event not found, using ${pqEvent.id.substring(0, 16)}... (${allCandidates.length} total candidates).`);
}
} else {
otsLog('WARNING: Could not fetch any proof carrier events from relays. Republishing may fail.');
}
}
await startOtsFlow();
});
}
}
// F-H3: Build status DOM safely — type is internally controlled, message uses textContent
function setStatus(element, type, message) {
const div = document.createElement('div');
div.className = `pq-status pq-status-${type}`;
div.textContent = message;
element.innerHTML = '';
element.appendChild(div);
}
function setKeyIcon(elementId, icon) {
const el = document.getElementById(elementId);
if (!el) return;
// For checklist items (Step 3, Step 6), toggle the pq-done class to
// show a solid red box — no text inside the box, matching the
// Migration Steps checklist style.
if (el.classList.contains('pq-checklist-item')) {
const box = el.querySelector('.pq-checklist-box');
if (box) box.textContent = '';
if (icon) {
el.classList.add('pq-done');
el.classList.remove('pq-active');
} else {
el.classList.remove('pq-done');
}
return;
}
// Legacy .pq-key-icon elements (no longer used but kept for safety)
const iconEl = el.querySelector('.pq-key-icon');
if (iconEl) iconEl.textContent = icon;
}
/* ================================================================
BLOCK HEIGHT
================================================================ */
async function fetchBlockHeight() {
try {
const response = await fetch('https://mempool.space/api/blocks/tip/height');
const height = await response.text();
currentBlockHeight = parseInt(height, 10);
return currentBlockHeight;
} catch (e) {
console.warn('[post-quantum] Could not fetch block height:', e.message);
return 0;
}
}
/* ================================================================
AUTHENTICATION
================================================================ */
async function initNostrLoginLite() {
if (!window.NOSTR_LOGIN_LITE) return false;
if (!window.NOSTR_LOGIN_LITE._initialized) {
await window.NOSTR_LOGIN_LITE.init({
methods: { extension: true, local: true, nip46: true, seedphrase: true, nsigner: true }
});
}
return true;
}
async function signIn() {
if (window.NOSTR_LOGIN_LITE) {
await initNostrLoginLite();
if (window.nostr) {
try {
currentPubkey = await window.nostr.getPublicKey();
if (currentPubkey) {
console.log('[post-quantum] Authenticated (restored) as:', currentPubkey);
await showAuthedView();
return;
}
} catch (e) {
console.log('[post-quantum] Not yet authenticated, launching login modal...');
}
}
currentPubkey = await new Promise((resolve, reject) => {
let settled = false;
const timeout = setTimeout(() => {
if (!settled) { settled = true; cleanup(); reject(new Error('Login timed out')); }
}, 120000);
async function tryGetPubkey() {
if (settled) return;
if (window.nostr) {
try {
const pk = await window.nostr.getPublicKey();
if (pk && !settled) { settled = true; cleanup(); resolve(pk); }
} catch (e) { console.log('[post-quantum] getPublicKey not ready:', e.message); }
}
}
function authHandler(e) {
console.log('[post-quantum] Auth event:', e.type, e.detail ? JSON.stringify(e.detail).substring(0, 100) : '');
// nlMethodSelected includes the pubkey directly in the detail
if (e.type === 'nlMethodSelected' && e.detail && e.detail.pubkey) {
if (!settled) { settled = true; cleanup(); resolve(e.detail.pubkey); }
return;
}
// For other events, try getPublicKey with delays
tryGetPubkey();
setTimeout(tryGetPubkey, 200);
setTimeout(tryGetPubkey, 500);
setTimeout(tryGetPubkey, 1000);
setTimeout(tryGetPubkey, 2000);
}
function cleanup() {
window.removeEventListener('nlAuth', authHandler);
window.removeEventListener('nlAuthRestored', authHandler);
window.removeEventListener('nlMethodSelected', authHandler);
window.removeEventListener('nlAuthComplete', authHandler);
clearTimeout(timeout);
}
window.addEventListener('nlAuth', authHandler);
window.addEventListener('nlAuthRestored', authHandler);
window.addEventListener('nlMethodSelected', authHandler);
window.addEventListener('nlAuthComplete', authHandler);
// launch() may return undefined (not a Promise) after sign out
try {
const result = window.NOSTR_LOGIN_LITE.launch();
if (result && typeof result.then === 'function') {
result.then(() => {
tryGetPubkey(); setTimeout(tryGetPubkey, 200); setTimeout(tryGetPubkey, 500);
}).catch(e => { if (!settled) { settled = true; cleanup(); reject(e); } });
} else {
// launch() returned undefined — just wait for events
console.log('[post-quantum] launch() returned non-Promise, waiting for auth events...');
}
} catch (e) {
if (!settled) { settled = true; cleanup(); reject(e); }
}
});
console.log('[post-quantum] Authenticated as:', currentPubkey);
await showAuthedView();
return;
}
if (!window.nostr) {
throw new Error('No Nostr signer found. Install a NIP-07 browser extension (nos2x, Alby) or use nostr-login-lite.');
}
currentPubkey = await window.nostr.getPublicKey();
console.log('[post-quantum] Authenticated (extension) as:', currentPubkey);
await showAuthedView();
}
/* ================================================================
STEP 2: SEED PHRASE
================================================================ */
let userEntropyChunks = [];
let entropyCollecting = false;
function getSelectedEntropyBits() {
const radio = document.querySelector('input[name="pqWordCount"]:checked');
return radio ? parseInt(radio.value, 10) : 256;
}
function generateAndShowSeed() {
const entropyBits = getSelectedEntropyBits();
// Use user entropy if available, otherwise pure CSPRNG
if (userEntropyChunks.length > 0) {
const userEntropy = new TextEncoder().encode(userEntropyChunks.join(''));
pqMnemonic = generateSeedPhraseWithEntropy(userEntropy, entropyBits);
otsLog(`Seed generated with ${userEntropyChunks.length} entropy chunks from user input (${entropyBits}-bit).`);
} else {
pqMnemonic = generateSeedPhrase(entropyBits);
otsLog(`Seed generated (${entropyBits}-bit).`);
}
const words = pqMnemonic.split(' ');
pqSeedDisplay.innerHTML = words.map((word, i) =>
`<div class="pq-seed-word"><span class="pq-seed-number">${i + 1}.</span>${word}</div>`
).join('');
pqSeedConfirmed.checked = false;
pqSeedContinueBtn.disabled = true;
}
// Entropy collection from mouse and keyboard (optional)
function startEntropyCollection() {
const entropyBox = document.getElementById('pqEntropyBox');
const entropyBar = document.getElementById('pqEntropyBar');
const entropyHint = document.getElementById('pqEntropyHint');
const regenWithEntropyBtn = document.getElementById('pqRegenerateWithEntropyBtn');
const maxChunks = 64; // collect up to 64 events
let collected = 0;
function updateBar() {
const pct = Math.min(100, Math.round((collected / maxChunks) * 100));
entropyBar.style.width = pct + '%';
if (collected >= maxChunks) {
entropyHint.textContent = 'Enough entropy collected. Click "Generate with Extra Entropy" to use it.';
} else if (collected > 0) {
entropyHint.textContent = `Collected ${collected}/${maxChunks} entropy samples. Keep going, or click "Generate with Extra Entropy" to use what you have.`;
}
// Show the "Generate with Extra Entropy" button once we have at least 1 chunk
if (collected > 0 && regenWithEntropyBtn) {
regenWithEntropyBtn.classList.remove('pq-hidden');
regenWithEntropyBtn.disabled = false;
}
}
function collectEvent(data) {
if (collected >= maxChunks) return;
userEntropyChunks.push(data + ':' + Date.now() + ':' + Math.random());
collected++;
updateBar();
}
entropyBox.addEventListener('mousemove', (e) => {
collectEvent(`m${e.clientX},${e.clientY}`);
});
entropyBox.addEventListener('keypress', (e) => {
collectEvent(`k${e.key}:${e.keyCode}`);
});
entropyBox.addEventListener('keydown', (e) => {
// Also capture non-printable keys
if (e.key.length > 1) collectEvent(`k${e.key}:${e.keyCode}`);
});
}
// Seed mode toggle
function showSeedGeneratePanel() {
document.getElementById('pqSeedGeneratePanel').style.display = 'block';
document.getElementById('pqSeedOwnPanel').style.display = 'none';
document.getElementById('pqSeedToggleGenerate').classList.add('pq-seed-toggle-active');
document.getElementById('pqSeedToggleOwn').classList.remove('pq-seed-toggle-active');
// Generate a fresh seed when switching to the generate panel
userEntropyChunks = [];
generateAndShowSeed();
// Reset the 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; }
}
function showSeedOwnPanel() {
document.getElementById('pqSeedGeneratePanel').style.display = 'none';
document.getElementById('pqSeedOwnPanel').style.display = 'block';
document.getElementById('pqSeedToggleGenerate').classList.remove('pq-seed-toggle-active');
document.getElementById('pqSeedToggleOwn').classList.add('pq-seed-toggle-active');
}
/* ================================================================
STEP 3: DERIVE PQ KEYS
================================================================ */
async function derivePQKeys() {
showView('pqDeriveStep');
setStepActive(3);
pqDeriveProgress.style.width = '0%';
pqDeriveContinueBtn.classList.add('pq-hidden');
try {
pqSeed = mnemonicToSeed(pqMnemonic);
pqDeriveProgress.style.width = '15%';
pqSecpKeys = deriveSecp256k1FromSeed(pqSeed);
pqDeriveProgress.style.width = '30%';
await new Promise(r => setTimeout(r, 100));
pqKeys = derivePQKeysFromSeed(pqSeed);
pqDeriveProgress.style.width = '60%';
setKeyIcon('pqKeyMlDsa44', 'Done');
document.getElementById('pqKeyMlDsa44Pub').textContent = bytesToBase64(pqKeys.mlDsa44.publicKey).substring(0, 60) + '...';
await new Promise(r => setTimeout(r, 100));
setKeyIcon('pqKeyMlDsa65', 'Done');
document.getElementById('pqKeyMlDsa65Pub').textContent = bytesToBase64(pqKeys.mlDsa65.publicKey).substring(0, 60) + '...';
await new Promise(r => setTimeout(r, 100));
setKeyIcon('pqKeySlhDsa', 'Done');
document.getElementById('pqKeySlhDsaPub').textContent = bytesToBase64(pqKeys.slhDsa.publicKey).substring(0, 60) + '...';
await new Promise(r => setTimeout(r, 100));
setKeyIcon('pqKeyFalcon', 'Done');
document.getElementById('pqKeyFalconPub').textContent = bytesToBase64(pqKeys.falcon512.publicKey).substring(0, 60) + '...';
await new Promise(r => setTimeout(r, 100));
setKeyIcon('pqKeyMlKem', 'Done');
document.getElementById('pqKeyMlKemPub').textContent = bytesToBase64(pqKeys.mlKem.publicKey).substring(0, 60) + '...';
pqDeriveProgress.style.width = '100%';
setStatus(pqDeriveStatus, 'success', 'All 5 post-quantum keys derived successfully!');
pqDeriveContinueBtn.classList.remove('pq-hidden');
setStepDone(3);
} catch (error) {
console.error('[post-quantum] Key derivation failed:', error);
setStatus(pqDeriveStatus, 'error', `Key derivation failed: ${error.message}`);
}
}
/* ================================================================
STEP 4: SIGN EVENT
================================================================ */
async function signEvent() {
pqSignBtn.disabled = true;
setStepActive(4);
setStatus(pqSignStatus, 'info', ' Refreshing Bitcoin block height...');
try {
const blockHeight = await fetchBlockHeight();
console.log('[post-quantum] Block height for statement:', blockHeight);
const blockHeightEl = document.getElementById('pqBlockHeight');
if (blockHeightEl && blockHeight > 0) blockHeightEl.textContent = String(blockHeight);
if (!window.nostr || !window.nostr.signEvent) {
throw new Error('Nostr signer not available (window.nostr.signEvent missing)');
}
// ---- Phase 1: Build and sign the kind 1 announcement event ----
setStatus(pqSignStatus, 'info', ' Building kind 1 announcement event...');
const kind1Template = buildKind1Announcement(currentPubkey, blockHeight, pqKeys);
setStatus(pqSignStatus, 'info', 'PQ signatures complete. Requesting secp256k1 signature for kind 1 announcement...');
const kind1Signed = await window.nostr.signEvent(kind1Template);
// Ensure the signed event has all fields
kind1Event = {
id: kind1Signed.id,
pubkey: kind1Signed.pubkey,
created_at: kind1Template.created_at,
kind: 1,
content: kind1Template.content,
tags: kind1Template.tags,
sig: kind1Signed.sig
};
otsLog(`Kind 1 announcement signed. Event ID: ${kind1Event.id.substring(0, 32)}...`);
// ---- Phase 2: Timestamp the full kind 1 event hash ----
const fullHash = hashFullEvent(kind1Event);
otsLog(`SHA-256 of full kind 1 event: ${fullHash.substring(0, 32)}...`);
let otsProof = null;
try {
setStatus(pqSignStatus, 'info', ' Submitting kind 1 event hash to OpenTimestamps...');
otsProof = await timestampEvent(fullHash);
otsLog(`Pending OTS proof created (${otsProof.length} bytes) for kind 1 event hash.`);
} catch (otsError) {
console.warn('[post-quantum] OTS submission failed, continuing without proof:', otsError.message);
otsLog(`WARNING: OTS submission failed: ${otsError.message}. Kind 9999 event will not have OTS proof yet.`);
}
// ---- Phase 3: Build and sign the proof carrier event (kind 9999) ----
setStatus(pqSignStatus, 'info', ' Requesting secp256k1 signature for proof carrier...');
let proofCarrierTemplate;
if (otsProof && otsProof.length > 0) {
proofCarrierTemplate = buildProofCarrier(kind1Event, otsProof, { otsStatus: 'pending' });
} else {
// No OTS proof — build carrier without ots/ots_status tags (will be added later)
proofCarrierTemplate = buildProofCarrier(kind1Event, new Uint8Array(0), { otsStatus: 'pending' });
// Remove the empty ots tag
proofCarrierTemplate.tags = proofCarrierTemplate.tags.filter(t => t[0] !== 'ots');
}
const proofCarrierSigned = await window.nostr.signEvent(proofCarrierTemplate);
pqEvent = {
id: proofCarrierSigned.id,
pubkey: proofCarrierSigned.pubkey,
created_at: proofCarrierTemplate.created_at,
kind: NIP_QR_KIND,
content: proofCarrierTemplate.content,
tags: proofCarrierTemplate.tags,
sig: proofCarrierSigned.sig
};
otsLog(`Proof carrier signed. Event ID: ${pqEvent.id.substring(0, 32)}...`);
// Save the pending OTS proof for the OTS step
if (otsProof && otsProof.length > 0) {
pendingOtsBytes = otsProof;
}
const eventJsonStr = JSON.stringify(pqEvent, null, 2);
pqEventPreview.textContent = eventJsonStr;
pqEventPreviewWrap.classList.remove('pq-hidden');
setStatus(pqSignStatus, 'success', 'Both events signed successfully!');
setStepDone(4);
setStepActive(5);
// Show publish step
document.getElementById('pqKind1IdDisplay').textContent = `Event ID: ${kind1Event.id}`;
document.getElementById('pqKind1Preview').textContent = JSON.stringify(kind1Event, null, 2);
pqEventIdDisplay.textContent = `Event ID: ${pqEvent.id}`;
document.getElementById('pqSuccessEventPreview').textContent = eventJsonStr;
setTimeout(() => { renderRelayList(); showView('pqPublishStep'); }, 1000);
} catch (error) {
console.error('[post-quantum] Sign failed:', error);
setStatus(pqSignStatus, 'error', `Failed: ${error.message}`);
pqSignBtn.disabled = false;
}
}
/* ================================================================
STEP 5: PUBLISH TO RELAYS
================================================================ */
function publishToRelays(event, relayUrls) {
const eventJson = JSON.stringify(['EVENT', event]);
return Promise.all(relayUrls.map(relayUrl => {
return new Promise((resolve) => {
try {
const ws = new WebSocket(relayUrl);
let resolved = false;
const timeout = setTimeout(() => {
if (!resolved) { resolved = true; try { ws.close(); } catch (e) {} resolve({ relay: relayUrl, success: false, error: 'timeout' }); }
}, 15000);
ws.onopen = () => { ws.send(eventJson); };
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data);
if (data[0] === 'OK' && data[1] === event.id) {
if (!resolved) { resolved = true; clearTimeout(timeout); try { ws.close(); } catch (e) {} resolve({ relay: relayUrl, success: true }); }
} else if (data[0] === 'NOTICE') {
if (!resolved) { resolved = true; clearTimeout(timeout); try { ws.close(); } catch (e) {} resolve({ relay: relayUrl, success: false, error: data[1] }); }
}
} catch (e) {}
};
ws.onerror = () => { if (!resolved) { resolved = true; clearTimeout(timeout); resolve({ relay: relayUrl, success: false, error: 'connection error' }); } };
ws.onclose = () => { if (!resolved) { resolved = true; clearTimeout(timeout); resolve({ relay: relayUrl, success: false, error: 'closed without OK' }); } };
} catch (e) { resolve({ relay: relayUrl, success: false, error: e.message }); }
});
}));
}
/* ================================================================
RELAY QUERY (fetch ALL proof carrier events for OTS resume)
G56-02: fetch all candidates, not limit: 1
================================================================ */
function queryRelayForProofCarriers(pubkeyHex, relayUrl) {
return new Promise((resolve, reject) => {
try {
const ws = new WebSocket(relayUrl);
let resolved = false;
const events = [];
const timeout = setTimeout(() => {
if (!resolved) { resolved = true; try { ws.close(); } catch (e) {} resolve(events); }
}, 15000);
ws.onopen = () => {
const subId = 'pq_resume_' + Math.random().toString(36).substring(7);
ws.send(JSON.stringify(['REQ', subId, { kinds: [NIP_QR_KIND], authors: [pubkeyHex] }]));
};
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data);
if (data[0] === 'EVENT') {
if (data[2]) events.push(data[2]);
} else if (data[0] === 'EOSE') {
if (!resolved) { resolved = true; clearTimeout(timeout); try { ws.close(); } catch (e) {} resolve(events); }
}
} catch (e) {}
};
ws.onerror = () => { if (!resolved) { resolved = true; clearTimeout(timeout); reject(new Error('connection error')); } };
} catch (e) { reject(e); }
});
}
/* ================================================================
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, kind9999: 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, kind9999: 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 = '';
box.style.background = ok ? 'var(--accent-color)' : 'transparent';
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 k9999 = makeStatusCheckbox('k9999');
k9999.box.classList.add('pq-relay-status-kind9999');
row.appendChild(k9999.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.kind9999) setRelayStatus(url, 'kind9999', 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
================================================================ */
document.getElementById('pqSignInBtn').addEventListener('click', async () => {
pqSignInError.innerHTML = '';
try { await signIn(); }
catch (error) { console.error('[post-quantum] Sign-in failed:', error); setStatus(pqSignInError, 'error', error.message); }
});
document.getElementById('pqStartBtn').addEventListener('click', () => {
userEntropyChunks = [];
generateAndShowSeed();
showView('pqSeedStep');
setStepActive(2);
startEntropyCollection();
});
// Seed mode toggle
document.getElementById('pqSeedToggleGenerate').addEventListener('click', () => showSeedGeneratePanel());
document.getElementById('pqSeedToggleOwn').addEventListener('click', () => showSeedOwnPanel());
// Word-count selector: regenerate when the user switches between 12/24 words
document.querySelectorAll('input[name="pqWordCount"]').forEach((radio) => {
radio.addEventListener('change', () => {
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; }
});
});
// Bring your own seed: real-time validation
const pqSeedInput = document.getElementById('pqSeedInput');
const pqSeedValidation = document.getElementById('pqSeedValidation');
const pqSeedOwnContinueBtn = document.getElementById('pqSeedOwnContinueBtn');
pqSeedInput.addEventListener('input', () => {
const value = pqSeedInput.value.trim();
if (!value) {
pqSeedValidation.innerHTML = '';
pqSeedOwnContinueBtn.disabled = true;
return;
}
if (isValidMnemonic(value)) {
const wordCount = value.split(/\s+/).length;
pqSeedValidation.textContent = `Valid ${wordCount}-word BIP39 seed phrase`;
pqSeedValidation.style.color = '#00aa00';
pqSeedOwnContinueBtn.disabled = false;
} else {
pqSeedValidation.textContent = 'Invalid seed phrase (checksum or word list mismatch)';
pqSeedValidation.style.color = '#cc0000';
pqSeedOwnContinueBtn.disabled = true;
}
});
pqSeedOwnContinueBtn.addEventListener('click', () => {
pqMnemonic = pqSeedInput.value.trim();
setStepDone(2);
derivePQKeys();
});
document.getElementById('pqCopySeedBtn').addEventListener('click', () => {
if (pqMnemonic) {
navigator.clipboard.writeText(pqMnemonic).then(() => {
const btn = document.getElementById('pqCopySeedBtn');
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
});
}
});
// "Generate with Extra Entropy" — generates a fresh seed mixing CSPRNG + user entropy
document.getElementById('pqRegenerateWithEntropyBtn').addEventListener('click', () => { generateAndShowSeed(); });
pqSeedConfirmed.addEventListener('change', () => { pqSeedContinueBtn.disabled = !pqSeedConfirmed.checked; });
pqSeedContinueBtn.addEventListener('click', () => { setStepDone(2); derivePQKeys(); });
pqDeriveContinueBtn.addEventListener('click', () => { showView('pqSignStep'); setStepActive(4); });
pqSignBtn.addEventListener('click', () => { signEvent(); });
document.getElementById('pqPublishBtn').addEventListener('click', async () => {
const publishBtn = document.getElementById('pqPublishBtn');
const publishStatus = document.getElementById('pqPublishStatus');
publishBtn.disabled = true;
setStepActive(5);
publishStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing kind 1 announcement to relays...</div>';
try {
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.`);
// Publish kind 9999 proof carrier
publishStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing kind 9999 proof carrier to relays...</div>';
const results2 = await publishToRelays(pqEvent, relayUrls);
results2.forEach(r => setRelayStatus(r.relay, 'kind9999', r.success));
const success2 = results2.filter(r => r.success).length;
const fail2 = results2.filter(r => !r.success).length;
otsLog(`Kind 9999 proof carrier published: ${success2} ok, ${fail2} failed.`);
setStatus(publishStatus, 'success', `Published! Kind 1: ${success1}/${relayUrls.length} relays. Kind 9999: ${success2}/${relayUrls.length} relays.`);
publishBtn.textContent = 'Published';
publishBtn.disabled = true;
setStepDone(5);
// Show a Continue button so the user can review the relay results before advancing.
const continueBtn = document.getElementById('pqOtsContinueBtn');
if (continueBtn) continueBtn.classList.remove('pq-hidden');
} catch (error) {
setStatus(publishStatus, 'error', `Publish failed: ${error.message}`);
publishBtn.disabled = false;
}
});
// Continue to OpenTimestamps step (shown after successful publish)
document.getElementById('pqOtsContinueBtn').addEventListener('click', () => { prepareOtsStep(); });
// 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 Proof Carrier Event button
document.getElementById('pqCopyProofCarrierBtn').addEventListener('click', () => {
if (!pqEvent) return;
navigator.clipboard.writeText(JSON.stringify(pqEvent, null, 2)).then(() => {
const btn = document.getElementById('pqCopyProofCarrierBtn');
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy Proof Carrier Event'; }, 2000);
});
});
// Sign out button
const pqSignOutBtn = document.getElementById('pqSignOutBtn');
pqSignOutBtn.addEventListener('click', async () => {
console.log('[post-quantum] Signing out...');
// Stop OTS polling, but preserve its persisted workflow for the next login.
if (otsPollInterval) { clearInterval(otsPollInterval); otsPollInterval = null; }
pendingOtsBytes = null;
const persistedOts = localStorage.getItem('pq-pending-ots');
// Logout from nostr-login-lite
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
try { await window.NOSTR_LOGIN_LITE.logout(); } catch (e) { console.warn('[post-quantum] NLL logout failed:', e); }
}
// Clear local state
currentPubkey = null;
pqMnemonic = null;
pqSeed = null;
pqKeys = null;
pqSecpKeys = null;
pqEvent = null;
kind1Event = null; // F-L1: clear all secret references
userEntropyChunks = [];
pendingOtsBytes = null;
currentBlockHeight = 0;
pqRelays = DEFAULT_RELAYS.slice();
// Clear auth/session storage, then restore only the OTS workflow record.
try {
localStorage.clear();
if (persistedOts) localStorage.setItem('pq-pending-ots', persistedOts);
} catch (e) {}
try { sessionStorage.clear(); } catch (e) {}
// Hide sign out button
pqSignOutBtn.classList.add('pq-hidden');
// Reset checklist
for (let i = 1; i <= 7; i++) setStepPending(i);
setStepActive(1);
// Show sign-in view
showView('pqNotAuthed');
console.log('[post-quantum] Signed out');
});
/* ================================================================
STEP 6: OPENTIMESTAMPS
================================================================ */
let otsPollInterval = null;
let pendingOtsBytes = null;
function otsLog(message) {
const logEl = document.getElementById('pqOtsLog');
if (!logEl) return;
const time = new Date().toLocaleTimeString();
const line = document.createElement('div');
line.textContent = `[${time}] ${message}`;
line.style.borderBottom = '1px solid var(--muted-color)';
line.style.padding = '2px 0';
logEl.appendChild(line);
// Auto-scroll to bottom
logEl.scrollTop = logEl.scrollHeight;
}
function prepareOtsStep() {
// 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 otsLogEl = document.getElementById('pqOtsLog');
if (otsLogEl) otsLogEl.innerHTML = '';
setKeyIcon('pqOtsConfirm', '');
setKeyIcon('pqOtsConfirmedPublish', '');
// Check for a saved workflow from a previous session (same event ID)
let existing = loadPendingOts();
if (existing && !isDetachedOtsFile(existing.ots)) {
otsLog('Discarding stale proof data created by an older implementation (not a complete detached .ots file).');
clearPendingOts();
existing = null;
}
if (existing && existing.eventId === pqEvent.id) {
pendingOtsBytes = existing.ots;
otsLog(`Loaded saved OTS workflow for event ${pqEvent.id.substring(0, 16)}...`);
if (existing.pendingPublished) setKeyIcon('pqOtsPendingPublish', 'Done');
if (existing.confirmedPublished) {
setKeyIcon('pqOtsConfirm', 'Done');
setKeyIcon('pqOtsConfirmedPublish', 'Done');
setStepDone(6);
setStepActive(7);
otsStatus.innerHTML = '<div class="pq-status pq-status-success">Confirmed timestamp proof was already published.</div>';
showOtsProof(pendingOtsBytes);
return;
}
showOtsProof(pendingOtsBytes);
setKeyIcon('pqOtsPendingPublish', 'Done');
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 9999 event. Waiting for Bitcoin confirmation...</div>';
startOtsPolling();
return;
}
// The pending proof should already be embedded in the event from step 4 (signEvent).
// Extract it from the event's 'ots' tag.
const otsTag = pqEvent.tags && pqEvent.tags.find(t => t[0] === 'ots');
if (otsTag && otsTag[1]) {
try {
pendingOtsBytes = base64ToBytes(otsTag[1]);
if (isDetachedOtsFile(pendingOtsBytes)) {
otsLog(`Found pending OTS proof embedded in event (${pendingOtsBytes.length} bytes).`);
showOtsProof(pendingOtsBytes);
savePendingOts(pqEvent.id, pendingOtsBytes, {
ownerPubkey: currentPubkey,
targetKind: pqEvent.kind,
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 9999 event. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
startOtsPolling();
return;
}
} catch (e) {
otsLog(`WARNING: Could not parse OTS tag from event: ${e.message}`);
}
}
// No proof in the event — OTS submission failed during step 4.
// Submit the kind 1 event hash to OpenTimestamps now, then publish a new kind 9999 event with the proof.
const sha256Tag = pqEvent.tags && pqEvent.tags.find(t => t[0] === 'sha256');
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>';
return;
}
const fullHash = sha256Tag[1];
otsLog(`Submitting kind 1 event hash (${fullHash.substring(0, 16)}...) to OpenTimestamps calendar...`);
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Submitting kind 1 event hash to OpenTimestamps...</div>';
try {
const otsBytes = await timestampEvent(fullHash);
pendingOtsBytes = otsBytes;
otsLog(`Created pending detached .ots file (${otsBytes.length} bytes).`);
showOtsProof(otsBytes);
// Republish the event with the OTS proof embedded
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing new kind 9999 event with embedded OTS proof...</div>';
await publishUpgradedEvent(otsBytes);
setKeyIcon('pqOtsPendingPublish', 'Done');
savePendingOts(pqEvent.id, otsBytes, {
ownerPubkey: currentPubkey,
targetKind: pqEvent.kind,
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>';
startOtsPolling();
} catch (error) {
console.error('[ots] Submission failed:', error);
otsLog(`ERROR: ${error.message}`);
setStatus(otsStatus, 'error', `OpenTimestamps workflow failed: ${error.message}`);
}
}
function showOtsProof(otsBytes) {
const proofWrap = document.getElementById('pqOtsProofWrap');
const proofEl = document.getElementById('pqOtsProof');
proofEl.textContent = bytesToBase64(otsBytes);
proofWrap.classList.remove('pq-hidden');
}
function startOtsPolling() {
const confirmDetail = document.getElementById('pqOtsConfirmDetail');
let pollCount = 0;
if (otsPollInterval) clearInterval(otsPollInterval);
async function poll() {
pollCount++;
otsLog(`Poll ${pollCount}: Sending current .ots proof to upgrade service...`);
confirmDetail.textContent = `Polling (attempt ${pollCount})...`;
try {
const result = await upgradeOts(pendingOtsBytes);
pendingOtsBytes = result.proof;
savePendingOts(pqEvent.id, pendingOtsBytes, {
ownerPubkey: currentPubkey,
targetKind: pqEvent.kind
});
showOtsProof(pendingOtsBytes);
// Run full cryptographic verification: parse the proof, bind the
// target digest to the event's sha256 tag, walk the Merkle path,
// and check the block header against a Bitcoin API.
const sha256Tag = pqEvent.tags && pqEvent.tags.find(t => t[0] === 'sha256');
const verifyResult = await verifyOtsProof(pendingOtsBytes, sha256Tag ? sha256Tag[1] : undefined);
if (verifyResult.verified) {
const att = verifyResult.bitcoinAttestations[0];
const date = att ? new Date(att.time * 1000).toISOString().substring(0, 19) : 'unknown';
otsLog(`Poll ${pollCount}: Bitcoin attestation VERIFIED (block ${att ? att.height : '?'}, mined ${date} UTC). Proof: ${pendingOtsBytes.length} bytes.`);
setKeyIcon('pqOtsConfirm', 'Done');
confirmDetail.textContent = `Verified on Bitcoin blockchain (block ${att ? att.height : '?'})!`;
if (otsPollInterval) { clearInterval(otsPollInterval); otsPollInterval = null; }
const saved = loadPendingOts();
if (!saved || !saved.confirmedPublished) {
setStatus(document.getElementById('pqOtsStatus'), 'success', 'Bitcoin attestation verified. Publishing new kind 9999 with confirmed proof...');
await publishUpgradedEvent(pendingOtsBytes);
}
} else if (isOtsConfirmed(pendingOtsBytes)) {
// Structural check found a Bitcoin attestation tag but full
// verification failed — log the errors for diagnosis.
const errs = verifyResult.errors.length > 0 ? ` Errors: ${verifyResult.errors.join('; ')}` : '';
otsLog(`Poll ${pollCount}: Bitcoin attestation tag found but verification incomplete.${errs} Will retry.`);
confirmDetail.textContent = `Attestation found, verifying (attempt ${pollCount}). Retrying in 60s...`;
} else {
// Still pending — the upgrade helper's stderr often contains
// "Failed! Timestamp not complete" which is normal pending output
// from ots-cli, not an actual error. Sanitize it for display.
const rawDetail = (result.detail || '').replace(/\s+/g, ' ').trim();
const friendlyDetail = rawDetail
.replace(/Failed!\s*Timestamp not complete\.?\s*/gi, '')
.replace(/Failed!/gi, 'Pending')
.replace(/Pending confirmation in Bitcoin blockchain/gi, 'Waiting for Bitcoin confirmation')
.trim();
const detail = friendlyDetail ? ` (${friendlyDetail.slice(0, 180)})` : '';
otsLog(`Poll ${pollCount}: Still pending${detail}. Next poll in 60 seconds.`);
confirmDetail.textContent = `Still pending (attempt ${pollCount}). Next poll in 60s...`;
}
} catch (error) {
otsLog(`Poll ${pollCount}: ERROR - ${error.message}. Retrying in 60 seconds.`);
confirmDetail.textContent = `Poll failed (attempt ${pollCount}). Retrying in 60s...`;
}
}
poll();
otsPollInterval = setInterval(poll, 60000);
}
async function publishUpgradedEvent(upgradedOtsBytes) {
const otsStatus = document.getElementById('pqOtsStatus');
try {
otsLog('Building upgraded proof carrier with confirmed OTS proof...');
const savedWorkflow = loadPendingOts();
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);
if (!window.nostr || !window.nostr.signEvent) throw new Error('Nostr signer not available');
otsLog('Requesting secp256k1 signature for upgraded proof carrier...');
const signedEvent = await window.nostr.signEvent(upgradedTemplate);
const relayUrls = configuredRelays.split(',').map(s => s.trim()).filter(Boolean);
const results = await publishToRelays(signedEvent, relayUrls);
const successCount = results.filter(r => r.success).length;
const failCount = results.filter(r => !r.success).length;
if (successCount === 0) throw new Error('No relay accepted the upgraded proof carrier');
setKeyIcon('pqOtsConfirmedPublish', 'Done');
otsLog(`Upgraded proof carrier published. Successful: ${successCount}; failed: ${failCount}.`);
// Update pqEvent to the new event so future operations use it
pqEvent = signedEvent;
savePendingOts(pqEvent.id, upgradedOtsBytes, {
ownerPubkey: currentPubkey,
targetKind: NIP_QR_KIND,
relayUrls: configuredRelays,
pendingPublished: true,
confirmedPublished: true
});
setStatus(otsStatus, 'success', `Upgraded proof carrier published to ${successCount} relays. Timestamping complete.`);
setStepDone(6);
setStepActive(7);
return signedEvent;
} catch (error) {
otsLog(`ERROR publishing upgraded event: ${error.message}`);
setStatus(otsStatus, 'error', `Failed to publish upgraded event: ${error.message}`);
throw error;
}
}
// OTS event listeners
document.getElementById('pqOtsDownloadBtn').addEventListener('click', () => {
if (pendingOtsBytes) {
const blob = new Blob([pendingOtsBytes], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${pqEvent.id.substring(0, 16)}.ots`;
a.click();
URL.revokeObjectURL(url);
}
});
/* ================================================================
INITIALIZATION
================================================================ */
(async function main() {
console.log('[post-quantum] Starting...');
setStepActive(1);
if (window.NOSTR_LOGIN_LITE) {
try {
await initNostrLoginLite();
if (window.nostr) {
try {
currentPubkey = await window.nostr.getPublicKey();
if (currentPubkey) {
console.log('[post-quantum] Auto-logged in as:', currentPubkey);
await showAuthedView();
return;
}
} catch (e) { console.log('[post-quantum] Auth not restored:', e.message); }
}
} catch (e) { console.log('[post-quantum] nostr-login-lite init failed:', e.message); }
}
if (!window.NOSTR_LOGIN_LITE && !window.nostr) {
noSignerWarning.style.display = 'block';
document.getElementById('pqSignInBtn').disabled = true;
}
if (!window.NOSTR_LOGIN_LITE && window.nostr && window.nostr.getPublicKey) {
try {
currentPubkey = await window.nostr.getPublicKey();
if (currentPubkey) {
console.log('[post-quantum] Auto-logged in (extension) as:', currentPubkey);
await showAuthedView();
return;
}
} catch (e) { console.log('[post-quantum] Auto-login failed:', e.message); }
}
showView('pqNotAuthed');
console.log('[post-quantum] Initialization complete');
})();
</script>
</body>
</html>