clone

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

main
nobel / chlorine / chlorine.js
chlorine.js 4.6K
1(()=>{2 let g = globalThis, d = g.document, K = "ncl:session", pool = {}, wait = {}, keys = {}, n = 0, s, C3 if (g.chlorine) return4 let emit = (type, detail, el = d ?? g)=>el.dispatchEvent?.(new CustomEvent("ncl:" + type, {detail, cancelable:1, bubbles:1, composed:1}))5 let store = (v)=>{6 try { v ? localStorage[K] = JSON.stringify(v) : localStorage.removeItem(K) } catch {}7 }8 let ck = (pk)=>keys[s.sk + pk] ||= fluorine.key(s.sk, pk)9 let tx = (url, m)=>{10 let w = pool[url]11 if (!w){12 w = pool[url] = new WebSocket(url)13 w.q = []14 w.onopen = ()=>{15 w.send(JSON.stringify(["REQ", "ncl", {kinds:[24133], "#p":[s.pk], limit:0}]))16 w.q.splice(0).forEach((x)=>w.send(x))17 }18 w.onmessage = async(m)=>{19 try {20 let [t, , e] = JSON.parse(m.data), r21 if (t != "EVENT" || e.kind != 24133) return22 r = JSON.parse(await fluorine.decrypt(e.content, await ck(e.pubkey)))23 if (e.pubkey == s.remote) wait[r.id]?.(r)24 else if (!s.remote && r.result == s.secret) s.remote = e.pubkey, s.hook()25 } catch {}26 }27 w.onclose = ()=>pool[url] == w && (delete pool[url], s?.relays.includes(url) && setTimeout(()=>s && !pool[url] && tx(url), 3e3))28 }29 if (m) m = JSON.stringify(m), w.readyState == 1 ? w.send(m) : w.q.push(m)30 }31 let close = ()=>{32 for (let u in pool) pool[u].close(), delete pool[u]33 }34 let start = (x)=>{35 close()36 s = {sk:helium.generate(), ...x}37 s.pk = helium.pub(s.sk)38 s.relays.forEach((u)=>tx(u))39 }40 let rpc = async(method, params = [], id = "ncl" + ++n + Date.now())=>{41 let e = await helium.finish({kind:24133, created_at:Date.now() / 1e3 | 0, tags:[["p", s.remote]], content:await fluorine.encrypt(JSON.stringify({id, method, params}), await ck(s.remote))}, s.sk)42 return new Promise((res, rej)=>{43 let t, arm = ()=>(clearTimeout(t), t = setTimeout(()=>wait[id]({error:"timeout"}), g.nclCfg?.timeout ?? 3e4))44 wait[id] = (r)=>{45 if (r.result == "auth_url") return arm(emit("auth", {url:r.error}) && g.open?.(r.error))46 clearTimeout(t)47 delete wait[id]48 r.error ? rej(Error(r.error)) : res(r.result)49 }50 arm()51 s.relays.forEach((u)=>tx(u, ["EVENT", e]))52 })53 }54 let use = ()=>{55 if (g.nostr != signer) C.previous = g.nostr, g.nostr = signer56 d?.documentElement.setAttribute("ncl-connected", s.user)57 emit("connected", {pubkey:s.user})58 return s.user59 }60 let ready = async()=>{61 s.user = await rpc("get_public_key")62 store(s)63 return use()64 }65 let connect = async(uri)=>{66 let u = new URL(uri.trim()), p = u.searchParams67 start({remote:u.hostname, relays:p.getAll("relay"), secret:p.get("secret")})68 try {69 await rpc("connect", [s.remote, s.secret].filter((x)=>x))70 return await ready()71 } catch(error) {72 close()73 s = null74 throw error75 }76 }77 let listen = ({relays = g.nclCfg?.relays, ...o} = {})=>{78 start({relays, secret:helium.generate().slice(0, 16)})79 let q = new URLSearchParams()80 relays.forEach((r)=>q.append("relay", r))81 q.set("secret", s.secret)82 for (let k in o) q.set(k, o[k])83 return {uri:`nostrconnect://${s.pk}?${q}`, done:new Promise((res, rej)=>s.hook = ()=>ready().then(res, rej))}84 }85 let disconnect = ()=>{86 close()87 s = null88 store()89 if (g.nostr == signer) g.nostr = C.previous90 d?.documentElement.removeAttribute("ncl-connected")91 emit("disconnected")92 }93 let pair = (x)=>({encrypt:(pk, text)=>rpc(x + "_encrypt", [pk, text]), decrypt:(pk, text)=>rpc(x + "_decrypt", [pk, text])})94 let signer = {95 getPublicKey:async()=>s.user,96 signEvent:async({kind, content, tags = [], created_at = Date.now() / 1e3 | 0})=>JSON.parse(await rpc("sign_event", [JSON.stringify({kind, content, tags, created_at})])),97 nip44:pair("nip44"),98 nip04:pair("nip04")99 }100 C = g.chlorine = {connect, listen, disconnect, rpc, signer, get session(){ return s }}101 try { s = JSON.parse(localStorage[K]) } catch {}102 if (s) s.relays.forEach((u)=>tx(u)), use()103 if (!d) return104 let fail = (el)=>(error)=>emit("error", {error}, el)105 d.addEventListener("submit", (evt, f = evt.target)=>{106 if (!f.hasAttribute("ncl-connect")) return107 evt.preventDefault()108 connect(new FormData(f).get("bunker")).catch(fail(f))109 })110 d.addEventListener("click", (evt)=>evt.target.closest?.("[ncl-disconnect]") && disconnect())111 let boot = (el)=>{112 if (s || !(el = d.querySelector("[ncl-nostrconnect]"))) return113 let a = (k)=>el.getAttribute("ncl-" + k), l = listen({relays:el.closest("[ncl-relay]")?.getAttribute("ncl-relay").split(/\s+/), name:a("name") || d.title, ...a("perms") && {perms:a("perms")}})114 if (emit("listen", {uri:l.uri}, el)) el.tagName == "A" ? el.href = l.uri : el.textContent = l.uri115 l.done.catch(fail(el))116 }117 d.readyState == "loading" ? d.addEventListener("DOMContentLoaded", ()=>boot()) : boot()118})()