signer.mx raw
1 package main
2
3 import (
4 "git.smesh.lol/smesh/web/common/helpers"
5 "git.smesh.lol/smesh/web/common/jsbridge/callback"
6 "git.smesh.lol/smesh/web/common/jsbridge/dom"
7 "git.smesh.lol/smesh/web/common/jsbridge/localstorage"
8 "git.smesh.lol/smesh/web/common/jsbridge/signer"
9 )
10
11 // Signer modal overlay inside the sm3sh app.
12 // Communicates with the extension background via window.nostr.smesh.
13
14 var (
15 signerPanel dom.Element
16 signerContent dom.Element
17 signerOpen bool
18 signerStatus string // "none", "locked", "unlocked"
19 pendingK0Name string // nickname to publish as kind 0 after login
20 viewProfileRow dom.Element
21 signerCBs []int32 // callback IDs registered by render functions
22 )
23
24 // initSigner checks for the extension and sets up the signer button.
25 func initSigner() {
26 signer.IsInstalled(func(ok bool) {
27 if !ok {
28 dom.ConsoleLog("signer worker not ready")
29 return
30 }
31 dom.ConsoleLog("signer worker ready")
32 refreshVaultStatus()
33 })
34 }
35
36 func refreshVaultStatus() {
37 signer.GetVaultStatus(func(status string) {
38 signerStatus = status
39 })
40 }
41
42 // buildSignerPanel creates the signer popover panel (hidden).
43 // Called from buildStatusBar so both login and main app share it.
44 func buildSignerPanel() {
45 signerPanel = dom.CreateElement("div")
46 dom.SetStyle(signerPanel, "position", "fixed")
47 dom.SetStyle(signerPanel, "bottom", "37px")
48 dom.SetStyle(signerPanel, "left", "12px") // re-aligned on toggle to the user button's left edge.
49 dom.SetStyle(signerPanel, "width", "360px")
50 dom.SetStyle(signerPanel, "maxWidth", "calc(100vw - 24px)")
51 dom.SetStyle(signerPanel, "maxHeight", "60vh")
52 dom.SetStyle(signerPanel, "overflowY", "auto")
53 dom.SetStyle(signerPanel, "background", "var(--bg2)")
54 dom.SetStyle(signerPanel, "border", "1px solid var(--border)")
55 dom.SetStyle(signerPanel, "borderRadius", "8px")
56 dom.SetStyle(signerPanel, "padding", "12px 16px")
57 dom.SetStyle(signerPanel, "fontSize", "13px")
58 dom.SetStyle(signerPanel, "display", "none")
59 dom.SetStyle(signerPanel, "zIndex", "99")
60
61 signerContent = dom.CreateElement("div")
62 dom.AppendChild(signerPanel, signerContent)
63
64 viewProfileRow = dom.CreateElement("div")
65 dom.SetStyle(viewProfileRow, "display", "none")
66 dom.SetStyle(viewProfileRow, "borderTop", "1px solid var(--border)")
67 dom.SetStyle(viewProfileRow, "marginTop", "12px")
68 dom.SetStyle(viewProfileRow, "paddingTop", "10px")
69 viewProfileLink := dom.CreateElement("span")
70 dom.SetTextContent(viewProfileLink, "view profile")
71 dom.SetStyle(viewProfileLink, "cursor", "pointer")
72 dom.SetStyle(viewProfileLink, "color", "var(--accent)")
73 dom.SetStyle(viewProfileLink, "fontFamily", "'Fira Code', monospace")
74 dom.AddEventListener(viewProfileLink, "click", dom.RegisterCallback(func() {
75 if pubhex == "" {
76 return
77 }
78 hideSignerPanel()
79 showProfile(pubhex)
80 }))
81 dom.AppendChild(viewProfileRow, viewProfileLink)
82 dom.AppendChild(signerPanel, viewProfileRow)
83 }
84
85 // toggleSignerPanel shows/hides the signer popover.
86 func toggleSignerPanel() {
87 if signerOpen {
88 hideSignerPanel()
89 return
90 }
91 if popoverOpen {
92 togglePopover()
93 }
94 closeFeedPopover()
95 signerOpen = true
96 // Align panel's left edge with the user avatar button's left edge,
97 // clamped so the right edge stays on screen.
98 if int32(userBtn) != 0 {
99 x := dom.BoundingClientLeft(userBtn)
100 vw := dom.GetViewportWidth()
101 panelW := 360
102 if panelW > vw-12 {
103 panelW = vw - 12
104 }
105 if x+panelW > vw-6 {
106 x = vw - 6 - panelW
107 }
108 if x < 6 {
109 x = 6
110 }
111 dom.SetStyle(signerPanel, "left", itoa(x)|"px")
112 }
113 dom.SetStyle(signerPanel, "display", "block")
114 if int32(signerBtnEl) != 0 {
115 dom.SetStyle(signerBtnEl, "background", "var(--accent)")
116 dom.SetStyle(signerBtnEl, "color", "var(--bg)")
117 }
118 if int32(userBtn) != 0 {
119 dom.SetStyle(userBtn, "background", "var(--accent)")
120 dom.SetStyle(userBtn, "color", "#fff")
121 }
122 renderSignerUI()
123 }
124
125 func hideSignerPanel() {
126 if !signerOpen {
127 return
128 }
129 signerOpen = false
130 dom.SetStyle(signerPanel, "display", "none")
131 if int32(signerBtnEl) != 0 {
132 dom.SetStyle(signerBtnEl, "background", "transparent")
133 dom.SetStyle(signerBtnEl, "color", "var(--muted)")
134 }
135 if int32(userBtn) != 0 {
136 dom.SetStyle(userBtn, "background", "transparent")
137 dom.SetStyle(userBtn, "color", "var(--fg)")
138 }
139 }
140
141 func signerCB(fn func()) (n int32) {
142 id := dom.RegisterCallback(fn)
143 signerCBs = append(signerCBs, id)
144 return id
145 }
146
147 func releaseSignerCBs() {
148 for _, id := range signerCBs {
149 callback.Release(int32(id))
150 }
151 signerCBs = nil
152 }
153
154 func renderSignerUI() {
155 releaseSignerCBs()
156 dom.SetTextContent(signerContent, "")
157
158 if !signer.HasSigner() {
159 p := dom.CreateElement("p")
160 dom.SetStyle(p, "color", "var(--muted)")
161 dom.SetStyle(p, "fontSize", "13px")
162 dom.SetTextContent(p, t("no_signer_backend"))
163 dom.AppendChild(signerContent, p)
164 return
165 }
166
167 signer.GetVaultStatus(func(status string) {
168 signerStatus = status
169 dom.SetTextContent(signerContent, "")
170
171 if pubhex != "" {
172 dom.SetStyle(viewProfileRow, "display", "block")
173 } else {
174 dom.SetStyle(viewProfileRow, "display", "none")
175 }
176
177 switch status {
178 case "none":
179 renderCreateVault()
180 case "locked":
181 renderUnlockVault()
182 case "unlocked":
183 renderIdentityList()
184 }
185 })
186 }
187
188 func renderCreateVault() {
189 // "No vault found" notice.
190 p := dom.CreateElement("p")
191 dom.SetTextContent(p, t("no_vault"))
192 dom.SetStyle(p, "marginTop", "0")
193 dom.SetStyle(p, "marginBottom", "12px")
194 dom.AppendChild(signerContent, p)
195
196 // Single shared password field at the top.
197 pwInput := dom.CreateElement("input")
198 dom.SetAttribute(pwInput, "type", "password")
199 dom.SetAttribute(pwInput, "placeholder", t("vault_password"))
200 dom.SetAttribute(pwInput, "class", "signer-input")
201 dom.SetAttribute(pwInput, "autocomplete", "new-password")
202 dom.SetAttribute(pwInput, "autocapitalize", "off")
203 dom.SetAttribute(pwInput, "autocorrect", "off")
204 dom.SetAttribute(pwInput, "spellcheck", "false")
205 dom.SetStyle(pwInput, "marginBottom", "8px")
206 dom.AppendChild(signerContent, pwInput)
207
208 errEl := dom.CreateElement("div")
209 dom.SetStyle(errEl, "color", "#e55")
210 dom.SetStyle(errEl, "fontSize", "12px")
211 dom.SetStyle(errEl, "minHeight", "14px")
212 dom.SetStyle(errEl, "marginBottom", "4px")
213 dom.AppendChild(signerContent, errEl)
214
215 getPw := func() string {
216 pw := dom.GetProperty(pwInput, "value")
217 if pw == "" {
218 dom.SetTextContent(errEl, "password required")
219 } else {
220 dom.SetTextContent(errEl, "")
221 }
222 return pw
223 }
224
225 sep0 := dom.CreateElement("hr")
226 dom.SetAttribute(sep0, "class", "signer-sep")
227 dom.AppendChild(signerContent, sep0)
228
229 // --- nsec import ---
230 nsecInput := dom.CreateElement("input")
231 dom.SetAttribute(nsecInput, "type", "password")
232 dom.SetAttribute(nsecInput, "placeholder", "nsec1...")
233 dom.SetAttribute(nsecInput, "class", "signer-input")
234 dom.SetAttribute(nsecInput, "autocomplete", "off")
235 dom.SetAttribute(nsecInput, "autocapitalize", "off")
236 dom.SetAttribute(nsecInput, "autocorrect", "off")
237 dom.SetAttribute(nsecInput, "spellcheck", "false")
238 dom.SetStyle(nsecInput, "marginBottom", "6px")
239 dom.AppendChild(signerContent, nsecInput)
240
241 nsecBtn := dom.CreateElement("button")
242 dom.SetAttribute(nsecBtn, "class", "signer-btn")
243 dom.SetTextContent(nsecBtn, "create vault with nsec")
244 dom.SetStyle(nsecBtn, "marginTop", "0")
245 dom.SetStyle(nsecBtn, "marginBottom", "8px")
246 dom.AddEventListener(nsecBtn, "click", signerCB(func() {
247 nsec := dom.GetProperty(nsecInput, "value")
248 pw := getPw()
249 if len(nsec) == 0 {
250 dom.SetTextContent(errEl, "nsec required")
251 return
252 }
253 if pw == "" {
254 return
255 }
256 dom.SetTextContent(nsecBtn, "...")
257 restore := func() { dom.SetTextContent(nsecBtn, "create vault with nsec") }
258 addAndFinish := func() {
259 signer.AddIdentity(nsec, func(ok bool) {
260 if !ok {
261 dom.SetTextContent(errEl, "invalid nsec or already present")
262 restore()
263 return
264 }
265 renderSignerUI()
266 })
267 }
268 signer.GetVaultStatus(func(status string) {
269 switch status {
270 case "none":
271 signer.CreateVault(pw, func(ok bool) {
272 if !ok {
273 dom.SetTextContent(errEl, "vault create failed")
274 restore()
275 return
276 }
277 addAndFinish()
278 })
279 case "locked":
280 signer.UnlockVault(pw, func(ok bool) {
281 if !ok {
282 signer.LastUnlockError(func(reason string) {
283 msg := "wrong password"
284 if reason == "kdf-failed" || reason == "decrypt-failed" {
285 msg = "unlock failed: " | reason
286 }
287 logActivity("unlock", "FAILED: " | reason, "")
288 dom.SetTextContent(errEl, msg)
289 restore()
290 })
291 return
292 }
293 logActivity("unlock", "vault unlocked", "")
294 addAndFinish()
295 })
296 default:
297 addAndFinish()
298 }
299 })
300 }))
301 dom.AppendChild(signerContent, nsecBtn)
302
303 sep1 := dom.CreateElement("hr")
304 dom.SetAttribute(sep1, "class", "signer-sep")
305 dom.AppendChild(signerContent, sep1)
306
307 // --- HD Vault (generate) ---
308 h3 := dom.CreateElement("h3")
309 dom.SetTextContent(h3, t("hd_keychain"))
310 dom.AppendChild(signerContent, h3)
311
312 createBtn := dom.CreateElement("button")
313 dom.SetAttribute(createBtn, "class", "signer-btn")
314 dom.SetTextContent(createBtn, t("generate_keychain"))
315 createCB := signerCB(func() {
316 pw := getPw()
317 if pw == "" {
318 return
319 }
320 dom.SetTextContent(createBtn, t("generating"))
321 dom.SetAttribute(createBtn, "disabled", "true")
322 // Empty mnemonic -> WASM generates a fresh BIP-39 phrase.
323 // Passing any non-empty non-mnemonic string (e.g. a label) would be
324 // treated as a BIP-39 phrase and PBKDF2 a deterministic seed.
325 signer.CreateHDVault(pw, "", func(mnemonic string) {
326 if mnemonic == "" {
327 dom.SetTextContent(createBtn, t("create_failed"))
328 dom.SetAttribute(createBtn, "disabled", "")
329 return
330 }
331 signer.DeriveIdentity("Identity 1", func(pk string) {
332 showMnemonicReveal(mnemonic)
333 })
334 })
335 })
336 dom.AddEventListener(createBtn, "click", createCB)
337 dom.AppendChild(signerContent, createBtn)
338
339 // --- Restore from mnemonic ---
340 sep2 := dom.CreateElement("hr")
341 dom.SetAttribute(sep2, "class", "signer-sep")
342 dom.AppendChild(signerContent, sep2)
343
344 h3r := dom.CreateElement("h3")
345 dom.SetTextContent(h3r, t("restore_seed"))
346 dom.AppendChild(signerContent, h3r)
347
348 mnInput := dom.CreateElement("textarea")
349 dom.SetAttribute(mnInput, "placeholder", t("seed_placeholder"))
350 dom.SetAttribute(mnInput, "class", "signer-input signer-textarea")
351 dom.SetAttribute(mnInput, "rows", "3")
352 dom.AppendChild(signerContent, mnInput)
353
354 idxInput := dom.CreateElement("input")
355 dom.SetAttribute(idxInput, "type", "number")
356 dom.SetAttribute(idxInput, "placeholder", t("account_placeholder"))
357 dom.SetAttribute(idxInput, "class", "signer-input")
358 dom.SetProperty(idxInput, "value", "1")
359 dom.AppendChild(signerContent, idxInput)
360
361 restoreBtn := dom.CreateElement("button")
362 dom.SetAttribute(restoreBtn, "class", "signer-btn")
363 dom.SetTextContent(restoreBtn, t("restore_keychain"))
364 restoreCB := signerCB(func() {
365 mn := dom.GetProperty(mnInput, "value")
366 pw := getPw()
367 if mn == "" || pw == "" {
368 return
369 }
370 idxStr := dom.GetProperty(idxInput, "value")
371 targetIdx := parseAccountIdx(idxStr)
372 if targetIdx < 1 {
373 targetIdx = 1
374 }
375 dom.SetTextContent(restoreBtn, t("restoring"))
376 dom.SetAttribute(restoreBtn, "disabled", "true")
377 signer.RestoreHDVault(pw, mn, "Identity 0", func(ok bool) {
378 if !ok {
379 dom.SetTextContent(restoreBtn, t("restore_failed"))
380 dom.SetAttribute(restoreBtn, "disabled", "")
381 return
382 }
383 dom.SetTextContent(restoreBtn, t("deriving_n") | " " | itoa(targetIdx) | "...")
384 restoreDeriveUpTo(targetIdx, 1, func() {
385 renderSignerUI()
386 })
387 })
388 })
389 dom.AddEventListener(restoreBtn, "click", restoreCB)
390 dom.AppendChild(signerContent, restoreBtn)
391
392 // --- Import vault file ---
393 sep3 := dom.CreateElement("hr")
394 dom.SetAttribute(sep3, "class", "signer-sep")
395 dom.AppendChild(signerContent, sep3)
396
397 h3b := dom.CreateElement("h3")
398 dom.SetTextContent(h3b, t("import_vault"))
399 dom.AppendChild(signerContent, h3b)
400
401 importBtn := dom.CreateElement("button")
402 dom.SetAttribute(importBtn, "class", "signer-btn signer-btn-secondary")
403 dom.SetTextContent(importBtn, t("choose_vault"))
404 importCB := signerCB(func() {
405 dom.PickFileText(".json", func(data string) {
406 if data == "" {
407 return
408 }
409 dom.SetTextContent(importBtn, t("restoring"))
410 dom.SetAttribute(importBtn, "disabled", "true")
411 signer.ImportVault(data, func(ok bool) {
412 if ok {
413 renderSignerUI()
414 } else {
415 dom.SetTextContent(importBtn, t("invalid_vault"))
416 dom.SetAttribute(importBtn, "disabled", "")
417 }
418 })
419 })
420 })
421 dom.AddEventListener(importBtn, "click", importCB)
422 dom.AppendChild(signerContent, importBtn)
423 }
424
425 // showMnemonicReveal displays the generated mnemonic for the user to write down.
426 func showMnemonicReveal(mnemonic string) {
427 dom.SetTextContent(signerContent, "")
428
429 h3 := dom.CreateElement("h3")
430 dom.SetTextContent(h3, t("write_seed"))
431 dom.AppendChild(signerContent, h3)
432
433 warn := dom.CreateElement("p")
434 dom.SetAttribute(warn, "class", "signer-warn")
435 dom.SetTextContent(warn, t("seed_warning"))
436 dom.AppendChild(signerContent, warn)
437
438 mnBox := dom.CreateElement("div")
439 dom.SetAttribute(mnBox, "class", "signer-mnemonic")
440
441 words := splitMnemonicWords(mnemonic)
442 for i, w := range words {
443 span := dom.CreateElement("span")
444 dom.SetAttribute(span, "class", "signer-word")
445 dom.SetTextContent(span, itoa(i+1) | ". " | w)
446 dom.AppendChild(mnBox, span)
447 }
448 dom.AppendChild(signerContent, mnBox)
449
450 copyBtn := dom.CreateElement("button")
451 dom.SetAttribute(copyBtn, "class", "signer-btn signer-btn-secondary")
452 dom.SetAttribute(copyBtn, "data-mn", mnemonic)
453 dom.SetTextContent(copyBtn, t("copy_clipboard"))
454 dom.SetAttribute(copyBtn, "data-label", t("copy_clipboard"))
455 dom.SetAttribute(copyBtn, "data-copied", t("copied"))
456 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)")
457 dom.AppendChild(signerContent, copyBtn)
458
459 doneBtn := dom.CreateElement("button")
460 dom.SetAttribute(doneBtn, "class", "signer-btn")
461 dom.SetTextContent(doneBtn, t("saved_it"))
462 doneCB := signerCB(func() {
463 renderSignerUI()
464 })
465 dom.AddEventListener(doneBtn, "click", doneCB)
466 dom.AppendChild(signerContent, doneBtn)
467 }
468
469 func splitMnemonicWords(s string) (ss []string) {
470 var words []string
471 start := -1
472 for i := 0; i < len(s); i++ {
473 if s[i] == ' ' {
474 if start >= 0 {
475 words = append(words, s[start:i])
476 start = -1
477 }
478 } else if start < 0 {
479 start = i
480 }
481 }
482 if start >= 0 {
483 words = append(words, s[start:])
484 }
485 return words
486 }
487
488
489 func renderUnlockVault() {
490 p := dom.CreateElement("p")
491 dom.SetTextContent(p, t("vault_locked"))
492 dom.SetStyle(p, "marginBottom", "12px")
493 dom.AppendChild(signerContent, p)
494
495 input := dom.CreateElement("input")
496 dom.SetAttribute(input, "type", "password")
497 dom.SetAttribute(input, "placeholder", t("password"))
498 dom.SetAttribute(input, "class", "signer-input")
499 dom.SetAttribute(input, "autocomplete", "current-password")
500 dom.SetAttribute(input, "autocapitalize", "off")
501 dom.SetAttribute(input, "autocorrect", "off")
502 dom.SetAttribute(input, "spellcheck", "false")
503 dom.AppendChild(signerContent, input)
504 dom.Focus(input)
505
506 btn := dom.CreateElement("button")
507 dom.SetAttribute(btn, "class", "signer-btn")
508 dom.SetTextContent(btn, t("unlock"))
509 cb := signerCB(func() {
510 pw := dom.GetProperty(input, "value")
511 if pw == "" {
512 return
513 }
514 dom.SetTextContent(p, t("deriving_key"))
515 dom.SetAttribute(btn, "disabled", "true")
516 logActivity("unlock", "deriving key from password", "")
517 signer.UnlockVault(pw, func(ok bool) {
518 if ok {
519 logActivity("unlock", "vault unlocked", "")
520 renderSignerUI()
521 return
522 }
523 signer.LastUnlockError(func(reason string) {
524 if reason == "" {
525 reason = "unknown"
526 }
527 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")
528 msg := "wrong_password"
529 if reason == "kdf-failed" || (len(reason) >= 14 && reason[:14] == "decrypt-failed") {
530 msg = "unlock_failed_kdf"
531 }
532 dom.SetTextContent(p, t(msg))
533 dom.SetAttribute(btn, "disabled", "")
534 })
535 })
536 })
537 dom.AddEventListener(btn, "click", cb)
538 dom.AddEnterKeyListener(input, cb)
539 dom.AppendChild(signerContent, btn)
540
541 // Reset button - lets user start over without digging into settings.
542 resetBtn := dom.CreateElement("button")
543 dom.SetAttribute(resetBtn, "class", "signer-btn")
544 dom.SetStyle(resetBtn, "background", "transparent")
545 dom.SetStyle(resetBtn, "color", "var(--muted)")
546 dom.SetStyle(resetBtn, "border", "1px solid var(--border)")
547 dom.SetStyle(resetBtn, "marginTop", "12px")
548 dom.SetTextContent(resetBtn, t("logout"))
549 dom.AddEventListener(resetBtn, "click", signerCB(func() {
550 if !dom.Confirm(t("logout_confirm")) {
551 return
552 }
553 signer.ResetExtension(func(ok bool) {
554 localstorage.RemoveItem(lsKeyPubkey)
555 dom.LocationAssign("/")
556 })
557 }))
558 dom.AppendChild(signerContent, resetBtn)
559 }
560
561 func renderIdentityList() {
562 signer.ListIdentities(func(list string) {
563 signer.IsHD(func(hd bool) {
564 dom.SetTextContent(signerContent, "")
565
566 h := dom.CreateElement("h3")
567 dom.SetTextContent(h, t("identities"))
568 dom.AppendChild(signerContent, h)
569
570 renderIdentitiesFromJSON(list, hd)
571
572 if hd {
573 renderHDControls()
574 }
575 renderLegacyAddIdentity()
576 renderBottomActions(hd)
577 })
578 })
579 }
580
581 func renderHDControls() {
582 addDiv := dom.CreateElement("div")
583 dom.SetAttribute(addDiv, "class", "signer-add")
584
585 nameInput := dom.CreateElement("input")
586 dom.SetAttribute(nameInput, "type", "text")
587 dom.SetAttribute(nameInput, "placeholder", t("nickname_placeholder"))
588 dom.SetAttribute(nameInput, "class", "signer-input")
589 dom.AppendChild(addDiv, nameInput)
590
591 deriveBtn := dom.CreateElement("button")
592 dom.SetAttribute(deriveBtn, "class", "signer-btn")
593 dom.SetTextContent(deriveBtn, t("derive_new"))
594 deriveCB := signerCB(func() {
595 name := dom.GetProperty(nameInput, "value")
596 dom.SetAttribute(deriveBtn, "disabled", "true")
597 dom.SetTextContent(deriveBtn, t("deriving"))
598 signer.DeriveIdentity(name, func(pk string) {
599 if pk == "" {
600 dom.SetAttribute(deriveBtn, "disabled", "")
601 dom.SetTextContent(deriveBtn, t("derive_new"))
602 return
603 }
604 // Stash name - kind 0 will be published after login when relays are live.
605 pendingK0Name = name
606 renderSignerUI()
607 })
608 })
609 dom.AddEventListener(deriveBtn, "click", deriveCB)
610 dom.AppendChild(addDiv, deriveBtn)
611 dom.AppendChild(signerContent, addDiv)
612 }
613
614 func parseAccountIdx(s string) (n int32) {
615 n = 0
616 for i := 0; i < len(s); i++ {
617 c := s[i]
618 if c >= '0' && c <= '9' {
619 n = n*10 + int32(c-'0')
620 } else {
621 break
622 }
623 }
624 return n
625 }
626
627 // restoreDeriveUpTo derives accounts from cur up to target sequentially.
628 func restoreDeriveUpTo(target, cur int32, done func()) {
629 if cur > target {
630 done()
631 return
632 }
633 signer.DeriveIdentity("Identity " | itoa(cur), func(pk string) {
634 restoreDeriveUpTo(target, cur+1, done)
635 })
636 }
637
638 // flushPendingK0 publishes a kind 0 if one was queued during identity derivation.
639 // Called from showApp() after relays are live.
640 func flushPendingK0() {
641 name := pendingK0Name
642 if name == "" || pubhex == "" {
643 return
644 }
645 pendingK0Name = ""
646 publishKind0ForIdentity(pubhex, name, func() {})
647 }
648
649
650 // publishKind0ForIdentity switches to the given identity, signs a kind 0 event
651 // with the nickname, publishes it to all relays, then calls done.
652 func publishKind0ForIdentity(pk, name string, done func()) {
653 signer.SwitchIdentity(pk, func(ok bool) {
654 if !ok {
655 done()
656 return
657 }
658 content := "{\"name\":" | helpers.JsonString(name) | ",\"display_name\":" | helpers.JsonString(name) | "}"
659 ts := dom.NowSeconds()
660 unsigned := "{\"kind\":0,\"content\":" | helpers.JsonString(content) |
661 ",\"tags\":[],\"created_at\":" | i64toa(ts) |
662 ",\"pubkey\":\"" | pk | "\"}"
663 dom.ConsoleLog("[signer-panel] signing kind 0 for " | pk[:8])
664 signer.SignEvent(unsigned, func(signed string) {
665 if len(signed) > 0 {
666 dom.PostToSW("[\"EVENT\"," | signed | "]")
667 dom.ConsoleLog("[signer-panel] published kind 0 for " | pk[:8] | " name=" | name)
668 } else {
669 dom.ConsoleLog("[signer-panel] sign failed for " | pk[:8] | " - check console for [signer] errors")
670 }
671 done()
672 })
673 })
674 }
675
676 func renderLegacyAddIdentity() {
677 addDiv := dom.CreateElement("div")
678 dom.SetAttribute(addDiv, "class", "signer-add")
679
680 addInput := dom.CreateElement("input")
681 dom.SetAttribute(addInput, "type", "password")
682 dom.SetAttribute(addInput, "placeholder", "nsec1...")
683 dom.SetAttribute(addInput, "class", "signer-input")
684 dom.SetAttribute(addInput, "autocomplete", "off")
685 dom.SetAttribute(addInput, "autocapitalize", "off")
686 dom.SetAttribute(addInput, "autocorrect", "off")
687 dom.SetAttribute(addInput, "spellcheck", "false")
688 dom.AppendChild(addDiv, addInput)
689
690 addErr := dom.CreateElement("div")
691 dom.SetStyle(addErr, "color", "#e55")
692 dom.SetStyle(addErr, "fontSize", "12px")
693 dom.SetStyle(addErr, "minHeight", "14px")
694 dom.SetStyle(addErr, "marginBottom", "4px")
695 dom.AppendChild(addDiv, addErr)
696
697 addBtn := dom.CreateElement("button")
698 dom.SetAttribute(addBtn, "class", "signer-btn")
699 dom.SetTextContent(addBtn, t("add"))
700 addCB := signerCB(func() {
701 nsec := dom.GetProperty(addInput, "value")
702 if nsec == "" {
703 dom.SetTextContent(addErr, "nsec required")
704 return
705 }
706 dom.SetTextContent(addErr, "")
707 signer.AddIdentity(nsec, func(ok bool) {
708 if ok {
709 dom.SetProperty(addInput, "value", "")
710 renderSignerUI()
711 return
712 }
713 dom.SetTextContent(addErr, "invalid nsec or already present")
714 })
715 })
716 dom.AddEventListener(addBtn, "click", addCB)
717 dom.AppendChild(addDiv, addBtn)
718 dom.AppendChild(signerContent, addDiv)
719 }
720
721 func renderBottomActions(hd bool) {
722 actions := dom.CreateElement("div")
723 dom.SetAttribute(actions, "class", "signer-actions")
724
725 // Show seed phrase button (HD only).
726 if hd {
727 seedBtn := dom.CreateElement("button")
728 dom.SetAttribute(seedBtn, "class", "signer-btn signer-btn-secondary")
729 dom.SetTextContent(seedBtn, t("show_seed"))
730 seedCB := signerCB(func() {
731 signer.GetMnemonic(func(m string) {
732 if m != "" {
733 showMnemonicReveal(m)
734 }
735 })
736 })
737 dom.AddEventListener(seedBtn, "click", seedCB)
738 dom.AppendChild(actions, seedBtn)
739 }
740
741 // Export vault button.
742 exportBtn := dom.CreateElement("button")
743 dom.SetAttribute(exportBtn, "class", "signer-btn signer-btn-secondary")
744 dom.SetTextContent(exportBtn, t("export_vault"))
745 exportCB := signerCB(func() {
746 showExportPasswordPrompt()
747 })
748 dom.AddEventListener(exportBtn, "click", exportCB)
749 dom.AppendChild(actions, exportBtn)
750
751 // Add HD keychain - navigates to the create-new-HD-keychain flow.
752 addHDBtn := dom.CreateElement("button")
753 dom.SetAttribute(addHDBtn, "class", "signer-btn signer-btn-secondary")
754 dom.SetTextContent(addHDBtn, "Add HD keychain")
755 addHDCB := signerCB(func() {
756 dom.SetTextContent(signerContent, "")
757 renderCreateVault()
758 })
759 dom.AddEventListener(addHDBtn, "click", addHDCB)
760 dom.AppendChild(actions, addHDBtn)
761
762 // Lock vault button.
763 lockBtn := dom.CreateElement("button")
764 dom.SetAttribute(lockBtn, "class", "signer-btn signer-btn-secondary")
765 dom.SetTextContent(lockBtn, t("lock_vault"))
766 lockCB := signerCB(func() {
767 signer.LockVault(func() {
768 renderSignerUI()
769 })
770 })
771 dom.AddEventListener(lockBtn, "click", lockCB)
772 dom.AppendChild(actions, lockBtn)
773
774 // Logout (wipes the vault and reloads).
775 resetBtn := dom.CreateElement("button")
776 dom.SetAttribute(resetBtn, "class", "signer-btn signer-btn-danger")
777 dom.SetTextContent(resetBtn, t("logout"))
778 resetCB := signerCB(func() {
779 if !dom.Confirm(t("logout_confirm")) {
780 return
781 }
782 signer.ResetExtension(func(ok bool) {
783 localstorage.RemoveItem(lsKeyPubkey)
784 dom.LocationAssign("/")
785 })
786 })
787 dom.AddEventListener(resetBtn, "click", resetCB)
788 dom.AppendChild(actions, resetBtn)
789
790 dom.AppendChild(signerContent, actions)
791 }
792
793 func showExportPasswordPrompt() {
794 // Replace signer content with password prompt.
795 dom.SetTextContent(signerContent, "")
796
797 label := dom.CreateElement("p")
798 dom.SetTextContent(label, t("export_password_prompt"))
799 dom.SetStyle(label, "marginBottom", "8px")
800 dom.AppendChild(signerContent, label)
801
802 pwInput := dom.CreateElement("input")
803 dom.SetAttribute(pwInput, "type", "password")
804 dom.SetAttribute(pwInput, "placeholder", t("password"))
805 dom.SetAttribute(pwInput, "class", "signer-input")
806 dom.SetAttribute(pwInput, "autocomplete", "new-password")
807 dom.SetAttribute(pwInput, "autocapitalize", "off")
808 dom.SetAttribute(pwInput, "autocorrect", "off")
809 dom.SetAttribute(pwInput, "spellcheck", "false")
810 dom.AppendChild(signerContent, pwInput)
811
812 confirmInput := dom.CreateElement("input")
813 dom.SetAttribute(confirmInput, "type", "password")
814 dom.SetAttribute(confirmInput, "placeholder", t("confirm_password"))
815 dom.SetAttribute(confirmInput, "class", "signer-input")
816 dom.SetAttribute(confirmInput, "autocomplete", "new-password")
817 dom.SetAttribute(confirmInput, "autocapitalize", "off")
818 dom.SetAttribute(confirmInput, "autocorrect", "off")
819 dom.SetAttribute(confirmInput, "spellcheck", "false")
820 dom.SetStyle(confirmInput, "marginTop", "8px")
821 dom.AppendChild(signerContent, confirmInput)
822
823 errEl := dom.CreateElement("p")
824 dom.SetStyle(errEl, "color", "var(--error, #e55)")
825 dom.SetStyle(errEl, "fontSize", "13px")
826 dom.SetStyle(errEl, "marginTop", "4px")
827 dom.AppendChild(signerContent, errEl)
828
829 btnRow := dom.CreateElement("div")
830 dom.SetStyle(btnRow, "display", "flex")
831 dom.SetStyle(btnRow, "gap", "8px")
832 dom.SetStyle(btnRow, "marginTop", "12px")
833
834 cancelBtn := dom.CreateElement("button")
835 dom.SetAttribute(cancelBtn, "class", "signer-btn signer-btn-secondary")
836 dom.SetTextContent(cancelBtn, t("cancel"))
837 dom.AddEventListener(cancelBtn, "click", signerCB(func() {
838 renderSignerUI()
839 }))
840 dom.AppendChild(btnRow, cancelBtn)
841
842 goBtn := dom.CreateElement("button")
843 dom.SetAttribute(goBtn, "class", "signer-btn signer-btn-primary")
844 dom.SetTextContent(goBtn, t("export_vault"))
845 dom.AddEventListener(goBtn, "click", signerCB(func() {
846 pw := dom.GetProperty(pwInput, "value")
847 confirm := dom.GetProperty(confirmInput, "value")
848 if pw == "" {
849 dom.SetTextContent(errEl, t("password_required"))
850 return
851 }
852 if pw != confirm {
853 dom.SetTextContent(errEl, t("passwords_mismatch"))
854 return
855 }
856 dom.SetTextContent(goBtn, "...")
857 signer.ExportVault(pw, func(data string) {
858 if data == "" {
859 dom.SetTextContent(errEl, t("export_failed"))
860 dom.SetTextContent(goBtn, t("export_vault"))
861 return
862 }
863 dom.DownloadText("smesh-vault.json", data, "application/json")
864 renderSignerUI()
865 })
866 }))
867 dom.AppendChild(btnRow, goBtn)
868 dom.AppendChild(signerContent, btnRow)
869
870 dom.Focus(pwInput)
871 }
872
873 func renderIdentitiesFromJSON(listJSON string, hd bool) {
874 i := 0
875 idxNum := 0
876 for i < len(listJSON) && listJSON[i] != '[' {
877 i++
878 }
879 i++
880 for i < len(listJSON) {
881 for i < len(listJSON) && listJSON[i] != '{' && listJSON[i] != ']' {
882 i++
883 }
884 if i >= len(listJSON) || listJSON[i] == ']' {
885 break
886 }
887 end := i + 1
888 depth := 1
889 for end < len(listJSON) && depth > 0 {
890 if listJSON[end] == '{' {
891 depth++
892 } else if listJSON[end] == '}' {
893 depth--
894 } else if listJSON[end] == '"' {
895 end++
896 for end < len(listJSON) && listJSON[end] != '"' {
897 if listJSON[end] == '\\' {
898 end++
899 }
900 end++
901 }
902 }
903 end++
904 }
905 obj := listJSON[i:end]
906 pk := helpers.JsonGetString(obj, "pubkey")
907 name := helpers.JsonGetString(obj, "name")
908 if pk == "" {
909 i = end
910 idxNum++
911 continue
912 }
913
914 row := dom.CreateElement("div")
915 dom.SetAttribute(row, "class", "signer-identity")
916
917 label := dom.CreateElement("span")
918 display := pk[:8] | "..."
919 if name != "" {
920 display = name | " (" | pk[:8] | "...)"
921 }
922 if hd {
923 display = "#" | itoa(idxNum) | " " | display
924 }
925 dom.SetTextContent(label, display)
926 dom.AppendChild(row, label)
927
928 btns := dom.CreateElement("span")
929
930 // HD identity 0 is the seed - skip it entirely.
931 if hd && idxNum == 0 {
932 i = end
933 idxNum++
934 continue
935 }
936
937 // Switch / current-identity indicator.
938 if pubhex != "" && pk == pubhex {
939 loggedIn := dom.CreateElement("span")
940 dom.SetAttribute(loggedIn, "class", "signer-btn-sm")
941 dom.SetStyle(loggedIn, "color", "var(--muted)")
942 dom.SetStyle(loggedIn, "opacity", "0.6")
943 dom.SetStyle(loggedIn, "cursor", "default")
944 dom.SetStyle(loggedIn, "pointerEvents", "none")
945 dom.SetTextContent(loggedIn, "Logged in")
946 dom.AppendChild(btns, loggedIn)
947 } else {
948 switchBtn := dom.CreateElement("button")
949 dom.SetAttribute(switchBtn, "class", "signer-btn-sm")
950 dom.SetStyle(switchBtn, "color", "var(--accent)")
951 dom.SetStyle(switchBtn, "fontWeight", "bold")
952 dom.SetTextContent(switchBtn, "Login")
953 switchPK := pk
954 switchCB := signerCB(func() {
955 signer.SwitchIdentity(switchPK, func(ok bool) {
956 if ok {
957 hideSignerPanel()
958 pubhex = switchPK
959 localstorage.SetItem(lsKeyPubkey, pubhex)
960 // Reset timestamp guards so the new identity's
961 // replaceable events aren't rejected as stale.
962 profileTs = 0
963 contactTs = 0
964 muteTs = 0
965 relayListTs = 0
966 inboxTs = 0
967 feedSubscribed = false
968 clearChildren(root)
969 bootstrapEncKey(func() {
970 showApp()
971 })
972 }
973 })
974 })
975 dom.AddEventListener(switchBtn, "click", switchCB)
976 dom.AppendChild(btns, switchBtn)
977 }
978
979 // Publish kind 0 button.
980 pubBtn := dom.CreateElement("button")
981 dom.SetAttribute(pubBtn, "class", "signer-btn-sm")
982 dom.SetTextContent(pubBtn, t("publish"))
983 pubPK := pk
984 pubName := name
985 pubCB := signerCB(func() {
986 dom.SetTextContent(pubBtn, "...")
987 dom.SetAttribute(pubBtn, "disabled", "true")
988 publishKind0ForIdentity(pubPK, pubName, func() {
989 dom.SetTextContent(pubBtn, t("publish"))
990 dom.SetAttribute(pubBtn, "disabled", "")
991 })
992 })
993 dom.AddEventListener(pubBtn, "click", pubCB)
994 dom.AppendChild(btns, pubBtn)
995
996 // Remove button.
997 rmBtn := dom.CreateElement("button")
998 dom.SetAttribute(rmBtn, "class", "signer-btn-sm signer-btn-danger")
999 dom.SetTextContent(rmBtn, "\u00d7")
1000 rmPK := pk
1001 rmCB := signerCB(func() {
1002 signer.RemoveIdentity(rmPK, func(ok bool) {
1003 if ok {
1004 renderSignerUI()
1005 }
1006 })
1007 })
1008 dom.AddEventListener(rmBtn, "click", rmCB)
1009 dom.AppendChild(btns, rmBtn)
1010
1011 dom.AppendChild(row, btns)
1012 dom.AppendChild(signerContent, row)
1013 i = end
1014 idxNum++
1015 }
1016 }
1017