Files
n_signer/examples/cyd_webserial_demo.html
T

906 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>n_signer CYD Web Serial Demo</title>
<script>
/* §12 Dark mode: set class synchronously before paint. */
(function () {
if (localStorage.getItem('theme') === 'dark') {
document.documentElement.classList.add('dark-mode');
}
})();
</script>
<style>
/*
* Two-tier rule block (§13). This is a single-file demo so the "global"
* sheet is inlined here, but the same rules apply: variables everywhere,
* no hardcoded hex in component rules, spacing on the 5/10/15/20/40 ladder.
*/
/* ---- §17 Minimum variable set ---- */
:root {
--font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", Courier, monospace;
--primary-color: #000000;
--secondary-color: #ffffff;
--accent-color: #ff0000;
--muted-color: #dddddd;
--color: var(--primary-color);
--background-color: var(--secondary-color);
--border-color: var(--muted-color);
--border-radius: 5px;
--border-width: 1px;
--border: var(--border-width) solid var(--border-color);
--header-height: 2vh;
--header-min-height: 60px;
--footer-height: 2vh;
--footer-min-height: 20px;
--image-grayscale: 100%;
--image-grayscale-hover: 20%;
}
/* §12 Dark mode = swap variables only. */
html.dark-mode, body.dark-mode {
--primary-color: #ffffff;
--secondary-color: #000000;
--muted-color: #777777;
}
/* ---- §3 Typography / global reset ---- */
* {
font-family: var(--font-family);
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
color: var(--color);
background-color: var(--background-color);
line-height: 1.4;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
a { color: var(--accent-color); }
/* ---- §4 Layout shell ---- */
#divHeader {
position: fixed;
top: 0; left: 0; right: 0;
height: var(--header-height);
min-height: var(--header-min-height);
background-color: var(--secondary-color);
border-bottom: 3px solid var(--primary-color);
display: flex;
align-items: center;
padding: 0 20px;
z-index: 2;
}
#divHeader .divHeaderText {
font-size: 200%;
font-weight: bold;
flex: 1;
text-align: center;
}
#divHeader .divHeaderButtons {
width: 40px;
display: flex;
justify-content: flex-start;
}
/* Keep the hamburger clickable above the open sidenav (§10 overlay). */
#btnHamburger { position: relative; z-index: 4; }
#divBody {
position: relative;
top: max(var(--header-height), var(--header-min-height));
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
width: 100%;
padding: 20px;
gap: 20px;
}
#divFooter {
position: fixed;
bottom: 0; left: 0; right: 0;
height: var(--footer-height);
min-height: var(--footer-min-height);
border-top: 3px solid var(--primary-color);
background-color: var(--secondary-color);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 20px;
z-index: 2;
}
.divFooterBox {
font-size: 12px;
color: var(--muted-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#divFooterCenter { text-align: center; flex: 1; }
#divFooterRight { text-align: right; }
/* ---- §10 SideNav ---- */
#divSideNav {
position: fixed;
top: 0; left: 0;
height: 100%;
width: 0;
transition: width 0.5s;
border-right: 3px solid var(--primary-color);
background-color: var(--secondary-color);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 3;
}
#divSideNav.open { width: clamp(400px, 50vw, 600px); }
#divSideNavBody {
flex: 1;
padding: 20px;
overflow-y: auto;
font-size: 14px;
}
/* Close button pinned to the top-right of the open sidenav. */
#btnCloseSideNav {
position: absolute;
top: 10px;
right: 10px;
width: 32px;
height: 32px;
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
cursor: pointer;
font-size: 20px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
z-index: 4;
}
#btnCloseSideNav:hover { border-color: var(--accent-color); }
#divSideNavBody h3 {
font-size: 14px;
font-weight: bold;
margin-top: 20px;
margin-bottom: 10px;
}
#divSideNavBody h3:first-child { margin-top: 0; }
#divSideNavBody p {
margin-bottom: 10px;
color: var(--muted-color);
}
#divSideNavBody code {
color: var(--primary-color);
white-space: pre-wrap;
}
#divVersionBar {
flex-shrink: 0;
border-top: 3px solid var(--primary-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
}
#versionDisplay { font-size: 14px; }
#divVersionBarButtons { display: flex; gap: 10px; }
/* Hamburger button (§6 icon-only button) */
#btnHamburger {
width: 32px; height: 32px;
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
cursor: pointer;
font-size: 20px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
}
#btnHamburger:hover { border-color: var(--accent-color); }
/* ---- §6 Buttons ---- */
.btn {
font-family: var(--font-family);
color: var(--primary-color);
background-color: var(--secondary-color);
border: var(--border-width) solid var(--primary-color);
border-radius: var(--border-radius);
padding: 8px 16px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: border-color 0.2s, background-color 0.2s, color 0.2s;
}
.btn:hover { border-color: var(--accent-color); }
.btn:active {
background-color: var(--accent-color);
color: var(--secondary-color);
border-color: var(--primary-color);
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.secondary { border-color: var(--muted-color); }
.btn.danger { border-color: var(--accent-color); color: var(--accent-color); }
.btn.danger:active { background-color: var(--accent-color); color: var(--secondary-color); }
/* ---- §7 Forms ---- */
.inpStyle {
border: var(--border);
border-radius: var(--border-radius);
padding: 10px;
font-size: 100%;
font-family: var(--font-family);
color: var(--primary-color);
background-color: var(--secondary-color);
width: 100%;
}
.inpStyle:focus { outline: none; border-color: var(--accent-color); }
textarea.inpStyle { resize: vertical; min-height: 64px; }
input[type="number"].inpStyle { max-width: 130px; }
label {
font-size: 12px;
color: var(--muted-color);
display: block;
margin: 10px 0 5px;
}
/* ---- §9 Cards ---- */
.divPostItem {
padding: 15px;
border: 1px solid var(--primary-color);
border-radius: 10px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
background-color: var(--secondary-color);
}
.divPostItem h2 {
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
}
.divPostItem .row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-top: 10px;
}
.divPostItem .note {
font-size: 12px;
color: var(--muted-color);
margin-bottom: 10px;
}
.divPostItem .warn {
font-size: 12px;
color: var(--accent-color);
margin-bottom: 10px;
}
/* Connection card spans full width */
.divPostItem.full { width: 100%; }
.divPostItem.section { flex: 1 1 320px; min-width: 320px; }
/* ---- §11 Feedback / state ---- */
.status {
padding: 5px 10px;
border: var(--border);
border-radius: 999px;
font-size: 12px;
color: var(--muted-color);
}
.status.ok { border-color: var(--primary-color); color: var(--primary-color); }
.status.err { border-color: var(--accent-color); color: var(--accent-color); }
pre {
margin-top: 10px;
padding: 10px;
border: var(--border);
border-radius: var(--border-radius);
background-color: var(--secondary-color);
color: var(--primary-color);
overflow: auto;
max-height: 200px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
/* §14 reduced motion */
@media (prefers-reduced-motion: reduce) {
#divSideNav { transition: none; }
.btn, #btnHamburger, img { transition: none; }
}
</style>
</head>
<body>
<!-- §16 canonical shell -->
<div id="divHeader">
<div class="divHeaderButtons">
<button id="btnHamburger" title="Menu" aria-label="Open menu"></button>
</div>
<div class="divHeaderText">n_signer CYD Web Serial</div>
<div class="divHeaderButtons"></div>
</div>
<div id="divBody">
<!-- Connection card (full width) -->
<section class="divPostItem full">
<h2>Connection</h2>
<p class="note">Connect to the CYD (CH340 serial) via Web Serial, then exercise every algorithm and verb in the n_signer API. Chrome/Edge/Brave/Opera only.</p>
<div class="row">
<button id="connectBtn" class="btn">Connect Web Serial</button>
<button id="disconnectBtn" class="btn secondary" disabled>Disconnect</button>
<span id="connStatus" class="status">Disconnected</span>
</div>
<pre id="log"></pre>
</section>
<!-- get_info (metadata) -->
<section class="divPostItem section">
<h2>get_info (version / capabilities)</h2>
<p class="note">No approval required. Safe to call before the mnemonic is loaded.</p>
<div class="row">
<button id="infoBtn" class="btn" disabled>get_info</button>
</div>
<pre id="infoOut"></pre>
</section>
<!-- Get Public Key (algorithm-based) -->
<section class="divPostItem section">
<h2>get_public_key (algorithm)</h2>
<label for="gpkAlg">Algorithm</label>
<select id="gpkAlg" class="inpStyle">
<option>secp256k1</option><option>ed25519</option><option>x25519</option>
<option>ml-dsa-65</option><option>slh-dsa-128s</option><option>ml-kem-768</option>
</select>
<label for="gpkIdx">Index</label>
<input id="gpkIdx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="gpkBtn" class="btn" disabled>get_public_key</button>
</div>
<pre id="gpkOut"></pre>
</section>
<!-- Nostr Get Public Key -->
<section class="divPostItem section">
<h2>nostr_get_public_key</h2>
<label for="ngpkIdx">nostr_index</label>
<input id="ngpkIdx" type="number" value="0" min="0" class="inpStyle" />
<label for="ngpkFmt">format</label>
<select id="ngpkFmt" class="inpStyle"><option>bare</option><option>structured</option></select>
<div class="row">
<button id="ngpkBtn" class="btn" disabled>nostr_get_public_key</button>
</div>
<pre id="ngpkOut"></pre>
</section>
<!-- Sign / Verify -->
<section class="divPostItem section">
<h2>sign / verify</h2>
<label for="signAlg">Algorithm</label>
<select id="signAlg" class="inpStyle">
<option>secp256k1</option><option>ed25519</option><option>ml-dsa-65</option><option>slh-dsa-128s</option>
</select>
<label for="signIdx">Index</label>
<input id="signIdx" type="number" value="0" min="0" class="inpStyle" />
<label for="signScheme">scheme (secp256k1 only)</label>
<select id="signScheme" class="inpStyle"><option>schnorr</option><option>ecdsa</option></select>
<label for="signMsg">message (hex — 32 bytes / 64 hex for schnorr)</label>
<input id="signMsg" value="2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" class="inpStyle" />
<div class="row">
<button id="signBtn" class="btn" disabled>sign</button>
<button id="verifyBtn" class="btn" disabled>verify</button>
</div>
<pre id="signOut"></pre>
</section>
<!-- KEM encapsulate / decapsulate -->
<section class="divPostItem section">
<h2>encapsulate / decapsulate (ml-kem-768)</h2>
<label for="kemPeer">peer pubkey hex (1184 bytes / 2368 hex) — leave empty to use self pubkey</label>
<textarea id="kemPeer" class="inpStyle" placeholder="auto: uses ml-kem-768 get_public_key"></textarea>
<label for="kemIdx">index (for decapsulate)</label>
<input id="kemIdx" type="number" value="0" min="0" class="inpStyle" />
<label for="kemCt">ciphertext hex (for decapsulate)</label>
<textarea id="kemCt" class="inpStyle" placeholder="filled by encapsulate"></textarea>
<div class="row">
<button id="encapBtn" class="btn" disabled>encapsulate</button>
<button id="decapBtn" class="btn" disabled>decapsulate</button>
</div>
<pre id="kemOut"></pre>
</section>
<!-- derive_shared_secret (x25519) -->
<section class="divPostItem section">
<h2>derive_shared_secret (x25519)</h2>
<label for="x25519Peer">peer pubkey hex (32 bytes / 64 hex)</label>
<input id="x25519Peer" class="inpStyle" placeholder="64 hex chars" />
<label for="x25519Idx">index</label>
<input id="x25519Idx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="x25519Btn" class="btn" disabled>derive_shared_secret</button>
</div>
<pre id="x25519Out"></pre>
</section>
<!-- derive (HMAC) -->
<section class="divPostItem section">
<h2>derive (secp256k1 HMAC-SHA256)</h2>
<label for="deriveData">data (UTF-8 string)</label>
<input id="deriveData" value="hello-derive" class="inpStyle" />
<label for="deriveIdx">index (required)</label>
<input id="deriveIdx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="deriveBtn" class="btn" disabled>derive</button>
</div>
<pre id="deriveOut"></pre>
</section>
<!-- Nostr Sign Event -->
<section class="divPostItem section">
<h2>nostr_sign_event</h2>
<label for="nseContent">content</label>
<textarea id="nseContent" class="inpStyle">hello from cyd webserial demo</textarea>
<label for="nseIdx">nostr_index</label>
<input id="nseIdx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="nseBtn" class="btn" disabled>nostr_sign_event</button>
</div>
<pre id="nseOut"></pre>
</section>
<!-- Nostr Mine Event -->
<section class="divPostItem section">
<h2>nostr_mine_event</h2>
<p class="warn">Slow on ESP32 — uses single-threaded PoW. Keep difficulty low.</p>
<label for="nmeContent">content</label>
<textarea id="nmeContent" class="inpStyle">mined by cyd</textarea>
<label for="nmeIdx">nostr_index</label>
<input id="nmeIdx" type="number" value="0" min="0" class="inpStyle" />
<label for="nmeDiff">difficulty (leading zero bits)</label>
<input id="nmeDiff" type="number" value="4" min="1" max="16" class="inpStyle" />
<label for="nmeTimeout">timeout (sec)</label>
<input id="nmeTimeout" type="number" value="30" min="1" max="60" class="inpStyle" />
<div class="row">
<button id="nmeBtn" class="btn" disabled>nostr_mine_event</button>
</div>
<pre id="nmeOut"></pre>
</section>
<!-- NIP-04 -->
<section class="divPostItem section">
<h2>nostr_nip04_encrypt / decrypt</h2>
<label for="nip04Peer">peer pubkey hex (32-byte x-only)</label>
<input id="nip04Peer" class="inpStyle" placeholder="64 hex chars" />
<label for="nip04Msg">plaintext</label>
<textarea id="nip04Msg" class="inpStyle">hello via nip04</textarea>
<label for="nip04Cipher">ciphertext (for decrypt)</label>
<textarea id="nip04Cipher" class="inpStyle" placeholder="ciphertext?iv=..."></textarea>
<label for="nip04Idx">nostr_index</label>
<input id="nip04Idx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="nip04EncBtn" class="btn" disabled>encrypt</button>
<button id="nip04DecBtn" class="btn" disabled>decrypt</button>
</div>
<pre id="nip04Out"></pre>
</section>
<!-- NIP-44 -->
<section class="divPostItem section">
<h2>nostr_nip44_encrypt / decrypt</h2>
<label for="nip44Peer">peer pubkey hex (32-byte x-only)</label>
<input id="nip44Peer" class="inpStyle" placeholder="64 hex chars" />
<label for="nip44Msg">plaintext</label>
<textarea id="nip44Msg" class="inpStyle">hello via nip44</textarea>
<label for="nip44Cipher">ciphertext (for decrypt)</label>
<textarea id="nip44Cipher" class="inpStyle" placeholder="base64 payload"></textarea>
<label for="nip44Idx">nostr_index</label>
<input id="nip44Idx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="nip44EncBtn" class="btn" disabled>encrypt</button>
<button id="nip44DecBtn" class="btn" disabled>decrypt</button>
</div>
<pre id="nip44Out"></pre>
</section>
<!-- OTP encrypt / decrypt -->
<section class="divPostItem section">
<h2>encrypt / decrypt (otp)</h2>
<p class="note">OTP pad is derived from the mnemonic on the CYD. Offset advances monotonically.</p>
<label for="otpPlain">plaintext (base64)</label>
<textarea id="otpPlain" class="inpStyle">SGVsbG8sIE9UUCB3b3JsZCE=</textarea>
<label for="otpCipher">ciphertext (for decrypt, base64)</label>
<textarea id="otpCipher" class="inpStyle" placeholder="filled by encrypt"></textarea>
<label for="otpEnc">encoding</label>
<select id="otpEnc" class="inpStyle"><option>ascii</option><option>binary</option></select>
<div class="row">
<button id="otpEncBtn" class="btn" disabled>encrypt</button>
<button id="otpDecBtn" class="btn" disabled>decrypt</button>
</div>
<pre id="otpOut"></pre>
</section>
</div>
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox">n_signer</div>
<div id="divFooterCenter" class="divFooterBox">ready</div>
<div id="divFooterRight" class="divFooterBox">Web Serial</div>
</div>
<!-- §10 SideNav -->
<div id="divSideNav">
<button id="btnCloseSideNav" title="Close menu" aria-label="Close menu">×</button>
<div id="divSideNavBody">
<h3>n_signer CYD Web Serial Demo</h3>
<p>A browser control panel for the CYD (ESP32-2432S028) signer firmware. Speaks the n_signer JSON-RPC protocol over Web Serial at 115200 baud.</p>
<h3>Getting started</h3>
<p>1. Plug the CYD into a USB port (CH340 enumerates as /dev/ttyUSB*).</p>
<p>2. Click <code>Connect Web Serial</code> and pick the CYD port.</p>
<p>3. <code>get_info</code> fires automatically and reports firmware version + supported verbs.</p>
<p>4. Enter a mnemonic on the CYD touchscreen to enable the signing verbs.</p>
<h3>Auth envelope</h3>
<p>Every request is signed with a demo secp256k1 key (priv = 0x0102…2020). The CYD verifies the kind-27235 auth event and replays-protects by event id.</p>
<h3>Transports</h3>
<p>Frames are 4-byte big-endian length + UTF-8 JSON payload. Same wire format as the host Unix-socket and TCP transports.</p>
</div>
<div id="divVersionBar">
<span id="versionDisplay">demo v1</span>
<div id="divVersionBarButtons">
<button id="themeToggleButton" class="btn secondary" title="Toggle theme">theme</button>
</div>
</div>
</div>
<script type="module">
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
const logEl = document.getElementById("log");
const connStatusEl = document.getElementById("connStatus");
const footerStatusEl = document.getElementById("divFooterCenter");
const connectBtn = document.getElementById("connectBtn");
const disconnectBtn = document.getElementById("disconnectBtn");
const btnHamburger = document.getElementById("btnHamburger");
const divSideNav = document.getElementById("divSideNav");
const themeToggleButton = document.getElementById("themeToggleButton");
let port = null;
let reader = null;
let writer = null;
let readLoopRunning = false;
let rxBuffer = new Uint8Array(0);
let pendingResolve = null;
function log(...args) {
logEl.textContent += args.join(" ") + "\n";
logEl.scrollTop = logEl.scrollHeight;
}
function setStatus(text, mode = "") {
connStatusEl.textContent = text;
connStatusEl.className = `status ${mode}`.trim();
footerStatusEl.textContent = text;
}
function hex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
}
function utf8(s) { return new TextEncoder().encode(s); }
function be32(n) {
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
}
async function sha256Hex(dataBytes) {
const h = await crypto.subtle.digest("SHA-256", dataBytes);
return hex(new Uint8Array(h));
}
function pretty(value) {
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
}
async function buildAuth(method, params) {
const callerPriv = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
const callerPubX = hex(schnorr.getPublicKey(callerPriv));
const createdAt = Math.floor(Date.now() / 1000);
const paramsJson = JSON.stringify(params);
const bodyHash = await sha256Hex(utf8(paramsJson));
const tags = [
["nsigner_rpc", "1"],
["nsigner_method", method],
["nsigner_body_hash", bodyHash],
];
const content = "cyd-webserial-demo";
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
const id = await sha256Hex(utf8(ser));
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
const sigHex = typeof sigBytes === "string" ? sigBytes : hex(sigBytes);
return { id, pubkey: callerPubX, created_at: createdAt, kind: 27235, tags, content, sig: sigHex };
}
/* ---- Web Serial transport ---- */
async function readLoop() {
readLoopRunning = true;
while (readLoopRunning && reader) {
try {
const { value, done } = await reader.read();
if (done) break;
if (value) {
const next = new Uint8Array(rxBuffer.length + value.length);
next.set(rxBuffer, 0);
next.set(value, rxBuffer.length);
rxBuffer = next;
tryDeliver();
}
} catch (e) {
log("read error:", e.message);
break;
}
}
readLoopRunning = false;
}
function tryDeliver() {
if (pendingResolve === null) return;
while (rxBuffer.length >= 4) {
const len = (rxBuffer[0] << 24) | (rxBuffer[1] << 16) | (rxBuffer[2] << 8) | rxBuffer[3];
if (len <= 0 || len > 16384) {
rxBuffer = rxBuffer.slice(1);
continue;
}
if (rxBuffer.length < 4 + len) return;
const payload = rxBuffer.slice(4, 4 + len);
rxBuffer = rxBuffer.slice(4 + len);
const text = new TextDecoder().decode(payload);
let parsed;
try { parsed = JSON.parse(text); } catch { parsed = text; }
const r = pendingResolve;
pendingResolve = null;
r(parsed);
if (pendingResolve === null) return;
}
}
async function sendRpc(reqObj) {
if (!writer) throw new Error("not connected");
const body = utf8(JSON.stringify(reqObj));
const frame = new Uint8Array(4 + body.length);
frame.set(be32(body.length), 0);
frame.set(body, 4);
await writer.write(frame);
const resp = await new Promise((resolve, reject) => {
pendingResolve = resolve;
setTimeout(() => {
if (pendingResolve === resolve) {
pendingResolve = null;
reject(new Error("timeout (30s) — check the CYD screen for an approval prompt"));
}
}, 65000);
});
return resp;
}
async function callVerb(method, params, outEl) {
outEl.textContent = "→ " + method + " " + JSON.stringify(params);
try {
const auth = await buildAuth(method, params);
const req = { id: String(Math.floor(Math.random() * 1e9)), method, params, auth };
const resp = await sendRpc(req);
outEl.textContent += "\n← " + pretty(resp);
} catch (e) {
outEl.textContent += "\n✗ " + e.message;
}
}
/* ---- SideNav + theme (§10, §12) ---- */
function openSideNav() { divSideNav.classList.add("open"); }
function closeSideNav() { divSideNav.classList.remove("open"); }
btnHamburger.addEventListener("click", () => {
if (divSideNav.classList.contains("open")) closeSideNav(); else openSideNav();
});
document.getElementById("btnCloseSideNav").addEventListener("click", closeSideNav);
/* Click on the scrim (the body, outside the sidenav) closes it. */
document.getElementById("divBody").addEventListener("click", () => {
if (divSideNav.classList.contains("open")) closeSideNav();
});
/* ESC closes sidenav (§14). */
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && divSideNav.classList.contains("open")) {
closeSideNav();
}
});
themeToggleButton.addEventListener("click", () => {
const isDark = document.documentElement.classList.toggle("dark-mode");
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
/* ---- Connect / Disconnect ---- */
connectBtn.addEventListener("click", async () => {
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
reader = port.readable.getReader();
writer = port.writable.getWriter();
rxBuffer = new Uint8Array(0);
readLoop();
setStatus("Connected", "ok");
log("Connected to CYD via Web Serial @ 115200 baud");
document.querySelectorAll("button[id$='Btn']").forEach(b => {
if (b !== connectBtn && b !== disconnectBtn) b.disabled = false;
});
disconnectBtn.disabled = false;
connectBtn.disabled = true;
/* Auto-fetch version/capabilities so the user sees what firmware is running. */
try { document.getElementById("infoBtn").click(); } catch (e) { log("auto get_info failed:", e.message); }
} catch (e) {
setStatus("Error", "err");
log("connect failed:", e.message);
}
});
disconnectBtn.addEventListener("click", async () => {
readLoopRunning = false;
try { if (reader) await reader.cancel(); } catch {}
try { if (writer) await writer.releaseLock(); } catch {}
try { if (port) await port.close(); } catch {}
reader = null; writer = null; port = null;
setStatus("Disconnected");
document.querySelectorAll("button[id$='Btn']").forEach(b => {
if (b !== connectBtn) b.disabled = true;
});
connectBtn.disabled = false;
disconnectBtn.disabled = true;
log("Disconnected");
});
/* ---- Verb wiring ---- */
const $ = id => document.getElementById(id);
$("infoBtn").addEventListener("click", () => {
callVerb("get_info", [], $("infoOut"));
});
$("gpkBtn").addEventListener("click", () => {
const alg = $("gpkAlg").value, idx = Number($("gpkIdx").value || 0);
callVerb("get_public_key", [{ algorithm: alg, index: idx }], $("gpkOut"));
});
$("ngpkBtn").addEventListener("click", () => {
const idx = Number($("ngpkIdx").value || 0), fmt = $("ngpkFmt").value;
const opts = { nostr_index: idx };
if (fmt === "structured") opts.format = "structured";
callVerb("nostr_get_public_key", [opts], $("ngpkOut"));
});
$("signBtn").addEventListener("click", () => {
const alg = $("signAlg").value, idx = Number($("signIdx").value || 0);
const scheme = $("signScheme").value, msg = $("signMsg").value;
const opts = { algorithm: alg, index: idx };
if (alg === "secp256k1") opts.scheme = scheme;
callVerb("sign", [msg, opts], $("signOut"));
});
$("verifyBtn").addEventListener("click", async () => {
const alg = $("signAlg").value, idx = Number($("signIdx").value || 0);
const scheme = $("signScheme").value, msg = $("signMsg").value;
const opts = { algorithm: alg, index: idx };
if (alg === "secp256k1") opts.scheme = scheme;
const outText = $("signOut").textContent;
const m = outText.match(/"signature"\s*:\s*"([0-9a-f]+)"/);
if (!m) { $("signOut").textContent += "\n✗ no signature found — run sign first"; return; }
callVerb("verify", [msg, m[1], opts], $("signOut"));
});
$("encapBtn").addEventListener("click", async () => {
let peer = $("kemPeer").value.trim();
if (!peer) {
const opts = [{ algorithm: "ml-kem-768", index: Number($("kemIdx").value || 0) }];
const auth = await buildAuth("get_public_key", opts);
const resp = await sendRpc({ id: String(Math.floor(Math.random()*1e9)), method: "get_public_key", params: opts, auth });
peer = resp.result && JSON.parse(resp.result).public_key;
if (!peer) { $("kemOut").textContent = "✗ could not fetch self pubkey"; return; }
$("kemPeer").value = peer;
}
callVerb("encapsulate", [peer, { algorithm: "ml-kem-768" }], $("kemOut"));
});
$("decapBtn").addEventListener("click", () => {
const ct = $("kemCt").value.trim();
const idx = Number($("kemIdx").value || 0);
if (!ct) { $("kemOut").textContent = "✗ paste a ciphertext first (from encapsulate)"; return; }
callVerb("decapsulate", [ct, { algorithm: "ml-kem-768", index: idx }], $("kemOut"));
});
$("x25519Btn").addEventListener("click", () => {
const peer = $("x25519Peer").value.trim();
const idx = Number($("x25519Idx").value || 0);
if (!peer) { $("x25519Out").textContent = "✗ enter peer pubkey"; return; }
callVerb("derive_shared_secret", [peer, { algorithm: "x25519", index: idx }], $("x25519Out"));
});
$("deriveBtn").addEventListener("click", () => {
const data = $("deriveData").value;
const idx = Number($("deriveIdx").value || 0);
callVerb("derive", [data, { algorithm: "secp256k1", index: idx }], $("deriveOut"));
});
$("nseBtn").addEventListener("click", () => {
const content = $("nseContent").value;
const idx = Number($("nseIdx").value || 0);
const event = { kind: 1, created_at: Math.floor(Date.now()/1000), tags: [], content };
callVerb("nostr_sign_event", [event, { nostr_index: idx }], $("nseOut"));
});
$("nmeBtn").addEventListener("click", () => {
const content = $("nmeContent").value;
const idx = Number($("nmeIdx").value || 0);
const diff = Number($("nmeDiff").value || 4);
const timeout = Number($("nmeTimeout").value || 30);
const event = { kind: 1, created_at: Math.floor(Date.now()/1000), tags: [], content };
callVerb("nostr_mine_event", [event, { nostr_index: idx, difficulty: diff, timeout_sec: timeout }], $("nmeOut"));
});
const nip04Enc = () => {
const peer = $("nip04Peer").value.trim(), msg = $("nip04Msg").value, idx = Number($("nip04Idx").value || 0);
if (!peer) { $("nip04Out").textContent = "✗ enter peer pubkey"; return; }
callVerb("nostr_nip04_encrypt", [peer, msg, { nostr_index: idx }], $("nip04Out"));
};
const nip04Dec = () => {
const peer = $("nip04Peer").value.trim(), ct = $("nip04Cipher").value, idx = Number($("nip04Idx").value || 0);
if (!peer || !ct) { $("nip04Out").textContent = "✗ enter peer pubkey + ciphertext"; return; }
callVerb("nostr_nip04_decrypt", [peer, ct, { nostr_index: idx }], $("nip04Out"));
};
$("nip04EncBtn").addEventListener("click", nip04Enc);
$("nip04DecBtn").addEventListener("click", nip04Dec);
const nip44Enc = () => {
const peer = $("nip44Peer").value.trim(), msg = $("nip44Msg").value, idx = Number($("nip44Idx").value || 0);
if (!peer) { $("nip44Out").textContent = "✗ enter peer pubkey"; return; }
callVerb("nostr_nip44_encrypt", [peer, msg, { nostr_index: idx }], $("nip44Out"));
};
const nip44Dec = () => {
const peer = $("nip44Peer").value.trim(), ct = $("nip44Cipher").value, idx = Number($("nip44Idx").value || 0);
if (!peer || !ct) { $("nip44Out").textContent = "✗ enter peer pubkey + ciphertext"; return; }
callVerb("nostr_nip44_decrypt", [peer, ct, { nostr_index: idx }], $("nip44Out"));
};
$("nip44EncBtn").addEventListener("click", nip44Enc);
$("nip44DecBtn").addEventListener("click", nip44Dec);
$("otpEncBtn").addEventListener("click", () => {
const pt = $("otpPlain").value, enc = $("otpEnc").value;
callVerb("encrypt", [pt, { algorithm: "otp", encoding: enc }], $("otpOut"));
});
$("otpDecBtn").addEventListener("click", () => {
const ct = $("otpCipher").value, enc = $("otpEnc").value;
if (!ct) { $("otpOut").textContent = "✗ paste ciphertext first (from encrypt)"; return; }
callVerb("decrypt", [ct, { algorithm: "otp", encoding: enc }], $("otpOut"));
});
if (!("serial" in navigator)) {
log("Web Serial not supported in this browser. Use Chrome/Edge/Brave/Opera.");
connectBtn.disabled = true;
}
</script>
</body>
</html>