1562 lines
60 KiB
HTML
1562 lines
60 KiB
HTML
<!DOCTYPE html>
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<html lang="en" dir="ltr">
|
|
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>TOOLS</title>
|
|
|
|
<link rel="stylesheet" href="./css/client.css" />
|
|
|
|
<!-- Initialize theme BEFORE any components load -->
|
|
<script>
|
|
(function () {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme === 'dark') {
|
|
document.documentElement.classList.add('dark-mode');
|
|
if (document.body) {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
}
|
|
})();
|
|
</script>
|
|
<link rel="shortcut icon" type="image/x-icon" href="./favicon/favicon-dots2.ico" />
|
|
|
|
<!-- SVG.js library (required by HamburgerMorphing) -->
|
|
<script src="./js/vendor/svg.min.js"></script>
|
|
|
|
<style media="screen">
|
|
#divBody {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
gap: 20px;
|
|
}
|
|
|
|
.divTool {
|
|
border: var(--header-border);
|
|
border-radius: 20px;
|
|
margin: 10px;
|
|
padding: 20px;
|
|
width: 30%;
|
|
min-width: 27em;
|
|
background: var(--secondary-color);
|
|
}
|
|
|
|
.divButton {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
border: 2px solid var(--primary-color);
|
|
justify-content: center;
|
|
margin-top: 2em;
|
|
height: 4em;
|
|
border-radius: 10px;
|
|
background: transparent;
|
|
color: var(--primary-color);
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s, color 0.2s;
|
|
}
|
|
|
|
.divSideTitle {
|
|
font-weight: bold;
|
|
text-align: center;
|
|
color: var(--primary-color);
|
|
margin-bottom: 10px;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.divButton:hover {
|
|
background-color: var(--accent-color);
|
|
border-color: var(--accent-color);
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
.divButton:active {
|
|
background-color: var(--muted-color);
|
|
border-color: var(--muted-color);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.inDiv {
|
|
background-color: var(--secondary-color);
|
|
font-size: 55%;
|
|
border: 1px solid var(--primary-color);
|
|
min-height: 2em;
|
|
padding: 0.5em;
|
|
margin-bottom: 5px;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.inDivTall {
|
|
background-color: var(--secondary-color);
|
|
font-size: 55%;
|
|
border: 1px solid var(--primary-color);
|
|
min-height: 4em;
|
|
padding: 0.5em;
|
|
margin-bottom: 5px;
|
|
word-wrap: break-word;
|
|
white-space: pre-wrap;
|
|
overflow-y: auto;
|
|
max-height: 6em;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.outDiv {
|
|
background-color: var(--muted-color);
|
|
font-size: 55%;
|
|
border: 1px solid var(--primary-color);
|
|
min-height: 2em;
|
|
padding: 0.5em;
|
|
margin-bottom: 5px;
|
|
overflow: auto;
|
|
color: var(--primary-color);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.outDivTall {
|
|
background-color: var(--muted-color);
|
|
font-size: 55%;
|
|
border: 1px solid var(--primary-color);
|
|
min-height: 8em;
|
|
padding: 0.5em;
|
|
margin-bottom: 5px;
|
|
overflow: auto;
|
|
color: var(--primary-color);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.clsClipboard:hover {
|
|
opacity: 0.8;
|
|
background-color: var(--accent-color);
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
.clsClipboard:active {
|
|
background-color: var(--muted-color);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.toolLabel {
|
|
font-size: 0.9em;
|
|
color: var(--text-color);
|
|
margin-top: 8px;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.divButtonRow {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 1em;
|
|
}
|
|
|
|
.divButtonHalf {
|
|
flex: 1;
|
|
margin-top: 0;
|
|
}
|
|
|
|
.mnemonicJsonBox {
|
|
min-height: 2em;
|
|
max-height: 2em;
|
|
white-space: nowrap;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.mnemonicJsonBox.expanded {
|
|
min-height: 8em;
|
|
max-height: 12em;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
#selNip19EncodeType {
|
|
width: 100%;
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- ================================================================
|
|
HAMBURGER BUTTON (Fixed, separate from header)
|
|
================================================================ -->
|
|
<div id="divSvgHam" class="divHeaderButtons">
|
|
<!-- HamburgerMorphing will be injected here -->
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
HEADER
|
|
================================================================ -->
|
|
<div id="divHeader">
|
|
<div id="divHeaderFlexLeft">
|
|
<!-- Hamburger is now separate fixed element -->
|
|
</div>
|
|
|
|
<div id="divHeaderFlexCenter">
|
|
<div class="divHeaderText">TOOLS</div>
|
|
</div>
|
|
|
|
<div id="divHeaderFlexRight">
|
|
<!-- No button in header right - logout is in sidenav footer -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
BODY - TOOLS
|
|
================================================================ -->
|
|
<div id="divBody">
|
|
<!-- KEY GENERATION -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">GENERATE NEW IDENTITY</div>
|
|
<div class="toolLabel">Secret Key (hex):</div>
|
|
<div id="inpSK" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">Secret Key (nsec):</div>
|
|
<div id="inpSKBech32" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">Public Key (hex):</div>
|
|
<div id="inpPK" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">Public Key (npub):</div>
|
|
<div id="inpPKBech32" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">FIPS IPv6:</div>
|
|
<div id="inpPKFipsIpv6" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">JSON:</div>
|
|
<div id="inpAllKeys" class="outDivTall clsClipboard"> </div>
|
|
<div id="btnConnectPool" class="divButton btn">Generate</div>
|
|
</div>
|
|
|
|
<!-- NSEC TO NPUB -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">CONVERT NSEC TO NPUB</div>
|
|
<div class="toolLabel">nsec/hex:</div>
|
|
<div id="inpNsecToNpub" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel" id="labelNsecOut">nsec:</div>
|
|
<div id="outNsecOther" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">npub (encoded):</div>
|
|
<div id="outNpubEncoded" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">npub (hex):</div>
|
|
<div id="outNpubHex" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">JSON:</div>
|
|
<div id="outNsecToNpubJson" class="outDivTall clsClipboard"> </div>
|
|
</div>
|
|
|
|
<!-- CREATE EVENT -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">CREATE EVENT</div>
|
|
<div class="toolLabel">Secret Key:</div>
|
|
<div id="inpEventSK" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Kind:</div>
|
|
<div id="inpKind" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Tags:</div>
|
|
<div id="inpTags" class="inDiv" contenteditable="true">[]</div>
|
|
<div class="toolLabel">Content:</div>
|
|
<div id="inpContent" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Event:</div>
|
|
<div id="taEvent" class="outDiv clsClipboard" contenteditable="false"> </div>
|
|
<div id="btnCreateEvent" class="divButton btn">Create</div>
|
|
</div>
|
|
|
|
<!-- NPUB TO HEX -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">CONVERT NPUB TO HEX</div>
|
|
<div class="toolLabel">npub:</div>
|
|
<div id="inpNpub" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">hex:</div>
|
|
<div id="outHex1" class="outDiv clsClipboard"> </div>
|
|
</div>
|
|
|
|
<!-- NPUB TO FIPS IPV6 -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">CONVERT NPUB TO FIPS IPV6</div>
|
|
<div class="toolLabel">npub:</div>
|
|
<div id="inpNpubFipsIpv6" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">fips ipv6:</div>
|
|
<div id="outNpubFipsIpv6" class="outDiv clsClipboard"> </div>
|
|
</div>
|
|
|
|
<!-- NSEC TO HEX -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">CONVERT NSEC TO HEX</div>
|
|
<div class="toolLabel">nsec:</div>
|
|
<div id="inpNsec" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">hex:</div>
|
|
<div id="outHex2" class="outDiv clsClipboard"> </div>
|
|
</div>
|
|
|
|
<!-- HEX TO NPUB -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">CONVERT HEX TO NPUB</div>
|
|
<div class="toolLabel">hex:</div>
|
|
<div id="inpHex" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">npub:</div>
|
|
<div id="outNpub" class="outDiv clsClipboard"> </div>
|
|
</div>
|
|
|
|
<!-- HEX TO NSEC -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">CONVERT HEX TO NSEC</div>
|
|
<div class="toolLabel">hex:</div>
|
|
<div id="inpHex2" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">nsec:</div>
|
|
<div id="outNsec" class="outDiv clsClipboard"> </div>
|
|
</div>
|
|
|
|
<!-- ENCODE NIP-19 -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">ENCODE NIP-19</div>
|
|
<div class="toolLabel">Type:</div>
|
|
<select id="selNip19EncodeType" class="inDiv">
|
|
<option value="npub">npub</option>
|
|
<option value="nsec">nsec</option>
|
|
<option value="note">note</option>
|
|
<option value="nprofile">nprofile</option>
|
|
<option value="nevent">nevent</option>
|
|
<option value="naddr">naddr</option>
|
|
<option value="nrelay">nrelay</option>
|
|
<option value="other">other</option>
|
|
</select>
|
|
<div id="divNip19EncodeTypeCustomWrap" class="clsHidden">
|
|
<div class="toolLabel">Custom Type (used when Type is "other"):</div>
|
|
<div id="inpNip19EncodeTypeCustom" class="inDiv" contenteditable="true"></div>
|
|
</div>
|
|
<div class="toolLabel">Special Value (hex or identifier depending on type):</div>
|
|
<div id="inpNip19EncodeSpecial" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Relays (optional, comma/newline separated):</div>
|
|
<div id="inpNip19EncodeRelays" class="inDivTall" contenteditable="true"></div>
|
|
<div class="toolLabel">Author Pubkey Hex (optional, for nevent/naddr):</div>
|
|
<div id="inpNip19EncodeAuthor" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Kind (optional for nevent, required for naddr):</div>
|
|
<div id="inpNip19EncodeKind" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Encoded:</div>
|
|
<div id="outNip19Encoded" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">JSON:</div>
|
|
<div id="outNip19EncodeJson" class="outDivTall clsClipboard"> </div>
|
|
<div id="btnEncodeNip19" class="divButton btn">Encode</div>
|
|
</div>
|
|
|
|
<!-- DECODE NIP-19 -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">DECODE NIP-19</div>
|
|
<div class="toolLabel">Bech32 Input (npub, nsec, note, nprofile, nevent, naddr, nrelay):</div>
|
|
<div id="inpNip19DecodeValue" class="inDivTall" contenteditable="true"></div>
|
|
<div class="toolLabel">Type:</div>
|
|
<div id="outNip19DecodeType" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">Special Value:</div>
|
|
<div id="outNip19DecodeSpecial" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">JSON:</div>
|
|
<div id="outNip19DecodeJson" class="outDivTall clsClipboard"> </div>
|
|
<div id="btnDecodeNip19" class="divButton btn">Decode</div>
|
|
</div>
|
|
|
|
<!-- LINUX TIME -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">LINUX EPOCH TIME</div>
|
|
<div class="toolLabel">Seconds since 1970:</div>
|
|
<div id="outLinuxTime" class="outDiv clsClipboard"> </div>
|
|
</div>
|
|
|
|
<!-- GENERATE MNEMONIC PHRASE -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">GENERATE MNEMONIC PHRASE</div>
|
|
<div class="toolLabel">Phrase:</div>
|
|
<div id="outMnemonicPhrase" class="outDiv clsClipboard"> </div>
|
|
<div id="btnGenMnemonic" class="divButton btn">Generate</div>
|
|
</div>
|
|
|
|
<!-- DECODE MNEMONIC PHRASE -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">DECODE MNEMONIC PHRASE</div>
|
|
<div class="toolLabel">Phrase:</div>
|
|
<div id="inMnemonicPhrase" class="inDiv" contenteditable="true"></div>
|
|
|
|
<div id="lblMnemonicJson0" class="toolLabel">Index 00 JSON:</div>
|
|
<div id="outMnemonicJson0" class="outDivTall mnemonicJsonBox clsClipboard"> </div>
|
|
<div id="lblMnemonicJson1" class="toolLabel">Index 01 JSON:</div>
|
|
<div id="outMnemonicJson1" class="outDivTall mnemonicJsonBox clsClipboard"> </div>
|
|
<div id="lblMnemonicJson2" class="toolLabel">Index 02 JSON:</div>
|
|
<div id="outMnemonicJson2" class="outDivTall mnemonicJsonBox clsClipboard"> </div>
|
|
<div id="lblMnemonicJson3" class="toolLabel">Index 03 JSON:</div>
|
|
<div id="outMnemonicJson3" class="outDivTall mnemonicJsonBox clsClipboard"> </div>
|
|
<div id="lblMnemonicJson4" class="toolLabel">Index 04 JSON:</div>
|
|
<div id="outMnemonicJson4" class="outDivTall mnemonicJsonBox clsClipboard"> </div>
|
|
|
|
<div class="divButtonRow">
|
|
<div id="btnMnemonicPrev" class="divButton divButtonHalf btn">Previous</div>
|
|
<div id="btnMnemonicNext" class="divButton divButtonHalf btn">Next</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ENCODE NIP-04 -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">ENCODE NIP-04</div>
|
|
<div class="toolLabel">nsec/hex:</div>
|
|
<div id="inpNip04EncPrivkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">npub/hex:</div>
|
|
<div id="inpNip04EncPubkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Plain Text:</div>
|
|
<div id="inpNip04EncPlaintext" class="inDivTall" contenteditable="true"></div>
|
|
<div class="toolLabel">Encrypted Message:</div>
|
|
<div id="outNip04Encrypted" class="outDivTall clsClipboard"> </div>
|
|
<div id="btnEncodeNip04" class="divButton btn">Encrypt</div>
|
|
</div>
|
|
|
|
<!-- DECODE NIP-04 -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">DECODE NIP-04</div>
|
|
<div class="toolLabel">nsec/hex:</div>
|
|
<div id="inpNip04DecPrivkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">npub/hex:</div>
|
|
<div id="inpNip04DecPubkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Encrypted Message:</div>
|
|
<div id="inpNip04Encrypted" class="inDivTall" contenteditable="true"></div>
|
|
<div class="toolLabel">Plain Text:</div>
|
|
<div id="outNip04Decrypted" class="outDivTall clsClipboard"> </div>
|
|
<div id="btnDecodeNip04" class="divButton btn">Decrypt</div>
|
|
</div>
|
|
|
|
<!-- ENCODE NIP-44 -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">ENCODE NIP-44</div>
|
|
<div class="toolLabel">nsec/hex:</div>
|
|
<div id="inpNip44EncPrivkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">npub/hex:</div>
|
|
<div id="inpNip44EncPubkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Plain Text:</div>
|
|
<div id="inpNip44EncPlaintext" class="inDivTall" contenteditable="true"></div>
|
|
<div class="toolLabel">Encrypted Message:</div>
|
|
<div id="outNip44Encrypted" class="outDivTall clsClipboard"> </div>
|
|
<div id="btnEncodeNip44" class="divButton btn">Encrypt</div>
|
|
</div>
|
|
|
|
<!-- DECODE NIP-44 -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">DECODE NIP-44</div>
|
|
<div class="toolLabel">nsec/hex:</div>
|
|
<div id="inpNip44DecPrivkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">npub/hex:</div>
|
|
<div id="inpNip44DecPubkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Encrypted Message:</div>
|
|
<div id="inpNip44Encrypted" class="inDivTall" contenteditable="true"></div>
|
|
<div class="toolLabel">Plain Text:</div>
|
|
<div id="outNip44Decrypted" class="outDivTall clsClipboard"> </div>
|
|
<div id="btnDecodeNip44" class="divButton btn">Decrypt</div>
|
|
</div>
|
|
|
|
<!-- HMAC-SHA256 D TAG (deterministic opaque NIP-33 d tag from privkey + path) -->
|
|
<div class="divTool">
|
|
<div class="divSideTitle">HMAC-SHA256 D TAG</div>
|
|
<div class="toolLabel">nsec/hex (privkey):</div>
|
|
<div id="inpHmacDTagPrivkey" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Path (e.g. Work/Projects/Secret):</div>
|
|
<div id="inpHmacDTagPath" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">Key derivation label (optional, defaults to sovereign-browser/bookmarks-folder-id-v1):</div>
|
|
<div id="inpHmacDTagLabel" class="inDiv" contenteditable="true"></div>
|
|
<div class="toolLabel">HMAC key (hex, derived from privkey + label):</div>
|
|
<div id="outHmacDTagKey" class="outDiv clsClipboard"> </div>
|
|
<div class="toolLabel">d tag (HMAC-SHA256(hmac_key, path), 64 hex chars):</div>
|
|
<div id="outHmacDTag" class="outDiv clsClipboard"> </div>
|
|
<div id="btnComputeHmacDTag" class="divButton btn">Compute</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
FOOTER
|
|
================================================================ -->
|
|
<div id="divFooter">
|
|
<div id="divFooterLeft" class="divFooterBox"></div>
|
|
<div id="divFooterCenter" class="divFooterBox"></div>
|
|
<div id="divFooterRight" class="divFooterBox"></div>
|
|
<div id="divFooterBalance" class="divFooterBox">0 sats</div>
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
SIDENAV
|
|
================================================================ -->
|
|
<div id="divSideNav">
|
|
<div id="divSideNavHeader">
|
|
<!-- No close button - use main hamburger to close -->
|
|
</div>
|
|
|
|
<div id="divSideNavBody">
|
|
<div id="divFiles"></div>
|
|
</div>
|
|
|
|
<div id="divAiSection" class="sidenavSection">
|
|
<div id="divAiSectionTitle" class="sidenavSectionTitle">AI</div>
|
|
<div id="divAiList" class="sidenavSectionList">
|
|
<div id="divAiProvidersList">No saved providers yet.</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div id="divRelaySection">
|
|
<div id="divRelaySectionTitle">
|
|
リレー
|
|
</div>
|
|
<div id="divRelayList">
|
|
Loading relays...
|
|
</div>
|
|
</div>
|
|
|
|
<div id="divBlossomSection">
|
|
|
|
<div id="divBlossomSectionTitle">ブロッサム</div>
|
|
|
|
<div id="divBlossomList">Loading blossom servers...</div>
|
|
|
|
</div>
|
|
|
|
|
|
<div id="divVersionBar">
|
|
<span id="versionDisplay">v0.0.1</span>
|
|
<div id="divVersionBarButtons">
|
|
<button id="themeToggleButton" title="Toggle Dark/Light Mode">
|
|
<div id="themeToggleHamburgerContainer"></div>
|
|
</button>
|
|
<button id="logoutButton" title="Logout">
|
|
<div id="logoutHamburgerContainer"></div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
REQUIRED SCRIPTS
|
|
================================================================ -->
|
|
<script src="./nostr.bundle.js"></script>
|
|
<script src="/nostr-login-lite/nostr-lite.js"></script>
|
|
|
|
<script type="module">
|
|
/* ================================================================
|
|
IMPORTS
|
|
================================================================ */
|
|
import { initNDKPage, getPubkey, injectHeaderAvatar, injectHeaderLoginButton, subscribe, publishEvent, disconnect, getVersion, updateVersionDisplay } from './js/init-ndk.mjs';
|
|
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
|
|
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
|
|
|
|
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
|
|
|
|
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
|
|
const versionInfo = await getVersion();
|
|
const VERSION = versionInfo.VERSION;
|
|
console.log(`[tools.html ${VERSION}] Loading...`);
|
|
|
|
/* ================================================================
|
|
GLOBAL VARIABLES
|
|
================================================================ */
|
|
let updateIntervalId = null;
|
|
let currentPubkey = null;
|
|
let NostrTools = null;
|
|
|
|
// Hamburger menu
|
|
let hamburgerInstance = null;
|
|
let isNavOpen = false;
|
|
|
|
// Version bar buttons
|
|
let logoutHamburger = null;
|
|
let themeToggleHamburger = null;
|
|
let isDarkMode = false;
|
|
|
|
// Mnemonic decode pagination
|
|
const MNEMONIC_PAGE_SIZE = 5;
|
|
let mnemonicPageIndex = 0;
|
|
|
|
/* ================================================================
|
|
DOM VARIABLES
|
|
================================================================ */
|
|
const divBody = document.getElementById("divBody");
|
|
const divSideNav = document.getElementById("divSideNav");
|
|
const divSideNavBody = document.getElementById("divSideNavBody");
|
|
const divFooterCenter = document.getElementById("divFooterCenter");
|
|
const divFooterRight = document.getElementById("divFooterRight");
|
|
|
|
// Tool elements
|
|
const btnConnectPool = document.getElementById(`btnConnectPool`);
|
|
const btnCreateEvent = document.getElementById(`btnCreateEvent`);
|
|
const inpNpub = document.getElementById(`inpNpub`);
|
|
const inpNpubFipsIpv6 = document.getElementById(`inpNpubFipsIpv6`);
|
|
const inpNsec = document.getElementById(`inpNsec`);
|
|
const inpHex = document.getElementById(`inpHex`);
|
|
const inpHex2 = document.getElementById(`inpHex2`);
|
|
const outLinuxTime = document.getElementById(`outLinuxTime`);
|
|
const outMnemonicPhrase = document.getElementById(`outMnemonicPhrase`);
|
|
const inMnemonicPhrase = document.getElementById(`inMnemonicPhrase`);
|
|
|
|
/* ================================================================
|
|
HELPER: Wait for NostrTools
|
|
================================================================ */
|
|
async function waitForNostrTools(timeout = 5000) {
|
|
const startTime = Date.now();
|
|
while (!window.NostrTools && !window.NOSTR_LOGIN_LITE?.NostrTools) {
|
|
if (Date.now() - startTime > timeout) {
|
|
console.error('Available window properties:', Object.keys(window).filter(k => k.toLowerCase().includes('nostr')));
|
|
throw new Error('NostrTools failed to load within timeout');
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
return window.NostrTools || window.NOSTR_LOGIN_LITE?.NostrTools;
|
|
}
|
|
|
|
/* ================================================================
|
|
HAMBURGER MENU
|
|
================================================================ */
|
|
function initHamburgerMenu() {
|
|
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
hamburgerInstance.animateTo('burger');
|
|
}
|
|
|
|
/* ================================================================
|
|
SIDENAV FUNCTIONS
|
|
================================================================ */
|
|
function openNav() {
|
|
divSideNav.style.zIndex = 3;
|
|
divSideNav.style.width = "clamp(400px, 50vw, 600px)";
|
|
isNavOpen = true;
|
|
if (hamburgerInstance) {
|
|
hamburgerInstance.animateTo('arrow_left');
|
|
}
|
|
divSideNavBody.innerHTML = "Settings and options coming soon...";
|
|
|
|
// Initialize version bar buttons when sidenav opens (lazy load)
|
|
if (!logoutHamburger) {
|
|
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
|
|
size: 24,
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
logoutHamburger.animateTo('x');
|
|
}
|
|
|
|
if (!themeToggleHamburger) {
|
|
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
|
|
size: 24,
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
|
|
const savedTheme = localStorage.getItem('theme');
|
|
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
|
|
const initialShape = isDarkMode ? 'moon' : 'circle';
|
|
themeToggleHamburger.animateTo(initialShape);
|
|
}
|
|
}
|
|
|
|
function closeNav() {
|
|
divSideNav.style.width = "0vw";
|
|
divSideNav.style.zIndex = -1;
|
|
isNavOpen = false;
|
|
if (hamburgerInstance) {
|
|
hamburgerInstance.animateTo('burger');
|
|
}
|
|
}
|
|
|
|
function toggleNav() {
|
|
if (isNavOpen) {
|
|
closeNav();
|
|
} else {
|
|
openNav();
|
|
}
|
|
}
|
|
|
|
/* ================================================================
|
|
UPDATE FOOTER
|
|
================================================================ */
|
|
const UpdateFooter = async () => {
|
|
try {
|
|
await updateFooterRelayStatus();
|
|
await updateSidenavRelaySection();
|
|
|
|
await updateBlossomSection();
|
|
// Update Linux time
|
|
if (outLinuxTime) {
|
|
outLinuxTime.innerText = Math.floor(Date.now() / 1000);
|
|
}
|
|
|
|
divFooterCenter.innerHTML = '';
|
|
divFooterRight.innerHTML = '';
|
|
} catch (error) {
|
|
console.error('[tools.html] Error updating footer:', error);
|
|
}
|
|
};
|
|
|
|
/* ================================================================
|
|
LOGOUT
|
|
================================================================ */
|
|
const Logout = async () => {
|
|
console.log("[tools.html] Starting logout process...");
|
|
|
|
if (updateIntervalId) {
|
|
clearInterval(updateIntervalId);
|
|
updateIntervalId = null;
|
|
}
|
|
|
|
disconnect();
|
|
|
|
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
|
|
await window.NOSTR_LOGIN_LITE.logout();
|
|
}
|
|
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
|
|
if (window.indexedDB) {
|
|
const databases = await window.indexedDB.databases();
|
|
for (const db of databases) {
|
|
if (db.name) {
|
|
window.indexedDB.deleteDatabase(db.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log("[tools.html] Logged out, reloading page");
|
|
location.reload(true);
|
|
};
|
|
|
|
/* ================================================================
|
|
TOOL FUNCTIONS
|
|
================================================================ */
|
|
|
|
// Helper function to convert hex string to Uint8Array
|
|
function hexToUint8Array(hex) {
|
|
const bytes = new Uint8Array(hex.length / 2);
|
|
for (let i = 0; i < hex.length; i += 2) {
|
|
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
// Helper function to convert Uint8Array to hex string
|
|
function uint8ArrayToHex(bytes) {
|
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
// Helper function to normalize key input (accepts both hex and bech32)
|
|
const normalizeKey = (keyInput, isPrivate = false) => {
|
|
const trimmed = keyInput.trim();
|
|
if (!trimmed) return null;
|
|
|
|
try {
|
|
if (isPrivate) {
|
|
if (trimmed.startsWith('nsec')) {
|
|
return NostrTools.nip19.decode(trimmed).data;
|
|
} else {
|
|
return hexToUint8Array(trimmed);
|
|
}
|
|
} else {
|
|
if (trimmed.startsWith('npub')) {
|
|
return NostrTools.nip19.decode(trimmed).data;
|
|
} else {
|
|
return trimmed;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`Invalid ${isPrivate ? 'private' : 'public'} key format: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// Helper function to produce NIP-06 style derivation path string
|
|
const getDerivationPath = (accountIndex = 0) => `m/44'/1237'/${accountIndex}'/0/0`;
|
|
|
|
const pubkeyHexToFipsIpv6 = async (pubkeyHex) => {
|
|
const pubkeyBytes = hexToUint8Array(pubkeyHex);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', pubkeyBytes);
|
|
const hash = new Uint8Array(hashBuffer);
|
|
|
|
const nodeAddr = hash.slice(0, 16);
|
|
nodeAddr[0] = 0xfd;
|
|
|
|
const ipv6Groups = [];
|
|
for (let i = 0; i < 16; i += 2) {
|
|
const group = ((nodeAddr[i] << 8) | nodeAddr[i + 1]).toString(16);
|
|
ipv6Groups.push(group);
|
|
}
|
|
|
|
return ipv6Groups.join(':');
|
|
};
|
|
|
|
const GenerateKeys = async () => {
|
|
let sk = NostrTools.generateSecretKey();
|
|
let pk = NostrTools.getPublicKey(sk);
|
|
let nsec = NostrTools.nip19.nsecEncode(sk);
|
|
let npub = NostrTools.nip19.npubEncode(pk);
|
|
|
|
let skHex = Array.from(sk).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
let fipsIpv6 = await pubkeyHexToFipsIpv6(pk);
|
|
|
|
document.getElementById(`inpSK`).innerText = skHex;
|
|
document.getElementById(`inpPK`).innerText = pk;
|
|
document.getElementById(`inpSKBech32`).innerText = nsec;
|
|
document.getElementById(`inpPKBech32`).innerText = npub;
|
|
document.getElementById(`inpPKFipsIpv6`).innerText = fipsIpv6;
|
|
let nAll = {}
|
|
nAll.nsec = nsec
|
|
nAll.npub = npub
|
|
nAll.npubHex = pk
|
|
nAll.nsecHex = skHex
|
|
nAll.fipsIpv6 = fipsIpv6
|
|
nAll.strDerivationPath = getDerivationPath(0)
|
|
document.getElementById(`inpAllKeys`).innerText = JSON.stringify(nAll, "", 2);
|
|
};
|
|
|
|
const CreateEvent = async () => {
|
|
console.log("Create Event");
|
|
let arrInp = JSON.parse(document.getElementById(`inpTags`).innerText || '[]');
|
|
let Temp = await objCreateEvent(
|
|
document.getElementById(`inpEventSK`).innerText,
|
|
document.getElementById(`inpKind`).innerText,
|
|
document.getElementById(`inpContent`).innerText,
|
|
arrInp
|
|
);
|
|
document.getElementById(`taEvent`).innerText = JSON.stringify(Temp, undefined, 4);
|
|
};
|
|
|
|
const objCreateEvent = async (PrivateKey, Kind, Content, arrTags = []) => {
|
|
let privateKeyBytes;
|
|
if (typeof PrivateKey === 'string') {
|
|
if (PrivateKey.startsWith('nsec')) {
|
|
privateKeyBytes = NostrTools.nip19.decode(PrivateKey).data;
|
|
} else {
|
|
privateKeyBytes = hexToUint8Array(PrivateKey);
|
|
}
|
|
} else {
|
|
privateKeyBytes = PrivateKey;
|
|
}
|
|
|
|
let event = {
|
|
kind: Number(Kind),
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: arrTags,
|
|
content: Content,
|
|
pubkey: NostrTools.getPublicKey(privateKeyBytes),
|
|
};
|
|
|
|
event.id = NostrTools.getEventHash(event);
|
|
event.sig = NostrTools.finalizeEvent(event, privateKeyBytes).sig;
|
|
|
|
return event;
|
|
};
|
|
|
|
const NpubToHex = async () => {
|
|
try {
|
|
let Hex = NostrTools.nip19.decode(document.getElementById(`inpNpub`).innerText).data;
|
|
document.getElementById(`outHex1`).innerText = Hex;
|
|
} catch (error) {
|
|
document.getElementById(`outHex1`).innerText = 'Error: Invalid npub';
|
|
}
|
|
};
|
|
|
|
const NpubToFipsIpv6 = async () => {
|
|
try {
|
|
const input = document.getElementById(`inpNpubFipsIpv6`).innerText.trim();
|
|
|
|
if (!input) {
|
|
document.getElementById(`outNpubFipsIpv6`).innerText = '';
|
|
return;
|
|
}
|
|
|
|
const decoded = NostrTools.nip19.decode(input);
|
|
if (decoded.type !== 'npub') {
|
|
throw new Error('Input must be npub');
|
|
}
|
|
|
|
const pubkeyHex = decoded.data;
|
|
const fipsIpv6 = await pubkeyHexToFipsIpv6(pubkeyHex);
|
|
document.getElementById(`outNpubFipsIpv6`).innerText = fipsIpv6;
|
|
} catch (error) {
|
|
document.getElementById(`outNpubFipsIpv6`).innerText = 'Error: Invalid npub';
|
|
}
|
|
};
|
|
|
|
const NsecToHex = async () => {
|
|
try {
|
|
let decoded = NostrTools.nip19.decode(document.getElementById(`inpNsec`).innerText).data;
|
|
let Hex = Array.from(decoded).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
document.getElementById(`outHex2`).innerText = Hex;
|
|
} catch (error) {
|
|
document.getElementById(`outHex2`).innerText = 'Error: Invalid nsec';
|
|
}
|
|
};
|
|
|
|
const HexToNpub = async () => {
|
|
try {
|
|
let hex = document.getElementById(`inpHex`).innerText.trim();
|
|
let npub = NostrTools.nip19.npubEncode(hex);
|
|
document.getElementById(`outNpub`).innerText = npub;
|
|
} catch (error) {
|
|
document.getElementById(`outNpub`).innerText = 'Error: Invalid hex';
|
|
}
|
|
};
|
|
|
|
const HexToNsec = async () => {
|
|
try {
|
|
let hex = document.getElementById(`inpHex2`).innerText.trim();
|
|
let bytes = hexToUint8Array(hex);
|
|
let nsec = NostrTools.nip19.nsecEncode(bytes);
|
|
document.getElementById(`outNsec`).innerText = nsec;
|
|
} catch (error) {
|
|
document.getElementById(`outNsec`).innerText = 'Error: Invalid hex';
|
|
}
|
|
};
|
|
|
|
const GenerateMnemonic = async () => {
|
|
let sw = NostrTools.nip06.generateSeedWords();
|
|
document.getElementById(`outMnemonicPhrase`).innerText = sw;
|
|
document.getElementById(`inMnemonicPhrase`).innerText = sw;
|
|
DecodeMnemonic(true);
|
|
};
|
|
|
|
const updateMnemonicPaginationState = () => {
|
|
const btnMnemonicPrev = document.getElementById(`btnMnemonicPrev`);
|
|
if (btnMnemonicPrev) {
|
|
const disabled = mnemonicPageIndex <= 0;
|
|
btnMnemonicPrev.style.opacity = disabled ? '0.5' : '1';
|
|
btnMnemonicPrev.style.pointerEvents = disabled ? 'none' : 'auto';
|
|
}
|
|
};
|
|
|
|
const setMnemonicBoxesExpanded = (expanded) => {
|
|
for (let i = 0; i < MNEMONIC_PAGE_SIZE; i++) {
|
|
const box = document.getElementById(`outMnemonicJson${i}`);
|
|
if (!box) continue;
|
|
box.classList.toggle('expanded', expanded);
|
|
}
|
|
};
|
|
|
|
const DecodeMnemonic = async (resetToFirstPage = false) => {
|
|
const mnemonicInput = document.getElementById(`inMnemonicPhrase`);
|
|
const mnemonic = mnemonicInput.innerText.trim();
|
|
mnemonicInput.innerText = mnemonic;
|
|
|
|
if (resetToFirstPage) {
|
|
mnemonicPageIndex = 0;
|
|
}
|
|
|
|
try {
|
|
if (!mnemonic) {
|
|
setMnemonicBoxesExpanded(false);
|
|
for (let i = 0; i < MNEMONIC_PAGE_SIZE; i++) {
|
|
const keyIndex = (mnemonicPageIndex * MNEMONIC_PAGE_SIZE) + i;
|
|
const label = document.getElementById(`lblMnemonicJson${i}`);
|
|
if (label) label.innerText = `Index ${String(keyIndex).padStart(2, '0')} JSON:`;
|
|
document.getElementById(`outMnemonicJson${i}`).innerText = '';
|
|
}
|
|
updateMnemonicPaginationState();
|
|
return;
|
|
}
|
|
|
|
setMnemonicBoxesExpanded(true);
|
|
|
|
for (let i = 0; i < MNEMONIC_PAGE_SIZE; i++) {
|
|
const keyIndex = (mnemonicPageIndex * MNEMONIC_PAGE_SIZE) + i;
|
|
const label = document.getElementById(`lblMnemonicJson${i}`);
|
|
if (label) label.innerText = `Index ${String(keyIndex).padStart(2, '0')} JSON:`;
|
|
|
|
const privKey = NostrTools.nip06.privateKeyFromSeedWords(mnemonic, "", keyIndex, 0, 0);
|
|
const nsec = NostrTools.nip19.nsecEncode(privKey);
|
|
const nsecHex = Array.from(privKey).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
const npubHex = NostrTools.getPublicKey(privKey);
|
|
const npub = NostrTools.nip19.npubEncode(npubHex);
|
|
const fipsIpv6 = await pubkeyHexToFipsIpv6(npubHex);
|
|
|
|
const outJson = {
|
|
index: keyIndex,
|
|
nsec,
|
|
npub,
|
|
npubHex,
|
|
nsecHex,
|
|
fipsIpv6,
|
|
strDerivationPath: getDerivationPath(keyIndex)
|
|
};
|
|
|
|
document.getElementById(`outMnemonicJson${i}`).innerText = JSON.stringify(outJson, "", 2);
|
|
}
|
|
} catch (error) {
|
|
setMnemonicBoxesExpanded(true);
|
|
for (let i = 0; i < MNEMONIC_PAGE_SIZE; i++) {
|
|
const keyIndex = (mnemonicPageIndex * MNEMONIC_PAGE_SIZE) + i;
|
|
const label = document.getElementById(`lblMnemonicJson${i}`);
|
|
if (label) label.innerText = `Index ${String(keyIndex).padStart(2, '0')} JSON:`;
|
|
document.getElementById(`outMnemonicJson${i}`).innerText = 'Error';
|
|
}
|
|
}
|
|
|
|
updateMnemonicPaginationState();
|
|
};
|
|
|
|
// NIP-04 Encryption
|
|
const EncodeNip04 = async () => {
|
|
try {
|
|
const privateKeyInput = document.getElementById(`inpNip04EncPrivkey`).innerText.trim();
|
|
const publicKeyInput = document.getElementById(`inpNip04EncPubkey`).innerText.trim();
|
|
const plaintext = document.getElementById(`inpNip04EncPlaintext`).innerText.trim();
|
|
|
|
if (!privateKeyInput || !publicKeyInput || !plaintext) {
|
|
document.getElementById(`outNip04Encrypted`).innerText = "Error: Please fill all fields";
|
|
return;
|
|
}
|
|
|
|
const privateKey = normalizeKey(privateKeyInput, true);
|
|
const publicKey = normalizeKey(publicKeyInput, false);
|
|
|
|
const encrypted = await NostrTools.nip04.encrypt(privateKey, publicKey, plaintext);
|
|
document.getElementById(`outNip04Encrypted`).innerText = encrypted;
|
|
} catch (error) {
|
|
console.error("NIP-04 encryption error:", error);
|
|
document.getElementById(`outNip04Encrypted`).innerText = `Error: ${error.message}`;
|
|
}
|
|
};
|
|
|
|
// NIP-04 Decryption
|
|
const DecodeNip04 = async () => {
|
|
try {
|
|
const privateKeyInput = document.getElementById(`inpNip04DecPrivkey`).innerText.trim();
|
|
const publicKeyInput = document.getElementById(`inpNip04DecPubkey`).innerText.trim();
|
|
const encrypted = document.getElementById(`inpNip04Encrypted`).innerText.trim();
|
|
|
|
if (!privateKeyInput || !publicKeyInput || !encrypted) {
|
|
document.getElementById(`outNip04Decrypted`).innerText = "Error: Please fill all fields";
|
|
return;
|
|
}
|
|
|
|
const privateKey = normalizeKey(privateKeyInput, true);
|
|
const publicKey = normalizeKey(publicKeyInput, false);
|
|
|
|
const decrypted = await NostrTools.nip04.decrypt(privateKey, publicKey, encrypted);
|
|
document.getElementById(`outNip04Decrypted`).innerText = decrypted;
|
|
} catch (error) {
|
|
console.error("NIP-04 decryption error:", error);
|
|
document.getElementById(`outNip04Decrypted`).innerText = `Error: ${error.message}`;
|
|
}
|
|
};
|
|
|
|
// NIP-44 Encryption
|
|
const EncodeNip44 = async () => {
|
|
try {
|
|
const privateKeyInput = document.getElementById(`inpNip44EncPrivkey`).innerText.trim();
|
|
const publicKeyInput = document.getElementById(`inpNip44EncPubkey`).innerText.trim();
|
|
const plaintext = document.getElementById(`inpNip44EncPlaintext`).innerText.trim();
|
|
|
|
if (!privateKeyInput || !publicKeyInput || !plaintext) {
|
|
document.getElementById(`outNip44Encrypted`).innerText = "Error: Please fill all fields";
|
|
return;
|
|
}
|
|
|
|
const privateKeyBytes = normalizeKey(privateKeyInput, true);
|
|
const publicKey = normalizeKey(publicKeyInput, false);
|
|
|
|
const conversationKey = NostrTools.nip44.getConversationKey(privateKeyBytes, publicKey);
|
|
const encrypted = NostrTools.nip44.encrypt(plaintext, conversationKey);
|
|
document.getElementById(`outNip44Encrypted`).innerText = encrypted;
|
|
} catch (error) {
|
|
console.error("NIP-44 encryption error:", error);
|
|
document.getElementById(`outNip44Encrypted`).innerText = `Error: ${error.message}`;
|
|
}
|
|
};
|
|
|
|
// NIP-44 Decryption
|
|
const DecodeNip44 = async () => {
|
|
try {
|
|
const privateKeyInput = document.getElementById(`inpNip44DecPrivkey`).innerText.trim();
|
|
const publicKeyInput = document.getElementById(`inpNip44DecPubkey`).innerText.trim();
|
|
const encrypted = document.getElementById(`inpNip44Encrypted`).innerText.trim()
|
|
.replace(/\s+/g, '')
|
|
.replace(/\u200B/g, '');
|
|
|
|
if (!privateKeyInput || !publicKeyInput || !encrypted) {
|
|
document.getElementById(`outNip44Decrypted`).innerText = "Error: Please fill all fields";
|
|
return;
|
|
}
|
|
|
|
const privateKeyBytes = normalizeKey(privateKeyInput, true);
|
|
const publicKey = normalizeKey(publicKeyInput, false);
|
|
|
|
const conversationKey = NostrTools.nip44.getConversationKey(privateKeyBytes, publicKey);
|
|
const decrypted = NostrTools.nip44.decrypt(encrypted, conversationKey);
|
|
document.getElementById(`outNip44Decrypted`).innerText = decrypted;
|
|
} catch (error) {
|
|
console.error("NIP-44 decryption error:", error);
|
|
document.getElementById(`outNip44Decrypted`).innerText = `Error: ${error.message}`;
|
|
}
|
|
};
|
|
|
|
// HMAC-SHA256 deterministic opaque d tag (for NIP-33 parameterized replaceable
|
|
// events where the d tag must be (a) opaque to observers and (b) identical
|
|
// across re-publishes so relays replace the prior event. Encryption (NIP-04 /
|
|
// NIP-44) cannot be used because both use a random IV/nonce per call, which
|
|
// would break replaceability. A MAC is deterministic by construction.
|
|
//
|
|
// hmac_key = HMAC-SHA256(privkey_bytes, label_utf8)
|
|
// d = HMAC-SHA256(hmac_key, path_utf8) → 64 hex chars
|
|
//
|
|
// The same privkey + label + path always yields the same d tag, so relays
|
|
// replace the prior event. Observers see only an opaque hash and learn
|
|
// nothing about the path. Used by sovereign_browser for nested bookmark
|
|
// folders (NIP-51 kind 30003) where the real path lives inside the
|
|
// NIP-44 encrypted content.
|
|
const ComputeHmacDTag = async () => {
|
|
try {
|
|
const privkeyInput = document.getElementById(`inpHmacDTagPrivkey`).innerText.trim();
|
|
const path = document.getElementById(`inpHmacDTagPath`).innerText;
|
|
let label = document.getElementById(`inpHmacDTagLabel`).innerText.trim();
|
|
if (!label) label = 'sovereign-browser/bookmarks-folder-id-v1';
|
|
|
|
if (!privkeyInput) {
|
|
document.getElementById(`outHmacDTagKey`).innerText = 'Error: privkey is required';
|
|
document.getElementById(`outHmacDTag`).innerText = '';
|
|
return;
|
|
}
|
|
if (!path) {
|
|
document.getElementById(`outHmacDTagKey`).innerText = '';
|
|
document.getElementById(`outHmacDTag`).innerText = 'Error: path is required';
|
|
return;
|
|
}
|
|
|
|
const privkeyBytes = normalizeKey(privkeyInput, true);
|
|
|
|
// hmac_key = HMAC-SHA256(privkey, label)
|
|
const labelBytes = new TextEncoder().encode(label);
|
|
const hmacKeyBuf = await crypto.subtle.importKey(
|
|
'raw', privkeyBytes,
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false, ['sign']
|
|
);
|
|
const hmacKeySig = await crypto.subtle.sign('HMAC', hmacKeyBuf, labelBytes);
|
|
const hmacKeyHex = Array.from(new Uint8Array(hmacKeySig))
|
|
.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
document.getElementById(`outHmacDTagKey`).innerText = hmacKeyHex;
|
|
|
|
// d = HMAC-SHA256(hmac_key, path)
|
|
const hmacKeyBytes = new Uint8Array(hmacKeySig);
|
|
const pathBytes = new TextEncoder().encode(path);
|
|
const dKeyBuf = await crypto.subtle.importKey(
|
|
'raw', hmacKeyBytes,
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false, ['sign']
|
|
);
|
|
const dSig = await crypto.subtle.sign('HMAC', dKeyBuf, pathBytes);
|
|
const dHex = Array.from(new Uint8Array(dSig))
|
|
.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
document.getElementById(`outHmacDTag`).innerText = dHex;
|
|
} catch (error) {
|
|
console.error('HMAC d-tag error:', error);
|
|
document.getElementById(`outHmacDTagKey`).innerText = `Error: ${error.message}`;
|
|
document.getElementById(`outHmacDTag`).innerText = '';
|
|
}
|
|
};
|
|
|
|
// NSEC to NPUB Conversion
|
|
const NsecToNpub = async () => {
|
|
try {
|
|
let input = document.getElementById('inpNsecToNpub').innerText.trim();
|
|
|
|
if (!input) {
|
|
document.getElementById('outNpubEncoded').innerText = '';
|
|
document.getElementById('outNpubHex').innerText = '';
|
|
document.getElementById('labelNsecOut').innerText = 'nsec:';
|
|
document.getElementById('outNsecOther').innerText = '';
|
|
document.getElementById('outNsecToNpubJson').innerText = '';
|
|
return;
|
|
}
|
|
|
|
let privateKeyHex;
|
|
let nsecEncoded;
|
|
let nsecHex;
|
|
|
|
if (input.startsWith('nsec')) {
|
|
privateKeyHex = NostrTools.nip19.decode(input).data;
|
|
nsecEncoded = input;
|
|
nsecHex = Array.from(privateKeyHex).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
document.getElementById('labelNsecOut').innerText = 'nsec (hex):';
|
|
document.getElementById('outNsecOther').innerText = nsecHex;
|
|
} else {
|
|
privateKeyHex = hexToUint8Array(input);
|
|
nsecHex = input;
|
|
nsecEncoded = NostrTools.nip19.nsecEncode(privateKeyHex);
|
|
document.getElementById('labelNsecOut').innerText = 'nsec (encoded):';
|
|
document.getElementById('outNsecOther').innerText = nsecEncoded;
|
|
}
|
|
|
|
let publicKeyHex = NostrTools.getPublicKey(privateKeyHex);
|
|
let npubEncoded = NostrTools.nip19.npubEncode(publicKeyHex);
|
|
|
|
document.getElementById('outNpubEncoded').innerText = npubEncoded;
|
|
document.getElementById('outNpubHex').innerText = publicKeyHex;
|
|
|
|
let jsonOutput = {
|
|
nsec: nsecEncoded,
|
|
npub: npubEncoded,
|
|
npubHex: publicKeyHex,
|
|
nsecHex: nsecHex
|
|
};
|
|
document.getElementById('outNsecToNpubJson').innerText = JSON.stringify(jsonOutput, "", 2);
|
|
} catch (error) {
|
|
console.error("NSEC to NPUB conversion error:", error);
|
|
document.getElementById('outNpubEncoded').innerText = 'Error: Invalid input';
|
|
document.getElementById('outNpubHex').innerText = 'Error: Invalid input';
|
|
document.getElementById('labelNsecOut').innerText = 'nsec:';
|
|
document.getElementById('outNsecOther').innerText = 'Error: Invalid input';
|
|
document.getElementById('outNsecToNpubJson').innerText = 'Error: Invalid input';
|
|
}
|
|
};
|
|
|
|
const UpdateNip19CustomTypeVisibility = () => {
|
|
const selNip19EncodeType = document.getElementById('selNip19EncodeType');
|
|
const divNip19EncodeTypeCustomWrap = document.getElementById('divNip19EncodeTypeCustomWrap');
|
|
if (!selNip19EncodeType || !divNip19EncodeTypeCustomWrap) return;
|
|
|
|
if (selNip19EncodeType.value === 'other') {
|
|
divNip19EncodeTypeCustomWrap.classList.remove('clsHidden');
|
|
} else {
|
|
divNip19EncodeTypeCustomWrap.classList.add('clsHidden');
|
|
}
|
|
};
|
|
|
|
// NIP-19 Encode
|
|
const EncodeNip19 = async () => {
|
|
try {
|
|
const selectedType = document.getElementById('selNip19EncodeType').value.trim().toLowerCase();
|
|
const customTypeRaw = document.getElementById('inpNip19EncodeTypeCustom').innerText.trim().toLowerCase();
|
|
const typeRaw = selectedType === 'other' ? customTypeRaw : selectedType;
|
|
const specialRaw = document.getElementById('inpNip19EncodeSpecial').innerText.trim();
|
|
const relaysRaw = document.getElementById('inpNip19EncodeRelays').innerText.trim();
|
|
const authorRaw = document.getElementById('inpNip19EncodeAuthor').innerText.trim();
|
|
const kindRaw = document.getElementById('inpNip19EncodeKind').innerText.trim();
|
|
|
|
if (!typeRaw) {
|
|
document.getElementById('outNip19Encoded').innerText = 'Error: Missing type';
|
|
document.getElementById('outNip19EncodeJson').innerText = 'Error: Missing type';
|
|
return;
|
|
}
|
|
|
|
const relays = relaysRaw
|
|
? relaysRaw
|
|
.split(/\n|,/)
|
|
.map(v => v.trim())
|
|
.filter(Boolean)
|
|
: [];
|
|
|
|
let encoded = '';
|
|
let jsonOutput = {
|
|
type: typeRaw,
|
|
selectedType
|
|
};
|
|
|
|
if (typeRaw === 'npub') {
|
|
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('npub requires 32-byte hex pubkey');
|
|
encoded = NostrTools.nip19.npubEncode(specialRaw);
|
|
jsonOutput.pubkey = specialRaw;
|
|
} else if (typeRaw === 'nsec') {
|
|
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('nsec requires 32-byte hex private key');
|
|
encoded = NostrTools.nip19.nsecEncode(hexToUint8Array(specialRaw));
|
|
jsonOutput.seckey = specialRaw;
|
|
} else if (typeRaw === 'note') {
|
|
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('note requires 32-byte hex event id');
|
|
encoded = NostrTools.nip19.noteEncode(specialRaw);
|
|
jsonOutput.id = specialRaw;
|
|
} else if (typeRaw === 'nprofile') {
|
|
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('nprofile requires 32-byte hex pubkey as special value');
|
|
const payload = { pubkey: specialRaw };
|
|
if (relays.length) payload.relays = relays;
|
|
encoded = NostrTools.nip19.nprofileEncode(payload);
|
|
jsonOutput.payload = payload;
|
|
} else if (typeRaw === 'nevent') {
|
|
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('nevent requires 32-byte hex event id as special value');
|
|
const payload = { id: specialRaw };
|
|
if (relays.length) payload.relays = relays;
|
|
if (authorRaw) {
|
|
if (!/^[a-fA-F0-9]{64}$/.test(authorRaw)) throw new Error('author must be 32-byte hex pubkey');
|
|
payload.author = authorRaw;
|
|
}
|
|
if (kindRaw) {
|
|
const kind = Number(kindRaw);
|
|
if (!Number.isInteger(kind) || kind < 0) throw new Error('kind must be a non-negative integer');
|
|
payload.kind = kind;
|
|
}
|
|
encoded = NostrTools.nip19.neventEncode(payload);
|
|
jsonOutput.payload = payload;
|
|
} else if (typeRaw === 'naddr') {
|
|
if (!authorRaw) throw new Error('naddr requires author pubkey hex');
|
|
if (!/^[a-fA-F0-9]{64}$/.test(authorRaw)) throw new Error('author must be 32-byte hex pubkey');
|
|
const kind = Number(kindRaw);
|
|
if (!Number.isInteger(kind) || kind < 0) throw new Error('naddr requires a non-negative integer kind');
|
|
const payload = {
|
|
identifier: specialRaw,
|
|
pubkey: authorRaw,
|
|
kind
|
|
};
|
|
if (relays.length) payload.relays = relays;
|
|
encoded = NostrTools.nip19.naddrEncode(payload);
|
|
jsonOutput.payload = payload;
|
|
} else if (typeRaw === 'nrelay') {
|
|
if (!specialRaw) throw new Error('nrelay requires relay URL as special value');
|
|
encoded = NostrTools.nip19.nrelayEncode(specialRaw);
|
|
jsonOutput.relay = specialRaw;
|
|
} else {
|
|
throw new Error('Unsupported type, use npub, nsec, note, nprofile, nevent, naddr, nrelay, or choose other with a supported custom value');
|
|
}
|
|
|
|
jsonOutput.encoded = encoded;
|
|
document.getElementById('outNip19Encoded').innerText = encoded;
|
|
document.getElementById('outNip19EncodeJson').innerText = JSON.stringify(jsonOutput, '', 2);
|
|
} catch (error) {
|
|
console.error('NIP-19 encode error:', error);
|
|
document.getElementById('outNip19Encoded').innerText = `Error: ${error.message}`;
|
|
document.getElementById('outNip19EncodeJson').innerText = `Error: ${error.message}`;
|
|
}
|
|
};
|
|
|
|
// NIP-19 Decode
|
|
const DecodeNip19 = async () => {
|
|
try {
|
|
const value = document.getElementById('inpNip19DecodeValue').innerText.trim();
|
|
if (!value) {
|
|
document.getElementById('outNip19DecodeType').innerText = '';
|
|
document.getElementById('outNip19DecodeSpecial').innerText = '';
|
|
document.getElementById('outNip19DecodeJson').innerText = '';
|
|
return;
|
|
}
|
|
|
|
const decoded = NostrTools.nip19.decode(value);
|
|
const type = decoded.type;
|
|
const data = decoded.data;
|
|
|
|
let special = '';
|
|
let outputData = data;
|
|
|
|
if (type === 'nsec' && data instanceof Uint8Array) {
|
|
special = uint8ArrayToHex(data);
|
|
outputData = {
|
|
nsecHex: special
|
|
};
|
|
} else if (type === 'npub' || type === 'note') {
|
|
special = data;
|
|
} else if (type === 'nprofile') {
|
|
special = data.pubkey || '';
|
|
} else if (type === 'nevent') {
|
|
special = data.id || '';
|
|
} else if (type === 'naddr') {
|
|
special = data.identifier || '';
|
|
} else if (type === 'nrelay') {
|
|
special = data;
|
|
}
|
|
|
|
document.getElementById('outNip19DecodeType').innerText = type;
|
|
document.getElementById('outNip19DecodeSpecial').innerText = special;
|
|
document.getElementById('outNip19DecodeJson').innerText = JSON.stringify({ type, data: outputData }, '', 2);
|
|
} catch (error) {
|
|
console.error('NIP-19 decode error:', error);
|
|
document.getElementById('outNip19DecodeType').innerText = 'Error';
|
|
document.getElementById('outNip19DecodeSpecial').innerText = `Error: ${error.message}`;
|
|
document.getElementById('outNip19DecodeJson').innerText = `Error: ${error.message}`;
|
|
}
|
|
};
|
|
|
|
/* ================================================================
|
|
EVENT LISTENERS
|
|
================================================================ */
|
|
function setupEventListeners() {
|
|
// Hamburger
|
|
const divSvgHam = document.getElementById('divSvgHam');
|
|
if (divSvgHam) {
|
|
divSvgHam.addEventListener('click', toggleNav);
|
|
}
|
|
|
|
// Tool buttons
|
|
if (btnConnectPool) btnConnectPool.addEventListener("click", GenerateKeys);
|
|
if (btnCreateEvent) btnCreateEvent.addEventListener("click", CreateEvent);
|
|
if (inpNpub) inpNpub.addEventListener("input", NpubToHex);
|
|
if (inpNpubFipsIpv6) inpNpubFipsIpv6.addEventListener("input", NpubToFipsIpv6);
|
|
if (inpNsec) inpNsec.addEventListener("input", NsecToHex);
|
|
if (inpHex) inpHex.addEventListener("input", HexToNpub);
|
|
if (inpHex2) inpHex2.addEventListener("input", HexToNsec);
|
|
|
|
const btnGenMnemonic = document.getElementById(`btnGenMnemonic`);
|
|
const btnMnemonicPrev = document.getElementById(`btnMnemonicPrev`);
|
|
const btnMnemonicNext = document.getElementById(`btnMnemonicNext`);
|
|
|
|
if (btnGenMnemonic) btnGenMnemonic.addEventListener("click", GenerateMnemonic);
|
|
|
|
if (btnMnemonicPrev) {
|
|
btnMnemonicPrev.addEventListener("click", () => {
|
|
if (mnemonicPageIndex > 0) {
|
|
mnemonicPageIndex -= 1;
|
|
DecodeMnemonic(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (btnMnemonicNext) {
|
|
btnMnemonicNext.addEventListener("click", () => {
|
|
mnemonicPageIndex += 1;
|
|
DecodeMnemonic(false);
|
|
});
|
|
}
|
|
|
|
if (inMnemonicPhrase) {
|
|
inMnemonicPhrase.addEventListener("keydown", (event) => {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
DecodeMnemonic(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// NIP-04 and NIP-44
|
|
const btnEncodeNip04 = document.getElementById(`btnEncodeNip04`);
|
|
const btnDecodeNip04 = document.getElementById(`btnDecodeNip04`);
|
|
const btnEncodeNip44 = document.getElementById(`btnEncodeNip44`);
|
|
const btnDecodeNip44 = document.getElementById(`btnDecodeNip44`);
|
|
|
|
if (btnEncodeNip04) btnEncodeNip04.addEventListener("click", EncodeNip04);
|
|
if (btnDecodeNip04) btnDecodeNip04.addEventListener("click", DecodeNip04);
|
|
if (btnEncodeNip44) btnEncodeNip44.addEventListener("click", EncodeNip44);
|
|
if (btnDecodeNip44) btnDecodeNip44.addEventListener("click", DecodeNip44);
|
|
|
|
const btnComputeHmacDTag = document.getElementById(`btnComputeHmacDTag`);
|
|
if (btnComputeHmacDTag) btnComputeHmacDTag.addEventListener("click", ComputeHmacDTag);
|
|
|
|
// NSEC to NPUB
|
|
const inpNsecToNpub = document.getElementById(`inpNsecToNpub`);
|
|
if (inpNsecToNpub) inpNsecToNpub.addEventListener("input", NsecToNpub);
|
|
|
|
// NIP-19
|
|
const btnEncodeNip19 = document.getElementById(`btnEncodeNip19`);
|
|
const btnDecodeNip19 = document.getElementById(`btnDecodeNip19`);
|
|
const inpNip19DecodeValue = document.getElementById(`inpNip19DecodeValue`);
|
|
const selNip19EncodeType = document.getElementById(`selNip19EncodeType`);
|
|
|
|
if (btnEncodeNip19) btnEncodeNip19.addEventListener("click", EncodeNip19);
|
|
if (btnDecodeNip19) btnDecodeNip19.addEventListener("click", DecodeNip19);
|
|
if (inpNip19DecodeValue) inpNip19DecodeValue.addEventListener("input", DecodeNip19);
|
|
if (selNip19EncodeType) selNip19EncodeType.addEventListener("change", UpdateNip19CustomTypeVisibility);
|
|
UpdateNip19CustomTypeVisibility();
|
|
|
|
// Clipboard functionality
|
|
let clsClipboard = document.getElementsByClassName(`clsClipboard`);
|
|
for (let Each of clsClipboard) {
|
|
Each.addEventListener("click", function () {
|
|
navigator.clipboard.writeText(Each.innerText);
|
|
// Visual feedback
|
|
const originalBg = Each.style.backgroundColor;
|
|
Each.style.backgroundColor = 'green';
|
|
setTimeout(() => {
|
|
Each.style.backgroundColor = originalBg;
|
|
}, 200);
|
|
});
|
|
}
|
|
|
|
// Version bar buttons
|
|
const themeToggleButton = document.getElementById('themeToggleButton');
|
|
const logoutButton = document.getElementById('logoutButton');
|
|
|
|
if (themeToggleButton) {
|
|
themeToggleButton.addEventListener('click', () => {
|
|
isDarkMode = !isDarkMode;
|
|
if (isDarkMode) {
|
|
localStorage.setItem('theme', 'dark');
|
|
} else {
|
|
localStorage.setItem('theme', 'light');
|
|
}
|
|
localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false');
|
|
window.location.reload();
|
|
});
|
|
}
|
|
|
|
if (logoutButton) {
|
|
logoutButton.addEventListener('click', async () => {
|
|
try {
|
|
await Logout();
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/* ================================================================
|
|
INITIALIZATION
|
|
================================================================ */
|
|
(async function main() {
|
|
console.log("[tools.html] Starting initialization...");
|
|
|
|
try {
|
|
// Initialize hamburger menu
|
|
initHamburgerMenu();
|
|
|
|
// Setup event listeners
|
|
setupEventListeners();
|
|
|
|
// Initialize NDK (allow unauthenticated access for public tools page)
|
|
await initNDKPage({ allowUnauthenticated: true });
|
|
|
|
// Try authenticated header state; fall back to login button when unauthenticated
|
|
try {
|
|
currentPubkey = await getPubkey();
|
|
await injectHeaderAvatar(currentPubkey);
|
|
console.log("[tools.html] Authenticated as:", currentPubkey);
|
|
} catch (authError) {
|
|
currentPubkey = null;
|
|
await injectHeaderLoginButton();
|
|
console.log("[tools.html] Running unauthenticated mode");
|
|
}
|
|
|
|
// Initialize relay UI
|
|
initFooterRelayStatus();
|
|
initSidenavRelaySection();
|
|
|
|
await initBlossomSection();
|
|
initAiSectionWithLocalConfig();
|
|
|
|
// Load NostrTools
|
|
NostrTools = await waitForNostrTools();
|
|
console.log("[tools.html] NostrTools loaded");
|
|
|
|
// Listen for relay activity
|
|
window.addEventListener('ndkRelayActivity', (event) => {
|
|
const { relayUrl, activity, stats } = event.detail;
|
|
setRelayActivityState(relayUrl, activity);
|
|
});
|
|
|
|
window.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'relayActivity') {
|
|
const { relayUrl, activity } = event.data;
|
|
setRelayActivityState(relayUrl, activity);
|
|
}
|
|
});
|
|
|
|
// Restore sidenav state
|
|
const sidenavWasOpen = localStorage.getItem('sidenavWasOpen');
|
|
if (sidenavWasOpen === 'true') {
|
|
localStorage.removeItem('sidenavWasOpen');
|
|
openNav();
|
|
}
|
|
|
|
// Start update loop
|
|
updateIntervalId = setInterval(UpdateFooter, 1000);
|
|
|
|
// Update version display
|
|
await updateVersionDisplay();
|
|
|
|
console.log('[tools.html] Initialization complete');
|
|
} catch (error) {
|
|
console.error('[tools.html] Initialization failed:', error);
|
|
divBody.innerHTML = `<div style="text-align: center; padding: 50px;">
|
|
<div style="font-size: 24px; margin-bottom: 20px; color: red;">❌ Authentication Error</div>
|
|
<div style="font-size: 16px; color: #666;">${error.message}</div>
|
|
<div style="margin-top: 20px;">
|
|
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px;">Retry</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|