clone

tiny dependency-free nostr libraries for plain html, each under 5kb and named for an element. public domain

main
nobel / iodine / iodine.js
iodine.js 3.9K
1(()=>{2 let g = window, d = document, J = JSON3 if (g.iodine) return4 let send = (el, type, detail)=>el.dispatchEvent(new CustomEvent("nio:" + type, {detail, cancelable:1, bubbles:1, composed:1}))5 let signer = ()=>g.nostr ?? helium.signer6 let hex = (s)=>{7 if (/^[0-9a-f]{64}$/.test(s)) return s8 let x = neon.decode(s).data9 return x.pubkey ?? x10 }11 let now = ()=>Date.now() / 1e3 | 012 let past = ()=>now() - (Math.random() * 172800 | 0)13 let relays = (el)=>el?.closest?.("[nhy-relay]")?.getAttribute("nhy-relay").split(/\s+/) ?? g.nhyCfg?.relays ?? []14 // Newest kind 10050 "relay" tags of pk, or [] after ~3s.15 let lookup = (pk, el)=>new Promise((res)=>{16 let rs = relays(el), best, n = 0, c17 let end = ()=>res(best?.tags.filter((t)=>t[0] == "relay").map((t)=>t[1]) ?? [], c?.close())18 c = hydrogen.req({19 relays:rs,20 filters:[{kinds:[10050], authors:[pk], limit:1}],21 onevent:(e)=>e.pubkey == pk && !(best?.created_at >= e.created_at) && (best = e),22 oneose:(u)=>rs.includes(u) && ++n == rs.length && end()23 })24 rs.length || end()25 setTimeout(end, 3e3)26 })27 let wrap = async(rumor, to)=>{28 let s = signer(), sk = helium.generate()29 let seal = await s.signEvent({pubkey:rumor.pubkey, kind:13, created_at:past(), tags:[], content:await s.nip44.encrypt(to, J.stringify(rumor))})30 return helium.finish({kind:1059, created_at:past(), tags:[["p", to]], content:await fluorine.nip44(sk).encrypt(to, J.stringify(seal))}, sk)31 }32 let unwrap = async(w)=>{33 try {34 let s = signer(), seal = J.parse(await s.nip44.decrypt(w.pubkey, w.content))35 if (seal.kind != 13 || !await helium.check(seal)) return36 let r = J.parse(await s.nip44.decrypt(seal.pubkey, seal.content))37 if (r.pubkey == seal.pubkey) return r38 } catch {}39 }40 let dm = async(to, content, rs, el = d)=>{41 let me = await signer().getPublicKey()42 to = hex(to)43 let rumor = {pubkey:me, created_at:now(), kind:14, tags:[["p", to]], content}, detail = {rumor, recipients:[...new Set([to, me])]}44 if (!send(el, "send", detail)) return45 rumor.id = await helium.id(rumor)46 let results = (await Promise.all(detail.recipients.map(async(pk)=>{47 let w = await wrap(rumor, pk), r = rs ?? await lookup(pk, el)48 return hydrogen.pub(w, r.length ? r : relays(el))49 }))).flat()50 send(el, "sent", {...detail, results})51 return results52 }53 let box = async(el)=>{54 let me = await signer().getPublicKey()55 el.__nio = await lookup(me, el)56 el.setAttribute("nar-ignore", "")57 el.setAttribute("nxe-filter", J.stringify({kinds:[1059], "#p":[me]}))58 el.dispatchEvent(new CustomEvent("nxe:process", {bubbles:1}))59 }60 let form = (el)=>el.addEventListener("submit", async(evt)=>{61 evt.preventDefault()62 let fd = new FormData(el)63 try {64 let rs = await dm(el.getAttribute("nio-to") || fd.get("to"), fd.get("content") ?? "", null, el)65 if (rs?.some((r)=>r.ok)) el.reset()66 } catch(error) {67 send(el, "error", {error})68 }69 })70 let init = (el)=>{71 if (el.__nio !== undefined || el.closest("[nio-ignore]")) return72 el.__nio = 073 el.hasAttribute("nio-inbox") ? box(el).catch((error)=>send(el, "error", {error})) : form(el)74 }75 let process = (n)=>{76 if (n.matches?.("[nio-inbox],[nio-send]")) init(n)77 n.querySelectorAll?.("[nio-inbox],[nio-send]").forEach(init)78 }79 d.addEventListener("nhy:config", (evt)=>{80 let c = evt.detail.cfg, el = c.elt81 if (!el?.hasAttribute?.("nio-inbox")) return82 if (el.__nio?.length) c.relays = el.__nio83 c.map = async(w)=>{84 let r = await unwrap(w), x = el.getAttribute("nio-with")85 try { x &&= hex(x) } catch {}86 if (r && (!x || [r.pubkey, ...r.tags.map((t)=>t[0] == "p" && t[1])].includes(x)) && send(el, "message", {wrap:w, rumor:r})) return r87 }88 })89 g.iodine = {send:dm, wrap, unwrap, relays:lookup}90 let start = ()=>{91 new MutationObserver((recs)=>recs.forEach((r)=>r.addedNodes.forEach(process))).observe(d.documentElement, {childList:1, subtree:1})92 process(d.body)93 }94 d.readyState == "loading" ? d.addEventListener("DOMContentLoaded", start) : start()95})()