package main import ( "git.smesh.lol/smesh/web/common/helpers" "git.smesh.lol/smesh/web/common/jsbridge/callback" "git.smesh.lol/smesh/web/common/jsbridge/dom" "git.smesh.lol/smesh/web/common/jsbridge/localstorage" "git.smesh.lol/smesh/web/common/jsbridge/signer" ) // Signer modal overlay inside the sm3sh app. // Communicates with the extension background via window.nostr.smesh. var ( signerPanel dom.Element signerContent dom.Element signerOpen bool signerStatus string // "none", "locked", "unlocked" pendingK0Name string // nickname to publish as kind 0 after login viewProfileRow dom.Element signerCBs []int32 // callback IDs registered by render functions ) // initSigner checks for the extension and sets up the signer button. func initSigner() { signer.IsInstalled(func(ok bool) { if !ok { dom.ConsoleLog("signer worker not ready") return } dom.ConsoleLog("signer worker ready") refreshVaultStatus() }) } func refreshVaultStatus() { signer.GetVaultStatus(func(status string) { signerStatus = status }) } // buildSignerPanel creates the signer popover panel (hidden). // Called from buildStatusBar so both login and main app share it. func buildSignerPanel() { signerPanel = dom.CreateElement("div") dom.SetStyle(signerPanel, "position", "fixed") dom.SetStyle(signerPanel, "bottom", "37px") dom.SetStyle(signerPanel, "left", "12px") // re-aligned on toggle to the user button's left edge. dom.SetStyle(signerPanel, "width", "360px") dom.SetStyle(signerPanel, "maxWidth", "calc(100vw - 24px)") dom.SetStyle(signerPanel, "maxHeight", "60vh") dom.SetStyle(signerPanel, "overflowY", "auto") dom.SetStyle(signerPanel, "background", "var(--bg2)") dom.SetStyle(signerPanel, "border", "1px solid var(--border)") dom.SetStyle(signerPanel, "borderRadius", "8px") dom.SetStyle(signerPanel, "padding", "12px 16px") dom.SetStyle(signerPanel, "fontSize", "13px") dom.SetStyle(signerPanel, "display", "none") dom.SetStyle(signerPanel, "zIndex", "99") signerContent = dom.CreateElement("div") dom.AppendChild(signerPanel, signerContent) viewProfileRow = dom.CreateElement("div") dom.SetStyle(viewProfileRow, "display", "none") dom.SetStyle(viewProfileRow, "borderTop", "1px solid var(--border)") dom.SetStyle(viewProfileRow, "marginTop", "12px") dom.SetStyle(viewProfileRow, "paddingTop", "10px") viewProfileLink := dom.CreateElement("span") dom.SetTextContent(viewProfileLink, "view profile") dom.SetStyle(viewProfileLink, "cursor", "pointer") dom.SetStyle(viewProfileLink, "color", "var(--accent)") dom.SetStyle(viewProfileLink, "fontFamily", "'Fira Code', monospace") dom.AddEventListener(viewProfileLink, "click", dom.RegisterCallback(func() { if pubhex == "" { return } hideSignerPanel() showProfile(pubhex) })) dom.AppendChild(viewProfileRow, viewProfileLink) dom.AppendChild(signerPanel, viewProfileRow) } // toggleSignerPanel shows/hides the signer popover. func toggleSignerPanel() { if signerOpen { hideSignerPanel() return } if popoverOpen { togglePopover() } closeFeedPopover() signerOpen = true // Align panel's left edge with the user avatar button's left edge, // clamped so the right edge stays on screen. if int32(userBtn) != 0 { x := dom.BoundingClientLeft(userBtn) vw := dom.GetViewportWidth() panelW := 360 if panelW > vw-12 { panelW = vw - 12 } if x+panelW > vw-6 { x = vw - 6 - panelW } if x < 6 { x = 6 } dom.SetStyle(signerPanel, "left", itoa(x)|"px") } dom.SetStyle(signerPanel, "display", "block") if int32(signerBtnEl) != 0 { dom.SetStyle(signerBtnEl, "background", "var(--accent)") dom.SetStyle(signerBtnEl, "color", "var(--bg)") } if int32(userBtn) != 0 { dom.SetStyle(userBtn, "background", "var(--accent)") dom.SetStyle(userBtn, "color", "#fff") } renderSignerUI() } func hideSignerPanel() { if !signerOpen { return } signerOpen = false dom.SetStyle(signerPanel, "display", "none") if int32(signerBtnEl) != 0 { dom.SetStyle(signerBtnEl, "background", "transparent") dom.SetStyle(signerBtnEl, "color", "var(--muted)") } if int32(userBtn) != 0 { dom.SetStyle(userBtn, "background", "transparent") dom.SetStyle(userBtn, "color", "var(--fg)") } } func signerCB(fn func()) (n int32) { id := dom.RegisterCallback(fn) signerCBs = append(signerCBs, id) return id } func releaseSignerCBs() { for _, id := range signerCBs { callback.Release(int32(id)) } signerCBs = nil } func renderSignerUI() { releaseSignerCBs() dom.SetTextContent(signerContent, "") if !signer.HasSigner() { p := dom.CreateElement("p") dom.SetStyle(p, "color", "var(--muted)") dom.SetStyle(p, "fontSize", "13px") dom.SetTextContent(p, t("no_signer_backend")) dom.AppendChild(signerContent, p) return } signer.GetVaultStatus(func(status string) { signerStatus = status dom.SetTextContent(signerContent, "") if pubhex != "" { dom.SetStyle(viewProfileRow, "display", "block") } else { dom.SetStyle(viewProfileRow, "display", "none") } switch status { case "none": renderCreateVault() case "locked": renderUnlockVault() case "unlocked": renderIdentityList() } }) } func renderCreateVault() { // "No vault found" notice. p := dom.CreateElement("p") dom.SetTextContent(p, t("no_vault")) dom.SetStyle(p, "marginTop", "0") dom.SetStyle(p, "marginBottom", "12px") dom.AppendChild(signerContent, p) // Single shared password field at the top. pwInput := dom.CreateElement("input") dom.SetAttribute(pwInput, "type", "password") dom.SetAttribute(pwInput, "placeholder", t("vault_password")) dom.SetAttribute(pwInput, "class", "signer-input") dom.SetAttribute(pwInput, "autocomplete", "new-password") dom.SetAttribute(pwInput, "autocapitalize", "off") dom.SetAttribute(pwInput, "autocorrect", "off") dom.SetAttribute(pwInput, "spellcheck", "false") dom.SetStyle(pwInput, "marginBottom", "8px") dom.AppendChild(signerContent, pwInput) errEl := dom.CreateElement("div") dom.SetStyle(errEl, "color", "#e55") dom.SetStyle(errEl, "fontSize", "12px") dom.SetStyle(errEl, "minHeight", "14px") dom.SetStyle(errEl, "marginBottom", "4px") dom.AppendChild(signerContent, errEl) getPw := func() string { pw := dom.GetProperty(pwInput, "value") if pw == "" { dom.SetTextContent(errEl, "password required") } else { dom.SetTextContent(errEl, "") } return pw } sep0 := dom.CreateElement("hr") dom.SetAttribute(sep0, "class", "signer-sep") dom.AppendChild(signerContent, sep0) // --- nsec import --- nsecInput := dom.CreateElement("input") dom.SetAttribute(nsecInput, "type", "password") dom.SetAttribute(nsecInput, "placeholder", "nsec1...") dom.SetAttribute(nsecInput, "class", "signer-input") dom.SetAttribute(nsecInput, "autocomplete", "off") dom.SetAttribute(nsecInput, "autocapitalize", "off") dom.SetAttribute(nsecInput, "autocorrect", "off") dom.SetAttribute(nsecInput, "spellcheck", "false") dom.SetStyle(nsecInput, "marginBottom", "6px") dom.AppendChild(signerContent, nsecInput) nsecBtn := dom.CreateElement("button") dom.SetAttribute(nsecBtn, "class", "signer-btn") dom.SetTextContent(nsecBtn, "create vault with nsec") dom.SetStyle(nsecBtn, "marginTop", "0") dom.SetStyle(nsecBtn, "marginBottom", "8px") dom.AddEventListener(nsecBtn, "click", signerCB(func() { nsec := dom.GetProperty(nsecInput, "value") pw := getPw() if len(nsec) == 0 { dom.SetTextContent(errEl, "nsec required") return } if pw == "" { return } dom.SetTextContent(nsecBtn, "...") restore := func() { dom.SetTextContent(nsecBtn, "create vault with nsec") } addAndFinish := func() { signer.AddIdentity(nsec, func(ok bool) { if !ok { dom.SetTextContent(errEl, "invalid nsec or already present") restore() return } renderSignerUI() }) } signer.GetVaultStatus(func(status string) { switch status { case "none": signer.CreateVault(pw, func(ok bool) { if !ok { dom.SetTextContent(errEl, "vault create failed") restore() return } addAndFinish() }) case "locked": signer.UnlockVault(pw, func(ok bool) { if !ok { signer.LastUnlockError(func(reason string) { msg := "wrong password" if reason == "kdf-failed" || reason == "decrypt-failed" { msg = "unlock failed: " | reason } logActivity("unlock", "FAILED: " | reason, "") dom.SetTextContent(errEl, msg) restore() }) return } logActivity("unlock", "vault unlocked", "") addAndFinish() }) default: addAndFinish() } }) })) dom.AppendChild(signerContent, nsecBtn) sep1 := dom.CreateElement("hr") dom.SetAttribute(sep1, "class", "signer-sep") dom.AppendChild(signerContent, sep1) // --- HD Vault (generate) --- h3 := dom.CreateElement("h3") dom.SetTextContent(h3, t("hd_keychain")) dom.AppendChild(signerContent, h3) createBtn := dom.CreateElement("button") dom.SetAttribute(createBtn, "class", "signer-btn") dom.SetTextContent(createBtn, t("generate_keychain")) createCB := signerCB(func() { pw := getPw() if pw == "" { return } dom.SetTextContent(createBtn, t("generating")) dom.SetAttribute(createBtn, "disabled", "true") // Empty mnemonic -> WASM generates a fresh BIP-39 phrase. // Passing any non-empty non-mnemonic string (e.g. a label) would be // treated as a BIP-39 phrase and PBKDF2 a deterministic seed. signer.CreateHDVault(pw, "", func(mnemonic string) { if mnemonic == "" { dom.SetTextContent(createBtn, t("create_failed")) dom.SetAttribute(createBtn, "disabled", "") return } signer.DeriveIdentity("Identity 1", func(pk string) { showMnemonicReveal(mnemonic) }) }) }) dom.AddEventListener(createBtn, "click", createCB) dom.AppendChild(signerContent, createBtn) // --- Restore from mnemonic --- sep2 := dom.CreateElement("hr") dom.SetAttribute(sep2, "class", "signer-sep") dom.AppendChild(signerContent, sep2) h3r := dom.CreateElement("h3") dom.SetTextContent(h3r, t("restore_seed")) dom.AppendChild(signerContent, h3r) mnInput := dom.CreateElement("textarea") dom.SetAttribute(mnInput, "placeholder", t("seed_placeholder")) dom.SetAttribute(mnInput, "class", "signer-input signer-textarea") dom.SetAttribute(mnInput, "rows", "3") dom.AppendChild(signerContent, mnInput) idxInput := dom.CreateElement("input") dom.SetAttribute(idxInput, "type", "number") dom.SetAttribute(idxInput, "placeholder", t("account_placeholder")) dom.SetAttribute(idxInput, "class", "signer-input") dom.SetProperty(idxInput, "value", "1") dom.AppendChild(signerContent, idxInput) restoreBtn := dom.CreateElement("button") dom.SetAttribute(restoreBtn, "class", "signer-btn") dom.SetTextContent(restoreBtn, t("restore_keychain")) restoreCB := signerCB(func() { mn := dom.GetProperty(mnInput, "value") pw := getPw() if mn == "" || pw == "" { return } idxStr := dom.GetProperty(idxInput, "value") targetIdx := parseAccountIdx(idxStr) if targetIdx < 1 { targetIdx = 1 } dom.SetTextContent(restoreBtn, t("restoring")) dom.SetAttribute(restoreBtn, "disabled", "true") signer.RestoreHDVault(pw, mn, "Identity 0", func(ok bool) { if !ok { dom.SetTextContent(restoreBtn, t("restore_failed")) dom.SetAttribute(restoreBtn, "disabled", "") return } dom.SetTextContent(restoreBtn, t("deriving_n") | " " | itoa(targetIdx) | "...") restoreDeriveUpTo(targetIdx, 1, func() { renderSignerUI() }) }) }) dom.AddEventListener(restoreBtn, "click", restoreCB) dom.AppendChild(signerContent, restoreBtn) // --- Import vault file --- sep3 := dom.CreateElement("hr") dom.SetAttribute(sep3, "class", "signer-sep") dom.AppendChild(signerContent, sep3) h3b := dom.CreateElement("h3") dom.SetTextContent(h3b, t("import_vault")) dom.AppendChild(signerContent, h3b) importBtn := dom.CreateElement("button") dom.SetAttribute(importBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(importBtn, t("choose_vault")) importCB := signerCB(func() { dom.PickFileText(".json", func(data string) { if data == "" { return } dom.SetTextContent(importBtn, t("restoring")) dom.SetAttribute(importBtn, "disabled", "true") signer.ImportVault(data, func(ok bool) { if ok { renderSignerUI() } else { dom.SetTextContent(importBtn, t("invalid_vault")) dom.SetAttribute(importBtn, "disabled", "") } }) }) }) dom.AddEventListener(importBtn, "click", importCB) dom.AppendChild(signerContent, importBtn) } // showMnemonicReveal displays the generated mnemonic for the user to write down. func showMnemonicReveal(mnemonic string) { dom.SetTextContent(signerContent, "") h3 := dom.CreateElement("h3") dom.SetTextContent(h3, t("write_seed")) dom.AppendChild(signerContent, h3) warn := dom.CreateElement("p") dom.SetAttribute(warn, "class", "signer-warn") dom.SetTextContent(warn, t("seed_warning")) dom.AppendChild(signerContent, warn) mnBox := dom.CreateElement("div") dom.SetAttribute(mnBox, "class", "signer-mnemonic") words := splitMnemonicWords(mnemonic) for i, w := range words { span := dom.CreateElement("span") dom.SetAttribute(span, "class", "signer-word") dom.SetTextContent(span, itoa(i+1) | ". " | w) dom.AppendChild(mnBox, span) } dom.AppendChild(signerContent, mnBox) copyBtn := dom.CreateElement("button") dom.SetAttribute(copyBtn, "class", "signer-btn signer-btn-secondary") dom.SetAttribute(copyBtn, "data-mn", mnemonic) dom.SetTextContent(copyBtn, t("copy_clipboard")) dom.SetAttribute(copyBtn, "data-label", t("copy_clipboard")) dom.SetAttribute(copyBtn, "data-copied", t("copied")) dom.SetAttribute(copyBtn, "onclick", "var b=this;navigator.clipboard.writeText(b.dataset.mn).then(function(){b.textContent=b.dataset.copied});setTimeout(function(){b.textContent=b.dataset.label},1500)") dom.AppendChild(signerContent, copyBtn) doneBtn := dom.CreateElement("button") dom.SetAttribute(doneBtn, "class", "signer-btn") dom.SetTextContent(doneBtn, t("saved_it")) doneCB := signerCB(func() { renderSignerUI() }) dom.AddEventListener(doneBtn, "click", doneCB) dom.AppendChild(signerContent, doneBtn) } func splitMnemonicWords(s string) (ss []string) { var words []string start := -1 for i := 0; i < len(s); i++ { if s[i] == ' ' { if start >= 0 { words = append(words, s[start:i]) start = -1 } } else if start < 0 { start = i } } if start >= 0 { words = append(words, s[start:]) } return words } func renderUnlockVault() { p := dom.CreateElement("p") dom.SetTextContent(p, t("vault_locked")) dom.SetStyle(p, "marginBottom", "12px") dom.AppendChild(signerContent, p) input := dom.CreateElement("input") dom.SetAttribute(input, "type", "password") dom.SetAttribute(input, "placeholder", t("password")) dom.SetAttribute(input, "class", "signer-input") dom.SetAttribute(input, "autocomplete", "current-password") dom.SetAttribute(input, "autocapitalize", "off") dom.SetAttribute(input, "autocorrect", "off") dom.SetAttribute(input, "spellcheck", "false") dom.AppendChild(signerContent, input) dom.Focus(input) btn := dom.CreateElement("button") dom.SetAttribute(btn, "class", "signer-btn") dom.SetTextContent(btn, t("unlock")) cb := signerCB(func() { pw := dom.GetProperty(input, "value") if pw == "" { return } dom.SetTextContent(p, t("deriving_key")) dom.SetAttribute(btn, "disabled", "true") logActivity("unlock", "deriving key from password", "") signer.UnlockVault(pw, func(ok bool) { if ok { logActivity("unlock", "vault unlocked", "") renderSignerUI() return } signer.LastUnlockError(func(reason string) { if reason == "" { reason = "unknown" } logActivity("unlock", "FAILED: " | reason, "reasons:\n wrong-password = sha256(password) != stored hash\n kdf-failed = Argon2id returned empty (memory/GC issue on mobile)\n decrypt-failed = key derived but AES-GCM decryption failed (corrupted vault or non-deterministic argon2)\n no-vault = vault data missing\n no-hash = vault missing hash field\n bad-iv = legacy IV invalid\n\nFor wrong-password, the entry includes computed= and stored= prefixes:\n - if they're stable across attempts but differ: stored hash was corrupted on save (likely a bug)\n - if computed differs across attempts: SHA256 is non-deterministic (memory corruption)\n - if same as last attempt: real typo") msg := "wrong_password" if reason == "kdf-failed" || (len(reason) >= 14 && reason[:14] == "decrypt-failed") { msg = "unlock_failed_kdf" } dom.SetTextContent(p, t(msg)) dom.SetAttribute(btn, "disabled", "") }) }) }) dom.AddEventListener(btn, "click", cb) dom.AddEnterKeyListener(input, cb) dom.AppendChild(signerContent, btn) // Reset button - lets user start over without digging into settings. resetBtn := dom.CreateElement("button") dom.SetAttribute(resetBtn, "class", "signer-btn") dom.SetStyle(resetBtn, "background", "transparent") dom.SetStyle(resetBtn, "color", "var(--muted)") dom.SetStyle(resetBtn, "border", "1px solid var(--border)") dom.SetStyle(resetBtn, "marginTop", "12px") dom.SetTextContent(resetBtn, t("logout")) dom.AddEventListener(resetBtn, "click", signerCB(func() { if !dom.Confirm(t("logout_confirm")) { return } signer.ResetExtension(func(ok bool) { localstorage.RemoveItem(lsKeyPubkey) dom.LocationAssign("/") }) })) dom.AppendChild(signerContent, resetBtn) } func renderIdentityList() { signer.ListIdentities(func(list string) { signer.IsHD(func(hd bool) { dom.SetTextContent(signerContent, "") h := dom.CreateElement("h3") dom.SetTextContent(h, t("identities")) dom.AppendChild(signerContent, h) renderIdentitiesFromJSON(list, hd) if hd { renderHDControls() } renderLegacyAddIdentity() renderBottomActions(hd) }) }) } func renderHDControls() { addDiv := dom.CreateElement("div") dom.SetAttribute(addDiv, "class", "signer-add") nameInput := dom.CreateElement("input") dom.SetAttribute(nameInput, "type", "text") dom.SetAttribute(nameInput, "placeholder", t("nickname_placeholder")) dom.SetAttribute(nameInput, "class", "signer-input") dom.AppendChild(addDiv, nameInput) deriveBtn := dom.CreateElement("button") dom.SetAttribute(deriveBtn, "class", "signer-btn") dom.SetTextContent(deriveBtn, t("derive_new")) deriveCB := signerCB(func() { name := dom.GetProperty(nameInput, "value") dom.SetAttribute(deriveBtn, "disabled", "true") dom.SetTextContent(deriveBtn, t("deriving")) signer.DeriveIdentity(name, func(pk string) { if pk == "" { dom.SetAttribute(deriveBtn, "disabled", "") dom.SetTextContent(deriveBtn, t("derive_new")) return } // Stash name - kind 0 will be published after login when relays are live. pendingK0Name = name renderSignerUI() }) }) dom.AddEventListener(deriveBtn, "click", deriveCB) dom.AppendChild(addDiv, deriveBtn) dom.AppendChild(signerContent, addDiv) } func parseAccountIdx(s string) (n int32) { n = 0 for i := 0; i < len(s); i++ { c := s[i] if c >= '0' && c <= '9' { n = n*10 + int32(c-'0') } else { break } } return n } // restoreDeriveUpTo derives accounts from cur up to target sequentially. func restoreDeriveUpTo(target, cur int32, done func()) { if cur > target { done() return } signer.DeriveIdentity("Identity " | itoa(cur), func(pk string) { restoreDeriveUpTo(target, cur+1, done) }) } // flushPendingK0 publishes a kind 0 if one was queued during identity derivation. // Called from showApp() after relays are live. func flushPendingK0() { name := pendingK0Name if name == "" || pubhex == "" { return } pendingK0Name = "" publishKind0ForIdentity(pubhex, name, func() {}) } // publishKind0ForIdentity switches to the given identity, signs a kind 0 event // with the nickname, publishes it to all relays, then calls done. func publishKind0ForIdentity(pk, name string, done func()) { signer.SwitchIdentity(pk, func(ok bool) { if !ok { done() return } content := "{\"name\":" | helpers.JsonString(name) | ",\"display_name\":" | helpers.JsonString(name) | "}" ts := dom.NowSeconds() unsigned := "{\"kind\":0,\"content\":" | helpers.JsonString(content) | ",\"tags\":[],\"created_at\":" | i64toa(ts) | ",\"pubkey\":\"" | pk | "\"}" dom.ConsoleLog("[signer-panel] signing kind 0 for " | pk[:8]) signer.SignEvent(unsigned, func(signed string) { if len(signed) > 0 { dom.PostToSW("[\"EVENT\"," | signed | "]") dom.ConsoleLog("[signer-panel] published kind 0 for " | pk[:8] | " name=" | name) } else { dom.ConsoleLog("[signer-panel] sign failed for " | pk[:8] | " - check console for [signer] errors") } done() }) }) } func renderLegacyAddIdentity() { addDiv := dom.CreateElement("div") dom.SetAttribute(addDiv, "class", "signer-add") addInput := dom.CreateElement("input") dom.SetAttribute(addInput, "type", "password") dom.SetAttribute(addInput, "placeholder", "nsec1...") dom.SetAttribute(addInput, "class", "signer-input") dom.SetAttribute(addInput, "autocomplete", "off") dom.SetAttribute(addInput, "autocapitalize", "off") dom.SetAttribute(addInput, "autocorrect", "off") dom.SetAttribute(addInput, "spellcheck", "false") dom.AppendChild(addDiv, addInput) addErr := dom.CreateElement("div") dom.SetStyle(addErr, "color", "#e55") dom.SetStyle(addErr, "fontSize", "12px") dom.SetStyle(addErr, "minHeight", "14px") dom.SetStyle(addErr, "marginBottom", "4px") dom.AppendChild(addDiv, addErr) addBtn := dom.CreateElement("button") dom.SetAttribute(addBtn, "class", "signer-btn") dom.SetTextContent(addBtn, t("add")) addCB := signerCB(func() { nsec := dom.GetProperty(addInput, "value") if nsec == "" { dom.SetTextContent(addErr, "nsec required") return } dom.SetTextContent(addErr, "") signer.AddIdentity(nsec, func(ok bool) { if ok { dom.SetProperty(addInput, "value", "") renderSignerUI() return } dom.SetTextContent(addErr, "invalid nsec or already present") }) }) dom.AddEventListener(addBtn, "click", addCB) dom.AppendChild(addDiv, addBtn) dom.AppendChild(signerContent, addDiv) } func renderBottomActions(hd bool) { actions := dom.CreateElement("div") dom.SetAttribute(actions, "class", "signer-actions") // Show seed phrase button (HD only). if hd { seedBtn := dom.CreateElement("button") dom.SetAttribute(seedBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(seedBtn, t("show_seed")) seedCB := signerCB(func() { signer.GetMnemonic(func(m string) { if m != "" { showMnemonicReveal(m) } }) }) dom.AddEventListener(seedBtn, "click", seedCB) dom.AppendChild(actions, seedBtn) } // Export vault button. exportBtn := dom.CreateElement("button") dom.SetAttribute(exportBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(exportBtn, t("export_vault")) exportCB := signerCB(func() { showExportPasswordPrompt() }) dom.AddEventListener(exportBtn, "click", exportCB) dom.AppendChild(actions, exportBtn) // Add HD keychain - navigates to the create-new-HD-keychain flow. addHDBtn := dom.CreateElement("button") dom.SetAttribute(addHDBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(addHDBtn, "Add HD keychain") addHDCB := signerCB(func() { dom.SetTextContent(signerContent, "") renderCreateVault() }) dom.AddEventListener(addHDBtn, "click", addHDCB) dom.AppendChild(actions, addHDBtn) // Lock vault button. lockBtn := dom.CreateElement("button") dom.SetAttribute(lockBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(lockBtn, t("lock_vault")) lockCB := signerCB(func() { signer.LockVault(func() { renderSignerUI() }) }) dom.AddEventListener(lockBtn, "click", lockCB) dom.AppendChild(actions, lockBtn) // Logout (wipes the vault and reloads). resetBtn := dom.CreateElement("button") dom.SetAttribute(resetBtn, "class", "signer-btn signer-btn-danger") dom.SetTextContent(resetBtn, t("logout")) resetCB := signerCB(func() { if !dom.Confirm(t("logout_confirm")) { return } signer.ResetExtension(func(ok bool) { localstorage.RemoveItem(lsKeyPubkey) dom.LocationAssign("/") }) }) dom.AddEventListener(resetBtn, "click", resetCB) dom.AppendChild(actions, resetBtn) dom.AppendChild(signerContent, actions) } func showExportPasswordPrompt() { // Replace signer content with password prompt. dom.SetTextContent(signerContent, "") label := dom.CreateElement("p") dom.SetTextContent(label, t("export_password_prompt")) dom.SetStyle(label, "marginBottom", "8px") dom.AppendChild(signerContent, label) pwInput := dom.CreateElement("input") dom.SetAttribute(pwInput, "type", "password") dom.SetAttribute(pwInput, "placeholder", t("password")) dom.SetAttribute(pwInput, "class", "signer-input") dom.SetAttribute(pwInput, "autocomplete", "new-password") dom.SetAttribute(pwInput, "autocapitalize", "off") dom.SetAttribute(pwInput, "autocorrect", "off") dom.SetAttribute(pwInput, "spellcheck", "false") dom.AppendChild(signerContent, pwInput) confirmInput := dom.CreateElement("input") dom.SetAttribute(confirmInput, "type", "password") dom.SetAttribute(confirmInput, "placeholder", t("confirm_password")) dom.SetAttribute(confirmInput, "class", "signer-input") dom.SetAttribute(confirmInput, "autocomplete", "new-password") dom.SetAttribute(confirmInput, "autocapitalize", "off") dom.SetAttribute(confirmInput, "autocorrect", "off") dom.SetAttribute(confirmInput, "spellcheck", "false") dom.SetStyle(confirmInput, "marginTop", "8px") dom.AppendChild(signerContent, confirmInput) errEl := dom.CreateElement("p") dom.SetStyle(errEl, "color", "var(--error, #e55)") dom.SetStyle(errEl, "fontSize", "13px") dom.SetStyle(errEl, "marginTop", "4px") dom.AppendChild(signerContent, errEl) btnRow := dom.CreateElement("div") dom.SetStyle(btnRow, "display", "flex") dom.SetStyle(btnRow, "gap", "8px") dom.SetStyle(btnRow, "marginTop", "12px") cancelBtn := dom.CreateElement("button") dom.SetAttribute(cancelBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(cancelBtn, t("cancel")) dom.AddEventListener(cancelBtn, "click", signerCB(func() { renderSignerUI() })) dom.AppendChild(btnRow, cancelBtn) goBtn := dom.CreateElement("button") dom.SetAttribute(goBtn, "class", "signer-btn signer-btn-primary") dom.SetTextContent(goBtn, t("export_vault")) dom.AddEventListener(goBtn, "click", signerCB(func() { pw := dom.GetProperty(pwInput, "value") confirm := dom.GetProperty(confirmInput, "value") if pw == "" { dom.SetTextContent(errEl, t("password_required")) return } if pw != confirm { dom.SetTextContent(errEl, t("passwords_mismatch")) return } dom.SetTextContent(goBtn, "...") signer.ExportVault(pw, func(data string) { if data == "" { dom.SetTextContent(errEl, t("export_failed")) dom.SetTextContent(goBtn, t("export_vault")) return } dom.DownloadText("smesh-vault.json", data, "application/json") renderSignerUI() }) })) dom.AppendChild(btnRow, goBtn) dom.AppendChild(signerContent, btnRow) dom.Focus(pwInput) } func renderIdentitiesFromJSON(listJSON string, hd bool) { i := 0 idxNum := 0 for i < len(listJSON) && listJSON[i] != '[' { i++ } i++ for i < len(listJSON) { for i < len(listJSON) && listJSON[i] != '{' && listJSON[i] != ']' { i++ } if i >= len(listJSON) || listJSON[i] == ']' { break } end := i + 1 depth := 1 for end < len(listJSON) && depth > 0 { if listJSON[end] == '{' { depth++ } else if listJSON[end] == '}' { depth-- } else if listJSON[end] == '"' { end++ for end < len(listJSON) && listJSON[end] != '"' { if listJSON[end] == '\\' { end++ } end++ } } end++ } obj := listJSON[i:end] pk := helpers.JsonGetString(obj, "pubkey") name := helpers.JsonGetString(obj, "name") if pk == "" { i = end idxNum++ continue } row := dom.CreateElement("div") dom.SetAttribute(row, "class", "signer-identity") label := dom.CreateElement("span") display := pk[:8] | "..." if name != "" { display = name | " (" | pk[:8] | "...)" } if hd { display = "#" | itoa(idxNum) | " " | display } dom.SetTextContent(label, display) dom.AppendChild(row, label) btns := dom.CreateElement("span") // HD identity 0 is the seed - skip it entirely. if hd && idxNum == 0 { i = end idxNum++ continue } // Switch / current-identity indicator. if pubhex != "" && pk == pubhex { loggedIn := dom.CreateElement("span") dom.SetAttribute(loggedIn, "class", "signer-btn-sm") dom.SetStyle(loggedIn, "color", "var(--muted)") dom.SetStyle(loggedIn, "opacity", "0.6") dom.SetStyle(loggedIn, "cursor", "default") dom.SetStyle(loggedIn, "pointerEvents", "none") dom.SetTextContent(loggedIn, "Logged in") dom.AppendChild(btns, loggedIn) } else { switchBtn := dom.CreateElement("button") dom.SetAttribute(switchBtn, "class", "signer-btn-sm") dom.SetStyle(switchBtn, "color", "var(--accent)") dom.SetStyle(switchBtn, "fontWeight", "bold") dom.SetTextContent(switchBtn, "Login") switchPK := pk switchCB := signerCB(func() { signer.SwitchIdentity(switchPK, func(ok bool) { if ok { hideSignerPanel() pubhex = switchPK localstorage.SetItem(lsKeyPubkey, pubhex) // Reset timestamp guards so the new identity's // replaceable events aren't rejected as stale. profileTs = 0 contactTs = 0 muteTs = 0 relayListTs = 0 inboxTs = 0 feedSubscribed = false clearChildren(root) bootstrapEncKey(func() { showApp() }) } }) }) dom.AddEventListener(switchBtn, "click", switchCB) dom.AppendChild(btns, switchBtn) } // Publish kind 0 button. pubBtn := dom.CreateElement("button") dom.SetAttribute(pubBtn, "class", "signer-btn-sm") dom.SetTextContent(pubBtn, t("publish")) pubPK := pk pubName := name pubCB := signerCB(func() { dom.SetTextContent(pubBtn, "...") dom.SetAttribute(pubBtn, "disabled", "true") publishKind0ForIdentity(pubPK, pubName, func() { dom.SetTextContent(pubBtn, t("publish")) dom.SetAttribute(pubBtn, "disabled", "") }) }) dom.AddEventListener(pubBtn, "click", pubCB) dom.AppendChild(btns, pubBtn) // Remove button. rmBtn := dom.CreateElement("button") dom.SetAttribute(rmBtn, "class", "signer-btn-sm signer-btn-danger") dom.SetTextContent(rmBtn, "\u00d7") rmPK := pk rmCB := signerCB(func() { signer.RemoveIdentity(rmPK, func(ok bool) { if ok { renderSignerUI() } }) }) dom.AddEventListener(rmBtn, "click", rmCB) dom.AppendChild(btns, rmBtn) dom.AppendChild(row, btns) dom.AppendChild(signerContent, row) i = end idxNum++ } }