Files
nostr_quantum_preparation/www/index.html
T

617 lines
28 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'; connect-src wss: https:; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none';" />
<meta name="referrer" content="no-referrer" />
<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: 12px; font-size: 13px; color: var(--accent-color);">
<strong>Verify out-of-band after migration:</strong> This seed phrase is your backup — but
this browser cannot cryptographically prove the published PQ keys came from it. After completing
the migration, take your seed phrase to a <em>different</em> device or browser, derive the keys
independently, and confirm the public keys match the published event. This is the only way to
catch a compromised browser that might have substituted attacker keys.
</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-info-text" id="pqEventSizeWarning" style="display: none; font-size: 13px; margin-top: 8px;"></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>
<button class="pq-button pq-button-secondary" id="pqDownloadArchiveBtn" title="Download a self-contained proof archive (kind 1 event + proof carrier + OTS proof) so you can verify it offline even if relays delete it.">Download Proof Archive</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" integrity="sha384-LNnPDD++DaWxljIhMLmfaoEoKB0B1HmACC6gNGLG+Qnmosprf/AX7u84pVY8xMpM" crossorigin="anonymous"></script>
<script src="/nostr-login-lite/nostr-lite.js" integrity="sha384-IQwa65eDC5trGjKn4cuEazmFiHTHD65gIqsjpzDtouc+j0MlyI927SZgHMWSi2aC" crossorigin="anonymous"></script>
<!-- Version display: fetch version.json and update title + header (G56-08: extracted to external file, self-initializing) -->
<script src="./js/version-display.js"></script>
<!-- Main application module (G56-08: extracted to external file for CSP compliance) -->
<script type="module" src="./js/index-app.mjs"></script>
</body>
</html>