hydrogen (relays and hooks) and helium (keys and signatures) are the two primitives everything else builds on. The old helium, NIP-77 sync, moves to lithium. Prefixes follow: nkr- becomes nhe-, sync's nhe- becomes nli-. helium migrates a stored nkr:key to nhe:key. chlorine, lithium, oganesson and radon now also start when loaded after the page, like xenon, europium, iodine and praseodymium already did.
- commit
- f73872f96040f6a677cc3190f73d38458748d923
- parent
- def2bdd37c699269249d45f6d79c0009e70a53b0
- author
- Dami <dami.etoile@gmail.com>
| M | chlorine/ | 6 | +- |
| M | chlorine/ | 13 | +- |
| M | demo/ | 6 | +- |
| M | demo/ | 6 | +- |
| M | demo/ | 6 | +- |
| M | demo/ | 6 | +- |
| M | demo/ | 2 | +- |
| M | fluorine/ | 12 | +- |
| M | fluorine/ | 6 | +- |
| M | gold/ | 6 | +- |
| M | helium/ | 57 | ++++----- |
| M | helium/ | 201 | +++++++++++++----------------- |
| M | hydrogen/ | 2 | +- |
| M | hydrogen/ | 2 | +- |
| M | iodine/ | 4 | +- |
| M | iodine/ | 10 | +- |
| D | krypton/ | 39 | ------ |
| D | krypton/ | 96 | -------------- |
| A | lithium/ | 48 | +++++++ |
| A | lithium/ | 123 | ++++++++++++++++++ |
| M | mercury/ | 4 | +- |
| M | mercury/ | 2 | +- |
| M | oganesson/ | 4 | +- |
| M | oganesson/ | 5 | +- |
| M | radon/ | 5 | +- |
| M | server.ts | 8 | +- |
| M | test/ | 30 | ++-- |
| M | test/ | 2 | +- |
| M | test/ | 32 | ++-- |
| M | test/ | 24 | ++-- |
| M | test/ | 88 | +++++-------- |
| M | test/ | 6 | +- |
| D | test/ | 44 | ------- |
| A | test/ | 62 | +++++++++ |
| M | test/ | 18 | +- |
| M | test/ | 26 | ++-- |
| M | test/ | 6 | +- |
@@ -1,9 +1,9 @@
11 # 🔌 chlorine.js - remote signing22 3-chlorine.js is a miniature NIP-46 client: it talks to a signer that lives elsewhere, a bunker or a phone app, over relays. It connects with a `bunker://` URI or by showing a `nostrconnect://` URI for the signer to scan, and remembers the session so reloads reconnect without a new handshake. It composes by wearing the NIP-07 face: once connected, `window.nostr` is chlorine's signer, so xenon.js publishes through it, krypton.js steps aside, and every other nobel library just works.3+chlorine.js is a miniature NIP-46 client: it talks to a signer that lives elsewhere, a bunker or a phone app, over relays. It connects with a `bunker://` URI or by showing a `nostrconnect://` URI for the signer to scan, and remembers the session so reloads reconnect without a new handshake. It composes by wearing the NIP-07 face: once connected, `window.nostr` is chlorine's signer, so xenon.js publishes through it, helium.js steps aside, and every other nobel library just works.44 55 ```html6-<script src="krypton.js"></script>6+<script src="helium.js"></script>77 <script src="fluorine.js"></script>88 <script src="chlorine.js"></script>99 <script src="xenon.js"></script>@@ -51,4 +51,4 @@ Dispatched on `document` (or the form or link involved), or on `globalThis` outs
5151 | `chlorine.previous` | the `window.nostr` that was there before |5252 | `chlorine.session` | `{sk, pk, remote, relays, secret, user}` or `null` |5353 54-Each signature, encryption and decryption is a relay round trip to the signer, so expect it to take a moment rather than be instant. The session, including the throwaway client key, is kept in `localStorage` under `ncl:session`. Requires krypton.js and fluorine.js. Tested against `nak bunker` (both flows) on tinyrelay and the dev relay.54+Each signature, encryption and decryption is a relay round trip to the signer, so expect it to take a moment rather than be instant. The session, including the throwaway client key, is kept in `localStorage` under `ncl:session`. Requires helium.js and fluorine.js. Tested against `nak bunker` (both flows) on tinyrelay and the dev relay.@@ -33,12 +33,12 @@
3333 }3434 let start = (x)=>{3535 close()36- s = {sk:krypton.generate(), ...x}37- s.pk = krypton.pub(s.sk)36+ s = {sk:helium.generate(), ...x}37+ s.pk = helium.pub(s.sk)3838 s.relays.forEach((u)=>tx(u))3939 }4040 let rpc = async(method, params = [], id = "ncl" + ++n + Date.now())=>{41- let e = await krypton.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)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)4242 return new Promise((res, rej)=>{4343 let t, arm = ()=>(clearTimeout(t), t = setTimeout(()=>wait[id]({error:"timeout"}), g.nclCfg?.timeout ?? 3e4))4444 wait[id] = (r)=>{@@ -75,7 +75,7 @@
7575 }7676 }7777 let listen = ({relays = g.nclCfg?.relays, ...o} = {})=>{78- start({relays, secret:krypton.generate().slice(0, 16)})78+ start({relays, secret:helium.generate().slice(0, 16)})7979 let q = new URLSearchParams()8080 relays.forEach((r)=>q.append("relay", r))8181 q.set("secret", s.secret)@@ -108,10 +108,11 @@
108108 connect(new FormData(f).get("bunker")).catch(fail(f))109109 })110110 d.addEventListener("click", (evt)=>evt.target.closest?.("[ncl-disconnect]") && disconnect())111- d.addEventListener("DOMContentLoaded", (el)=>{111+ let boot = (el)=>{112112 if (s || !(el = d.querySelector("[ncl-nostrconnect]"))) return113113 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")}})114114 if (emit("listen", {uri:l.uri}, el)) el.tagName == "A" ? el.href = l.uri : el.textContent = l.uri115115 l.done.catch(fail(el))116- })116+ }117+ d.readyState == "loading" ? d.addEventListener("DOMContentLoaded", ()=>boot()) : boot()117118 })()@@ -6,7 +6,7 @@
66 <title>europium demo</title>77 <script src="relay.js"></script>88 <script src="../neon/neon.js"></script>9-<script src="../krypton/krypton.js"></script>9+<script src="../helium/helium.js"></script>1010 <script src="../argon/argon.js"></script>1111 <script src="../hydrogen/hydrogen.js"></script>1212 <script src="../xenon/xenon.js"></script>@@ -40,8 +40,8 @@
4040 </main>4141 <script>4242 (async()=>{43- let sk = krypton.key(), pk = krypton.pub(sk), now = Date.now() / 1e3 | 044- let sign = (kind, content, tags = [])=>krypton.finish({kind, created_at:now, tags, content}, sk)43+ let sk = helium.key(), pk = helium.pub(sk), now = Date.now() / 1e3 | 044+ let sign = (kind, content, tags = [])=>helium.finish({kind, created_at:now, tags, content}, sk)4545 let profile = await sign(0, JSON.stringify({name:"Ada"}))4646 let note = await sign(1, "hello <b>europium</b> https://example.com/x")4747 let reply = await sign(1, "a reply", [["e", note.id, relay, "root"], ["p", pk]])@@ -6,7 +6,7 @@
66 <title>nobel demo</title>77 <script src="relay.js"></script>88 <script src="../neon/neon.js"></script>9-<script src="../krypton/krypton.js"></script>9+<script src="../helium/helium.js"></script>1010 <script src="../argon/argon.js"></script>1111 <script src="../hydrogen/hydrogen.js"></script>1212 <script src="../xenon/xenon.js"></script>@@ -42,7 +42,7 @@
4242 <body>4343 <main>4444 <h1><b>nobel</b> demo</h1>45- <p class="sub">xenon + krypton + argon + neon + radon, in plain HTML.</p>45+ <p class="sub">xenon + helium + argon + neon + radon, in plain HTML.</p>4646 4747 <div class="me">signed in as <b id="me" nrn-field="name">…</b> <span id="npub"></span></div>4848 @@ -84,7 +84,7 @@
8484 document.addEventListener("nxe:error", (evt)=>log.textContent = String(evt.detail.error))8585 // Unfilled tag placeholders read better empty.8686 document.addEventListener("nhy:event", (evt)=>evt.detail.fields["#t"] ??= "")87- let who = (window.nostr ?? krypton.signer).getPublicKey()87+ let who = (window.nostr ?? helium.signer).getPublicKey()8888 who.then((pk)=>{8989 me.setAttribute("nrn-profile", pk)9090 npub.textContent = neon.encode("npub", pk).slice(0, 16) + "…"@@ -10,7 +10,7 @@
1010 let q = new URLSearchParams(location.search)1111 </script>1212 <script src="../neon/neon.js"></script>13-<script src="../krypton/krypton.js"></script>13+<script src="../helium/helium.js"></script>1414 <script src="../fluorine/fluorine.js"></script>1515 <script src="../argon/argon.js"></script>1616 <script src="../hydrogen/hydrogen.js"></script>@@ -45,7 +45,7 @@
4545 <body>4646 <main>4747 <h1><b>iodine</b> demo</h1>48- <p class="sub">private messages (NIP-17 gift wraps) with xenon + krypton + fluorine + neon + radon.</p>48+ <p class="sub">private messages (NIP-17 gift wraps) with xenon + helium + fluorine + neon + radon.</p>4949 5050 <div class="me">you are <b id="npub">…</b></div>5151 @@ -78,7 +78,7 @@
7878 let f = evt.detail.fields7979 if (f["#p"]) f["#p"] = neon.encode("npub", f["#p"]).slice(0, 16) + "…"8080 })81- ;(window.nostr ?? krypton.signer).getPublicKey().then((pk)=>npub.textContent = neon.encode("npub", pk))81+ ;(window.nostr ?? helium.signer).getPublicKey().then((pk)=>npub.textContent = neon.encode("npub", pk))8282 </script>8383 </body>8484 </html>@@ -6,7 +6,7 @@
66 <title>nobel lanthanides</title>77 <script src="relay.js"></script>88 <script src="../neon/neon.js"></script>9-<script src="../krypton/krypton.js"></script>9+<script src="../helium/helium.js"></script>1010 <script src="../argon/argon.js"></script>1111 <script src="../hydrogen/hydrogen.js"></script>1212 <script src="../xenon/xenon.js"></script>@@ -53,8 +53,8 @@
5353 </section>5454 </main>5555 <script>56- // One local key (krypton's); its kind 0 comes from the form. Every preset points at it.57- let pk = krypton.pub(krypton.key())56+ // One local key (helium's); its kind 0 comes from the form. Every preset points at it.57+ let pk = helium.pub(helium.key())5858 for (let a of ["npr-card", "npr-chip", "npr-avatar"]) document.querySelectorAll(`[${a}=""]`).forEach((el)=>el.setAttribute(a, neon.encode("npub", pk)))5959 hand.setAttribute("nrn-profile", pk)6060 markup.textContent = praseodymium.markup("card", pk)@@ -6,7 +6,7 @@
66 <title>oganesson demo</title>77 <script src="relay.js"></script>88 <script src="../neon/neon.js"></script>9-<script src="../krypton/krypton.js"></script>9+<script src="../helium/helium.js"></script>1010 <script src="../argon/argon.js"></script>1111 <script src="../hydrogen/hydrogen.js"></script>1212 <script src="../xenon/xenon.js"></script>@@ -1,13 +1,13 @@
11 # 🧪 fluorine.js - NIP-44 encryption22 3-fluorine.js is a miniature nostr-tools nip44: NIP-44 v2 encryption in about 3.5kb of ChaCha20, HMAC-SHA256 and padding over `crypto.subtle`. It derives conversation keys through krypton.js's ECDH, then encrypts and decrypts payloads that any NIP-44 client can read. Loaded after krypton.js, it gives krypton's local signer a `nip44` face like a NIP-07 extension's, and chlorine.js and iodine.js use it for private messages and encrypted content.3+fluorine.js is a miniature nostr-tools nip44: NIP-44 v2 encryption in about 3.5kb of ChaCha20, HMAC-SHA256 and padding over `crypto.subtle`. It derives conversation keys through helium.js's ECDH, then encrypts and decrypts payloads that any NIP-44 client can read. Loaded after helium.js, it gives helium's local signer a `nip44` face like a NIP-07 extension's, and chlorine.js and iodine.js use it for private messages and encrypted content.44 55 ```html6-<script src="krypton.js"></script>6+<script src="helium.js"></script>77 <script src="fluorine.js"></script>88 <script type="module">9- let box = await krypton.signer.nip44.encrypt(theirPubkey, "meet at noon")10- let text = await krypton.signer.nip44.decrypt(theirPubkey, box)9+ let box = await helium.signer.nip44.encrypt(theirPubkey, "meet at noon")10+ let text = await helium.signer.nip44.decrypt(theirPubkey, box)1111 </script>1212 ```1313 @@ -22,10 +22,10 @@ The local key encrypts a message only it and the owner of `theirPubkey` can read
2222 | `fluorine.decrypt(payload, key)` | plaintext, or throws on a bad version, length, MAC or padding |2323 | `fluorine.nip44(sk)` | NIP-07 shaped `{encrypt(pk, text), decrypt(pk, payload)}` for that key, caching conversation keys |2424 | `fluorine.pad(n)` | padded length for an `n` byte message |25-| `krypton.signer.nip44` | `{encrypt, decrypt}` using `krypton.key()`, added when krypton.js is loaded first |25+| `helium.signer.nip44` | `{encrypt, decrypt}` using `helium.key()`, added when helium.js is loaded first |2626 2727 All functions return promises except `nip44` and `pad`. Plaintext must be 1 to 65535 bytes of UTF-8.2828 2929 ## Caveats3030 31-ECDH runs on krypton's BigInt arithmetic, which is not constant time, and the local key lives in `localStorage`. Prefer an extension's `nostr.nip44` for keys that matter. Tests check every official NIP-44 vector and round trips against nostr-tools.31+ECDH runs on helium's BigInt arithmetic, which is not constant time, and the local key lives in `localStorage`. Prefer an extension's `nostr.nip44` for keys that matter. Tests check every official NIP-44 vector and round trips against nostr-tools.@@ -40,7 +40,7 @@
4040 let key = async(sk, pk)=>{4141 let d = BigInt("0x" + sk)4242 if (!d || d >= N) fail("bad key")43- return hmac(new TextEncoder().encode("nip44-v2"), bytes(g.krypton.shared(sk, pk)))43+ return hmac(new TextEncoder().encode("nip44-v2"), bytes(g.helium.shared(sk, pk)))4444 }4545 let encrypt = async(text, ck, nonce = crypto.getRandomValues(new Uint8Array(32)))=>{4646 let m = new TextEncoder().encode(text), n = m.length@@ -71,6 +71,6 @@
7171 return {encrypt:async(pk, text)=>encrypt(text, await ck(pk)), decrypt:async(pk, payload)=>decrypt(payload, await ck(pk))}7272 }7373 g.fluorine = {key, encrypt, decrypt, nip44, pad}74- let signer = g.krypton?.signer75- if (signer) signer.nip44 = nip44(null, ()=>g.krypton.key())74+ let signer = g.helium?.signer75+ if (signer) signer.nip44 = nip44(null, ()=>g.helium.key())7676 })()@@ -1,9 +1,9 @@
11 # 🥇 gold.js - relay AUTH22 3-gold.js is a miniature NIP-42 client: it answers a relay's `AUTH` challenge with a signed kind 22242 event, so members-only relays let you read and publish. When a relay refuses a subscription or an event with `auth-required:`, gold authenticates once and re-sends it, and xenon never sees the refusal. It composes through xenon's `nhy:message` event and the `nhy:config` signing hook, so krypton.js, chlorine.js or a NIP-07 extension signs the AUTH event just as it signs posts.3+gold.js is a miniature NIP-42 client: it answers a relay's `AUTH` challenge with a signed kind 22242 event, so members-only relays let you read and publish. When a relay refuses a subscription or an event with `auth-required:`, gold authenticates once and re-sends it, and xenon never sees the refusal. It composes through xenon's `nhy:message` event and the `nhy:config` signing hook, so helium.js, chlorine.js or a NIP-07 extension signs the AUTH event just as it signs posts.44 55 ```html6-<script src="krypton.js"></script>6+<script src="helium.js"></script>77 <script src="gold.js"></script>88 <script src="xenon.js"></script>99 @@ -39,6 +39,6 @@ Dispatched on `document`.
3939 | `gold.auth(url)` | Promise of your pubkey once authenticated on that relay's current connection (connects if needed) |4040 | `gold.state[url]` | `{challenge, pubkey, ...}` for the current connection. A reconnect starts fresh |4141 42-Each connection gets at most one AUTH, and each refused subscription or event is retried once. If the relay still refuses, the refusal reaches xenon, so the publish reports `ok:false`. Load gold.js before xenon.js.42+Each connection gets at most one AUTH, and each refused subscription or event is retried once. If the relay still refuses, the refusal reaches xenon, so the publish reports `ok:false`.4343 4444 Privacy: gold is lazy by default and only authenticates when a relay refuses something. Each AUTH tells that relay which pubkey is connecting, so eager mode tells every relay you connect to. Use `nau-relays` to limit which relays get it.@@ -1,48 +1,39 @@
1-# 🎈 helium.js - sync for argon1+# 🔑 helium.js - keys & signatures22 3-helium.js is a miniature Negentropy (NIP-77) client, the replication layer for argon.js. It reconciles argon's local events for a filter with each relay's, then fetches only what is missing locally and publishes only what the relay lacks. Apps work fully offline and catch up both ways when a connection returns.3+helium.js is a miniature @noble/secp256k1: BIP-340 Schnorr keys, signing and verification in about 4kb of BigInt and `crypto.subtle`. Loaded alongside xenon.js, it signs `nxe-publish` events when no NIP-07 extension is present, and verifies incoming events under `nhe-verify`. It also runs in Deno and other runtimes with Web Crypto, where it needs no document.44 55 ```html6-<script src="argon.js"></script>76 <script src="helium.js"></script>7+<script src="xenon.js"></script>88 9-<main nhy-relay="wss://relay.example" nhe-sync='{"authors":["<your pubkey>"]}'></main>9+<form nxe-publish><input name="content"><button>Post</button></form>10+<ol nxe-filter='{"kinds":[1]}' nhe-verify></ol>1011 ```1112 12-On load and whenever the browser comes back online, the author's events are reconciled with every relay in both directions.13+The form publishes with a local key when there is no extension, and the list drops any event whose id or signature is invalid.1314 1415 ## Attributes1516 16-| attribute | default | description |17-|---|---|---|18-| `nhe-sync` | | JSON filter to reconcile. Runs on page load and on `online` |19-| `nhe-mode` | both ways | `pull` only fetches, `push` only publishes |17+| attribute | description |18+|---|---|19+| `nhe-verify` | Verify every event for subscriptions on or under this element |2020 21-Relays come from the closest `nhy-relay`, else `nhyCfg.relays`. Leave `limit` out of the filter: relays ignore it for sync.21+`window.nheCfg = {local:true}` always signs with the local key. `window.nheCfg = {verify:true}` verifies every subscription.2222 23-## Events23+## API2424 25-| event | cancel to | detail |26-|---|---|---|27-| `nhe:sync` | skip this run | `cfg` (`filter`, `relays`, `mode`), editable |28-| `nhe:synced` | | `cfg`, `result` per relay |29-| `nhe:error` | | `cfg`, `relay`, `error` (for example the relay's `NEG-ERR` reason) |25+| function | returns |26+|---|---|27+| `helium.generate()` | new secret key (hex) |28+| `helium.pub(sk)` | x-only public key (hex) |29+| `helium.id(event)` | event id (hex) |30+| `helium.finish(event, sk)` | signed event with `pubkey`, `id`, `sig` |31+| `helium.check(event)` | `true` if id and signature are valid |32+| `helium.sign(msg, sk, aux?)` | 64-byte signature (`Uint8Array`) |33+| `helium.verify(sig, msg, pk)` | `true` or `false` |34+| `helium.key(sk?)` | local key: gets (creating one), sets, or with `null` replaces |35+| `helium.signer` | NIP-07 shaped `{getPublicKey, signEvent}` using the local key |3036 31-`result` is `{relay, have, need, pulled, pushed}`: `have` and `need` are the ids only this side or only the relay held.37+## Caveats3238 33-## Scripting34-35-```js36-let result = await helium.sync({authors:[pk]}, "wss://relay.example", "pull")37-helium.run(element) // run an nhe-sync element now38-```39-40-## Behavior41-42-- Speaks Negentropy protocol v1 as the initiator. Relays that cap frames (tinyrelay and bindws use 60KB) simply take more rounds.43-- A relay ending the exchange with `NEG-CLOSE` (tinyrelay does when the sets already match) counts as done.44-- Fetched events are checked with krypton.js when it is loaded, and stored through argon.45-- Publishing is one event at a time. A `rate-limited: ... retry in Ns` reply waits that long and retries, so a large backlog trickles in at the relay's pace.46-- A connection idle for 60 seconds is closed. There is no fallback for relays without NIP-77: the run reports `nhe:error`.47-48-Requires argon.js. Tested against bindws (bind.ws) and tinyrelay.39+BigInt arithmetic is not constant time, and the local key lives in `localStorage`. Use it for demos, throwaway or low-value keys. Prefer a NIP-07 extension for anything that matters. Tests check keys and signatures byte for byte against @noble/curves.@@ -1,123 +1,96 @@
11 (()=>{2- let g = globalThis, d = g.document2+ let g = globalThis33 if (g.helium) return4- let n = 0, INF = 1/04+ let P = 2n ** 256n - 0x1000003d1n, N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n5+ let G = [0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n]6+ let mod = (a, m = P)=>(a %= m) < 0n ? a + m : a7+ let inv = (a, m = P)=>{8+ let [r0, r1, s0, s1] = [mod(a, m), m, 1n, 0n]9+ while (r1){10+ let q = r0 / r1;11+ [r0, r1, s0, s1] = [r1, r0 - q * r1, s1, s0 - q * s1]12+ }13+ return mod(s0, m)14+ }15+ let pow = (b, e, r = 1n)=>{16+ for (b = mod(b); e; e >>= 1n, b = b * b % P) if (e & 1n) r = r * b % P17+ return r18+ }19+ let add = (p, q)=>{20+ if (!p || !q) return p || q21+ let [x1, y1] = p, [x2, y2] = q22+ if (x1 == x2 && y1 != y2) return null23+ let l = x1 == x2 ? 3n * x1 * x1 * inv(2n * y1) : (y2 - y1) * inv(x2 - x1), x3 = mod(l * l - x1 - x2)24+ return [x3, mod(l * (x1 - x3) - y1)]25+ }26+ let mul = (k, p = G, r = null)=>{27+ for (; k; k >>= 1n, p = add(p, p)) if (k & 1n) r = add(r, p)28+ return r29+ }30+ let lift = (x)=>{31+ let c = mod(x * x * x + 7n), y = pow(c, (P + 1n) / 4n)32+ if (x >= P || y * y % P != c) throw Error("bad point")33+ return [x, y & 1n ? P - y : y]34+ }535 let hex = (b)=>[...b].map((x)=>x.toString(16).padStart(2, "0")).join("")6- let bytes = (h)=>(h.match(/../g) ?? []).map((x)=>parseInt(x, 16))7- let varint = (o, v, a = [v % 128])=>{8- while (v = Math.floor(v / 128)) a.unshift(v % 128 | 128)9- o.push(...a)36+ let bytes = (h)=>Uint8Array.from(h.match(/../g), (x)=>parseInt(x, 16))37+ let num = (b)=>BigInt("0x" + hex(b))38+ let b32 = (n)=>bytes(n.toString(16).padStart(64, "0"))39+ let cat = (...a)=>{40+ let o = new Uint8Array(a.reduce((s, x)=>s + x.length, 0)), i = 041+ for (let x of a) o.set(x, i), i += x.length42+ return o1043 }11- let session = (items)=>{12- let rt, wt, out13- let fp = async(lo, hi, sum = Array(32).fill(0))=>{14- for (let i = lo; i < hi; i++) for (let j = 0, c = 0; j < 32; j++) c = (c >> 8) + sum[j] + items[i].b[j], sum[j] = c & 25515- varint(sum, hi - lo)16- return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(sum)), 0, 16))17- }18- let bound = (t, id = "")=>{19- varint(out, t == INF ? 0 : t - wt + 1)20- wt = t21- varint(out, id.length / 2)22- out.push(...bytes(id))23- }24- let split = async(lo, hi, t, id, c = hi - lo)=>{25- if (c < 32){26- bound(t, id)27- out.push(2)28- varint(out, c)29- for (let i = lo; i < hi; i++) out.push(...items[i].b)30- }31- else for (let i = 0, cur = lo; i < 16; i++){32- let f = await fp(cur, cur += (c >> 4) + (i < c % 16)), p = items[cur - 1], q = items[cur], k = 033- if (cur == hi) bound(t, id)34- else if (p.t != q.t) bound(q.t)35- else {36- while (p.b[k] == q.b[k]) k++37- bound(q.t, q.id.slice(0, 2 * k + 2))38- }39- out.push(1, ...bytes(f))40- }41- }42- let reconcile = async(msg)=>{43- let b = bytes(msg), p = 1, have = [], need = [], prev = 0, skip, pb44- let read = (v = 0, x)=>{45- do v = v * 128 + ((x = b[p++]) & 127)46- while (x & 128)47- if (x == null) throw Error("short")48- return v49- }50- if (b[0] != 97) throw Error("v1 only")51- rt = wt = 052- out = [97]53- while (p < b.length){54- let v = read(), t = rt = v ? rt + v - 1 : INF, len = read(), id = hex(b.slice(p, p += len)), mode = read(), hi = prev, x55- while ((x = items[hi]) && (x.t < t || x.t == t && x.id < id.padEnd(64, "0"))) hi++56- if (mode == 1 && hex(b.slice(p, p += 16)) != await fp(prev, hi)){57- if (skip) bound(...pb), out.push(0)58- skip = 059- await split(prev, hi, t, id)60- } else if (mode == 2){61- let ids = new Set()62- for (let c = read(); c--;) ids.add(hex(b.slice(p, p += 32)))63- for (let i = prev; i < hi; i++) ids.delete(items[i].id) || have.push(items[i].id)64- need.push(...ids)65- skip = 166- } else skip = 167- prev = hi68- pb = [t, id]69- }70- return {reply:out.length > 1 && hex(out), have, need}71- }72- return {start:async()=>(wt = 0, out = [97], await split(0, items.length, INF, ""), hex(out)), reconcile}44+ let te = new TextEncoder()45+ let sha = async(...a)=>new Uint8Array(await crypto.subtle.digest("SHA-256", cat(...a)))46+ let tagged = async(tag, ...a)=>{47+ let t = await sha(te.encode(tag))48+ return num(await sha(t, t, ...a))7349 }74- let sync = async(filter, url, mode)=>{75- let items = (await argon.query([filter])).map((e)=>({t:e.created_at, id:e.id, b:bytes(e.id)})).sort((a, b)=>a.t - b.t || (a.id < b.id ? -1 : 1))76- let s = session(items), id = "nhe" + ++n, have = [], need = [], pulled = 0, pushed = 0, q = [], wake, closed77- let ws = new WebSocket(url), tx = (...m)=>ws.send(JSON.stringify(m)), t, idle = ()=>(clearTimeout(t), t = setTimeout(()=>ws.close(), 6e4))78- let rx = async(f, i)=>{79- while ((i = q.findIndex(f)) < 0) if (closed) throw Error("closed"); else await new Promise((r)=>wake = r)80- return q.splice(i, 1)[0]81- }82- ws.onmessage = (m)=>(idle(), q.push(JSON.parse(m.data)), wake?.())83- ws.onclose = ()=>(closed = 1, wake?.())50+ let sign = async(m, sk, aux = crypto.getRandomValues(new Uint8Array(32)))=>{51+ let d0 = num(sk)52+ if (!d0 || d0 >= N) throw Error("bad key")53+ let p = mul(d0), d = p[1] & 1n ? N - d0 : d0, px = b32(p[0])54+ let k0 = mod(await tagged("BIP0340/nonce", b32(d ^ await tagged("BIP0340/aux", aux)), px, m), N)55+ let R = mul(k0), k = R[1] & 1n ? N - k0 : k0, rx = b32(R[0])56+ let e = mod(await tagged("BIP0340/challenge", rx, px, m), N)57+ return cat(rx, b32(mod(k + e * d, N)))58+ }59+ let verify = async(sig, m, pk)=>{8460 try {85- await new Promise((r)=>ws.onopen = wake = r, idle())86- tx("NEG-OPEN", id, filter, await s.start())87- for (let k, m, r; ([k, , m] = await rx((x)=>x[1] == id))[0] != "NEG-CLOSE";){88- if (k != "NEG-MSG") throw Error(m)89- r = await s.reconcile(m)90- have.push(...r.have)91- need.push(...r.need)92- tx(...r.reply ? ["NEG-MSG", id, r.reply] : ["NEG-CLOSE", id])93- if (!r.reply) break94- }95- if (mode != "push") for (let i = 0; i < need.length; i += 250){96- tx("REQ", id, {ids:need.slice(i, i + 250)})97- for (let x; (x = await rx((x)=>x[1] == id))[0] == "EVENT";)98- if (need.includes(x[2].id) && await (g.krypton?.check(x[2]) ?? 1) && await argon.put(x[2])) pulled++99- tx("CLOSE", id)100- }101- if (mode != "pull") for (let e of await argon.query([{ids:have}])) for (let k, m; ;){102- tx("EVENT", e)103- ;[, , k, m] = await rx((x)=>x[0] == "OK" && x[1] == e.id)104- if (k && ++pushed || !(m = /retry in (\d+)/.exec(m))) break105- await new Promise((r)=>setTimeout(r, m[1] * 1e3, idle()))106- }107- return {relay:url, have, need, pulled, pushed}108- } finally {109- clearTimeout(t)110- ws.close()111- }61+ let r = num(sig.slice(0, 32)), s = num(sig.slice(32)), p = lift(num(pk))62+ if (sig.length != 64 || r >= P || s >= N) return false63+ let e = mod(await tagged("BIP0340/challenge", sig.slice(0, 32), pk, m), N)64+ let R = add(mul(s), mul(N - e, p))65+ return !!R && !(R[1] & 1n) && R[0] == r66+ } catch { return false }67+ }68+ let generate = (k)=>{69+ while (!k || num(k) >= N) k = crypto.getRandomValues(new Uint8Array(32))70+ return hex(k)71+ }72+ let pub = (sk)=>hex(b32(mul(num(bytes(sk)))[0]))73+ let shared = (sk, pk)=>hex(b32(mul(num(bytes(sk)), lift(num(bytes(pk))))[0]))74+ let id = async(e)=>hex(await sha(te.encode(JSON.stringify([0, e.pubkey, e.created_at, e.kind, e.tags, e.content]))))75+ let finish = async(e, sk)=>{76+ e = {...e, pubkey:pub(sk)}77+ e.id = await id(e)78+ e.sig = hex(await sign(bytes(e.id), bytes(sk)))79+ return e80+ }81+ let check = async(e)=>{82+ try { return e.id == await id(e) && verify(bytes(e.sig), bytes(e.id), bytes(e.pubkey)) } catch { return false }11283 }113- g.helium = {sync, session}114- if (!d) return115- let send = (el, type, detail)=>el.dispatchEvent(new CustomEvent("nhe:" + type, {detail, cancelable:1, bubbles:1}))116- let run = g.helium.run = (el)=>{117- let cfg = {filter:JSON.parse(el.getAttribute("nhe-sync")), relays:el.closest("[nhy-relay]")?.getAttribute("nhy-relay").split(/\s+/) ?? g.nhyCfg?.relays ?? [], mode:el.getAttribute("nhe-mode")}118- if (send(el, "sync", {cfg})) for (let url of cfg.relays) sync(cfg.filter, url, cfg.mode).then((result)=>send(el, "synced", {cfg, result}), (error)=>send(el, "error", {cfg, relay:url, error}))84+ let key = (k)=>{85+ let s = g.localStorage86+ if (k !== undefined) k ? s["nhe:key"] = k : delete s["nhe:key"]87+ return s["nhe:key"] ||= s["nkr:key"] || generate()11988 }120- let all = ()=>d.querySelectorAll("[nhe-sync]").forEach(run)121- g.addEventListener("online", all)122- d.addEventListener("DOMContentLoaded", all)89+ let signer = {getPublicKey:async()=>pub(key()), signEvent:(e)=>finish(e, key())}90+ g.helium = {generate, pub, shared, id, sign, verify, finish, check, key, signer}91+ g.document?.addEventListener("nhy:config", (evt)=>{92+ let c = evt.detail.cfg93+ if (c.event && (g.nheCfg?.local || !g.nostr)) c.sign = signer.signEvent94+ if (c.filters && (g.nheCfg?.verify || c.elt?.closest("[nhe-verify]"))) c.verify = check95+ })12396 })()@@ -26,7 +26,7 @@ That subscribes, signs with whatever signer is present, and publishes to the pag
2626 | `nhy:event` | `cfg.elt` or document | skip `cfg.onevent` | `cfg`, `event`, `from` (relay URL, or a cache name), `fields` (escaped, for templates) |2727 | `nhy:message` | document | skip hydrogen's own handling | `url`, `relay`, `msg`: every raw relay message, before anything else |2828 29-Signers (krypton, chlorine, an extension shim) set `cfg.sign` in `nhy:config`. Caches (argon) answer subscriptions there and store from `nhy:event`. Formatters (neon) add fields in `nhy:event`. Relay AUTH (gold) works through `nhy:message`.29+Signers (helium, chlorine, an extension shim) set `cfg.sign` in `nhy:config`. Caches (argon) answer subscriptions there and store from `nhy:event`. Formatters (neon) add fields in `nhy:event`. Relay AUTH (gold) works through `nhy:message`.3030 3131 ## API3232 @@ -63,7 +63,7 @@
6363 r.oks.set(event.id, done)6464 r.send(["EVENT", done.event = event])6565 })))66- // The family signing hook: nhy:config lets krypton, chlorine or anything else replace cfg.sign.66+ // The family signing hook: nhy:config lets helium, chlorine or anything else replace cfg.sign.6767 let sign = async(cfg)=>{6868 cfg.sign = (e)=>g.nostr.signEvent(e)6969 cfg.relays ??= relays(cfg.elt)@@ -1,6 +1,6 @@
11 # ✉️ iodine.js - private messages22 3-iodine.js is a miniature encrypted messenger: NIP-17 direct messages sealed and gift wrapped per NIP-59, in about 4kb. A form sends a message to someone's DM relays (their kind 10050 list), and a list shows your inbox, unwrapped and rendered like any other xenon feed. It composes with xenon.js, neon.js and radon.js, and signs and encrypts with any signer that has `nip44`: a NIP-07 extension, chlorine.js, or krypton.js with fluorine.js.3+iodine.js is a miniature encrypted messenger: NIP-17 direct messages sealed and gift wrapped per NIP-59, in about 4kb. A form sends a message to someone's DM relays (their kind 10050 list), and a list shows your inbox, unwrapped and rendered like any other xenon feed. It composes with xenon.js, neon.js and radon.js, and signs and encrypts with any signer that has `nip44`: a NIP-07 extension, chlorine.js, or helium.js with fluorine.js.44 55 ```html66 <form nio-send nio-to="npub1..."><textarea name="content"></textarea><button>Send</button></form>@@ -41,7 +41,7 @@ The inbox sets `nxe-filter` and `nar-ignore` on itself and hands over to xenon,
4141 | `iodine.unwrap(wrap)` | promise of the rumor, or `undefined` if it is not for you or does not check out |4242 | `iodine.relays(pubkey)` | promise of the DM relays in `pubkey`'s kind 10050 list, `[]` after 3s |4343 44-Requires xenon.js and krypton.js, fluorine.js for the gift wrap layer, and neon.js for `npub` recipients.44+Requires xenon.js and helium.js, fluorine.js for the gift wrap layer, and neon.js for `npub` recipients.4545 4646 ## Caveats4747 @@ -2,7 +2,7 @@
22 let g = window, d = document, J = JSON33 if (g.iodine) return44 let send = (el, type, detail)=>el.dispatchEvent(new CustomEvent("nio:" + type, {detail, cancelable:1, bubbles:1, composed:1}))5- let signer = ()=>g.nostr ?? krypton.signer5+ let signer = ()=>g.nostr ?? helium.signer66 let hex = (s)=>{77 if (/^[0-9a-f]{64}$/.test(s)) return s88 let x = neon.decode(s).data@@ -25,14 +25,14 @@
2525 setTimeout(end, 3e3)2626 })2727 let wrap = async(rumor, to)=>{28- let s = signer(), sk = krypton.generate()28+ let s = signer(), sk = helium.generate()2929 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 krypton.finish({kind:1059, created_at:past(), tags:[["p", to]], content:await fluorine.nip44(sk).encrypt(to, J.stringify(seal))}, sk)30+ return helium.finish({kind:1059, created_at:past(), tags:[["p", to]], content:await fluorine.nip44(sk).encrypt(to, J.stringify(seal))}, sk)3131 }3232 let unwrap = async(w)=>{3333 try {3434 let s = signer(), seal = J.parse(await s.nip44.decrypt(w.pubkey, w.content))35- if (seal.kind != 13 || !await krypton.check(seal)) return35+ if (seal.kind != 13 || !await helium.check(seal)) return3636 let r = J.parse(await s.nip44.decrypt(seal.pubkey, seal.content))3737 if (r.pubkey == seal.pubkey) return r3838 } catch {}@@ -42,7 +42,7 @@
4242 to = hex(to)4343 let rumor = {pubkey:me, created_at:now(), kind:14, tags:[["p", to]], content}, detail = {rumor, recipients:[...new Set([to, me])]}4444 if (!send(el, "send", detail)) return45- rumor.id = await krypton.id(rumor)45+ rumor.id = await helium.id(rumor)4646 let results = (await Promise.all(detail.recipients.map(async(pk)=>{4747 let w = await wrap(rumor, pk), r = rs ?? await lookup(pk, el)4848 return hydrogen.pub(w, r.length ? r : relays(el))@@ -1,39 +0,0 @@
1-# 🔑 krypton.js - keys & signatures2-3-krypton.js is a miniature @noble/secp256k1: BIP-340 Schnorr keys, signing and verification in about 4kb of BigInt and `crypto.subtle`. Loaded alongside xenon.js, it signs `nxe-publish` events when no NIP-07 extension is present, and verifies incoming events under `nkr-verify`. It also runs in Deno and other runtimes with Web Crypto, where it needs no document.4-5-```html6-<script src="krypton.js"></script>7-<script src="xenon.js"></script>8-9-<form nxe-publish><input name="content"><button>Post</button></form>10-<ol nxe-filter='{"kinds":[1]}' nkr-verify></ol>11-```12-13-The form publishes with a local key when there is no extension, and the list drops any event whose id or signature is invalid.14-15-## Attributes16-17-| attribute | description |18-|---|---|19-| `nkr-verify` | Verify every event for subscriptions on or under this element |20-21-`window.nkrCfg = {local:true}` always signs with the local key. `window.nkrCfg = {verify:true}` verifies every subscription.22-23-## API24-25-| function | returns |26-|---|---|27-| `krypton.generate()` | new secret key (hex) |28-| `krypton.pub(sk)` | x-only public key (hex) |29-| `krypton.id(event)` | event id (hex) |30-| `krypton.finish(event, sk)` | signed event with `pubkey`, `id`, `sig` |31-| `krypton.check(event)` | `true` if id and signature are valid |32-| `krypton.sign(msg, sk, aux?)` | 64-byte signature (`Uint8Array`) |33-| `krypton.verify(sig, msg, pk)` | `true` or `false` |34-| `krypton.key(sk?)` | local key: gets (creating one), sets, or with `null` replaces |35-| `krypton.signer` | NIP-07 shaped `{getPublicKey, signEvent}` using the local key |36-37-## Caveats38-39-BigInt arithmetic is not constant time, and the local key lives in `localStorage`. Use it for demos, throwaway or low-value keys. Prefer a NIP-07 extension for anything that matters. Tests check keys and signatures byte for byte against @noble/curves.@@ -1,96 +0,0 @@
1-(()=>{2- let g = globalThis3- if (g.krypton) return4- let P = 2n ** 256n - 0x1000003d1n, N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n5- let G = [0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n]6- let mod = (a, m = P)=>(a %= m) < 0n ? a + m : a7- let inv = (a, m = P)=>{8- let [r0, r1, s0, s1] = [mod(a, m), m, 1n, 0n]9- while (r1){10- let q = r0 / r1;11- [r0, r1, s0, s1] = [r1, r0 - q * r1, s1, s0 - q * s1]12- }13- return mod(s0, m)14- }15- let pow = (b, e, r = 1n)=>{16- for (b = mod(b); e; e >>= 1n, b = b * b % P) if (e & 1n) r = r * b % P17- return r18- }19- let add = (p, q)=>{20- if (!p || !q) return p || q21- let [x1, y1] = p, [x2, y2] = q22- if (x1 == x2 && y1 != y2) return null23- let l = x1 == x2 ? 3n * x1 * x1 * inv(2n * y1) : (y2 - y1) * inv(x2 - x1), x3 = mod(l * l - x1 - x2)24- return [x3, mod(l * (x1 - x3) - y1)]25- }26- let mul = (k, p = G, r = null)=>{27- for (; k; k >>= 1n, p = add(p, p)) if (k & 1n) r = add(r, p)28- return r29- }30- let lift = (x)=>{31- let c = mod(x * x * x + 7n), y = pow(c, (P + 1n) / 4n)32- if (x >= P || y * y % P != c) throw Error("bad point")33- return [x, y & 1n ? P - y : y]34- }35- let hex = (b)=>[...b].map((x)=>x.toString(16).padStart(2, "0")).join("")36- let bytes = (h)=>Uint8Array.from(h.match(/../g), (x)=>parseInt(x, 16))37- let num = (b)=>BigInt("0x" + hex(b))38- let b32 = (n)=>bytes(n.toString(16).padStart(64, "0"))39- let cat = (...a)=>{40- let o = new Uint8Array(a.reduce((s, x)=>s + x.length, 0)), i = 041- for (let x of a) o.set(x, i), i += x.length42- return o43- }44- let te = new TextEncoder()45- let sha = async(...a)=>new Uint8Array(await crypto.subtle.digest("SHA-256", cat(...a)))46- let tagged = async(tag, ...a)=>{47- let t = await sha(te.encode(tag))48- return num(await sha(t, t, ...a))49- }50- let sign = async(m, sk, aux = crypto.getRandomValues(new Uint8Array(32)))=>{51- let d0 = num(sk)52- if (!d0 || d0 >= N) throw Error("bad key")53- let p = mul(d0), d = p[1] & 1n ? N - d0 : d0, px = b32(p[0])54- let k0 = mod(await tagged("BIP0340/nonce", b32(d ^ await tagged("BIP0340/aux", aux)), px, m), N)55- let R = mul(k0), k = R[1] & 1n ? N - k0 : k0, rx = b32(R[0])56- let e = mod(await tagged("BIP0340/challenge", rx, px, m), N)57- return cat(rx, b32(mod(k + e * d, N)))58- }59- let verify = async(sig, m, pk)=>{60- try {61- let r = num(sig.slice(0, 32)), s = num(sig.slice(32)), p = lift(num(pk))62- if (sig.length != 64 || r >= P || s >= N) return false63- let e = mod(await tagged("BIP0340/challenge", sig.slice(0, 32), pk, m), N)64- let R = add(mul(s), mul(N - e, p))65- return !!R && !(R[1] & 1n) && R[0] == r66- } catch { return false }67- }68- let generate = (k)=>{69- while (!k || num(k) >= N) k = crypto.getRandomValues(new Uint8Array(32))70- return hex(k)71- }72- let pub = (sk)=>hex(b32(mul(num(bytes(sk)))[0]))73- let shared = (sk, pk)=>hex(b32(mul(num(bytes(sk)), lift(num(bytes(pk))))[0]))74- let id = async(e)=>hex(await sha(te.encode(JSON.stringify([0, e.pubkey, e.created_at, e.kind, e.tags, e.content]))))75- let finish = async(e, sk)=>{76- e = {...e, pubkey:pub(sk)}77- e.id = await id(e)78- e.sig = hex(await sign(bytes(e.id), bytes(sk)))79- return e80- }81- let check = async(e)=>{82- try { return e.id == await id(e) && verify(bytes(e.sig), bytes(e.id), bytes(e.pubkey)) } catch { return false }83- }84- let key = (k)=>{85- let s = g.localStorage86- if (k !== undefined) k ? s["nkr:key"] = k : delete s["nkr:key"]87- return s["nkr:key"] ||= generate()88- }89- let signer = {getPublicKey:async()=>pub(key()), signEvent:(e)=>finish(e, key())}90- g.krypton = {generate, pub, shared, id, sign, verify, finish, check, key, signer}91- g.document?.addEventListener("nhy:config", (evt)=>{92- let c = evt.detail.cfg93- if (c.event && (g.nkrCfg?.local || !g.nostr)) c.sign = signer.signEvent94- if (c.filters && (g.nkrCfg?.verify || c.elt?.closest("[nkr-verify]"))) c.verify = check95- })96-})()@@ -0,0 +1,48 @@
1+# 🔋 lithium.js - sync for argon2+3+lithium.js is a miniature Negentropy (NIP-77) client, the replication layer for argon.js. It reconciles argon's local events for a filter with each relay's, then fetches only what is missing locally and publishes only what the relay lacks. Apps work fully offline and catch up both ways when a connection returns.4+5+```html6+<script src="argon.js"></script>7+<script src="lithium.js"></script>8+9+<main nhy-relay="wss://relay.example" nli-sync='{"authors":["<your pubkey>"]}'></main>10+```11+12+On load and whenever the browser comes back online, the author's events are reconciled with every relay in both directions.13+14+## Attributes15+16+| attribute | default | description |17+|---|---|---|18+| `nli-sync` | | JSON filter to reconcile. Runs on page load and on `online` |19+| `nli-mode` | both ways | `pull` only fetches, `push` only publishes |20+21+Relays come from the closest `nhy-relay`, else `nhyCfg.relays`. Leave `limit` out of the filter: relays ignore it for sync.22+23+## Events24+25+| event | cancel to | detail |26+|---|---|---|27+| `nli:sync` | skip this run | `cfg` (`filter`, `relays`, `mode`), editable |28+| `nli:synced` | | `cfg`, `result` per relay |29+| `nli:error` | | `cfg`, `relay`, `error` (for example the relay's `NEG-ERR` reason) |30+31+`result` is `{relay, have, need, pulled, pushed}`: `have` and `need` are the ids only this side or only the relay held.32+33+## Scripting34+35+```js36+let result = await lithium.sync({authors:[pk]}, "wss://relay.example", "pull")37+lithium.run(element) // run an nli-sync element now38+```39+40+## Behavior41+42+- Speaks Negentropy protocol v1 as the initiator. Relays that cap frames (tinyrelay and bindws use 60KB) simply take more rounds.43+- A relay ending the exchange with `NEG-CLOSE` (tinyrelay does when the sets already match) counts as done.44+- Fetched events are checked with helium.js when it is loaded, and stored through argon.45+- Publishing is one event at a time. A `rate-limited: ... retry in Ns` reply waits that long and retries, so a large backlog trickles in at the relay's pace.46+- A connection idle for 60 seconds is closed. There is no fallback for relays without NIP-77: the run reports `nli:error`.47+48+Requires argon.js, and hydrogen.js for the `nli-sync` attribute. Tested against bindws (bind.ws) and tinyrelay.@@ -0,0 +1,123 @@
1+(()=>{2+ let g = globalThis, d = g.document3+ if (g.lithium) return4+ let n = 0, INF = 1/05+ let hex = (b)=>[...b].map((x)=>x.toString(16).padStart(2, "0")).join("")6+ let bytes = (h)=>(h.match(/../g) ?? []).map((x)=>parseInt(x, 16))7+ let varint = (o, v, a = [v % 128])=>{8+ while (v = Math.floor(v / 128)) a.unshift(v % 128 | 128)9+ o.push(...a)10+ }11+ let session = (items)=>{12+ let rt, wt, out13+ let fp = async(lo, hi, sum = Array(32).fill(0))=>{14+ for (let i = lo; i < hi; i++) for (let j = 0, c = 0; j < 32; j++) c = (c >> 8) + sum[j] + items[i].b[j], sum[j] = c & 25515+ varint(sum, hi - lo)16+ return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(sum)), 0, 16))17+ }18+ let bound = (t, id = "")=>{19+ varint(out, t == INF ? 0 : t - wt + 1)20+ wt = t21+ varint(out, id.length / 2)22+ out.push(...bytes(id))23+ }24+ let split = async(lo, hi, t, id, c = hi - lo)=>{25+ if (c < 32){26+ bound(t, id)27+ out.push(2)28+ varint(out, c)29+ for (let i = lo; i < hi; i++) out.push(...items[i].b)30+ }31+ else for (let i = 0, cur = lo; i < 16; i++){32+ let f = await fp(cur, cur += (c >> 4) + (i < c % 16)), p = items[cur - 1], q = items[cur], k = 033+ if (cur == hi) bound(t, id)34+ else if (p.t != q.t) bound(q.t)35+ else {36+ while (p.b[k] == q.b[k]) k++37+ bound(q.t, q.id.slice(0, 2 * k + 2))38+ }39+ out.push(1, ...bytes(f))40+ }41+ }42+ let reconcile = async(msg)=>{43+ let b = bytes(msg), p = 1, have = [], need = [], prev = 0, skip, pb44+ let read = (v = 0, x)=>{45+ do v = v * 128 + ((x = b[p++]) & 127)46+ while (x & 128)47+ if (x == null) throw Error("short")48+ return v49+ }50+ if (b[0] != 97) throw Error("v1 only")51+ rt = wt = 052+ out = [97]53+ while (p < b.length){54+ let v = read(), t = rt = v ? rt + v - 1 : INF, len = read(), id = hex(b.slice(p, p += len)), mode = read(), hi = prev, x55+ while ((x = items[hi]) && (x.t < t || x.t == t && x.id < id.padEnd(64, "0"))) hi++56+ if (mode == 1 && hex(b.slice(p, p += 16)) != await fp(prev, hi)){57+ if (skip) bound(...pb), out.push(0)58+ skip = 059+ await split(prev, hi, t, id)60+ } else if (mode == 2){61+ let ids = new Set()62+ for (let c = read(); c--;) ids.add(hex(b.slice(p, p += 32)))63+ for (let i = prev; i < hi; i++) ids.delete(items[i].id) || have.push(items[i].id)64+ need.push(...ids)65+ skip = 166+ } else skip = 167+ prev = hi68+ pb = [t, id]69+ }70+ return {reply:out.length > 1 && hex(out), have, need}71+ }72+ return {start:async()=>(wt = 0, out = [97], await split(0, items.length, INF, ""), hex(out)), reconcile}73+ }74+ let sync = async(filter, url, mode)=>{75+ let items = (await argon.query([filter])).map((e)=>({t:e.created_at, id:e.id, b:bytes(e.id)})).sort((a, b)=>a.t - b.t || (a.id < b.id ? -1 : 1))76+ let s = session(items), id = "nhe" + ++n, have = [], need = [], pulled = 0, pushed = 0, q = [], wake, closed77+ let ws = new WebSocket(url), tx = (...m)=>ws.send(JSON.stringify(m)), t, idle = ()=>(clearTimeout(t), t = setTimeout(()=>ws.close(), 6e4))78+ let rx = async(f, i)=>{79+ while ((i = q.findIndex(f)) < 0) if (closed) throw Error("closed"); else await new Promise((r)=>wake = r)80+ return q.splice(i, 1)[0]81+ }82+ ws.onmessage = (m)=>(idle(), q.push(JSON.parse(m.data)), wake?.())83+ ws.onclose = ()=>(closed = 1, wake?.())84+ try {85+ await new Promise((r)=>ws.onopen = wake = r, idle())86+ tx("NEG-OPEN", id, filter, await s.start())87+ for (let k, m, r; ([k, , m] = await rx((x)=>x[1] == id))[0] != "NEG-CLOSE";){88+ if (k != "NEG-MSG") throw Error(m)89+ r = await s.reconcile(m)90+ have.push(...r.have)91+ need.push(...r.need)92+ tx(...r.reply ? ["NEG-MSG", id, r.reply] : ["NEG-CLOSE", id])93+ if (!r.reply) break94+ }95+ if (mode != "push") for (let i = 0; i < need.length; i += 250){96+ tx("REQ", id, {ids:need.slice(i, i + 250)})97+ for (let x; (x = await rx((x)=>x[1] == id))[0] == "EVENT";)98+ if (need.includes(x[2].id) && await (g.helium?.check(x[2]) ?? 1) && await argon.put(x[2])) pulled++99+ tx("CLOSE", id)100+ }101+ if (mode != "pull") for (let e of await argon.query([{ids:have}])) for (let k, m; ;){102+ tx("EVENT", e)103+ ;[, , k, m] = await rx((x)=>x[0] == "OK" && x[1] == e.id)104+ if (k && ++pushed || !(m = /retry in (\d+)/.exec(m))) break105+ await new Promise((r)=>setTimeout(r, m[1] * 1e3, idle()))106+ }107+ return {relay:url, have, need, pulled, pushed}108+ } finally {109+ clearTimeout(t)110+ ws.close()111+ }112+ }113+ g.lithium = {sync, session}114+ if (!d) return115+ let send = (el, type, detail)=>el.dispatchEvent(new CustomEvent("nli:" + type, {detail, cancelable:1, bubbles:1}))116+ let run = g.lithium.run = (el)=>{117+ let cfg = {filter:JSON.parse(el.getAttribute("nli-sync")), relays:hydrogen.relays(el), mode:el.getAttribute("nli-mode")}118+ if (send(el, "sync", {cfg})) for (let url of cfg.relays) sync(cfg.filter, url, cfg.mode).then((result)=>send(el, "synced", {cfg, result}), (error)=>send(el, "error", {cfg, relay:url, error}))119+ }120+ let all = ()=>d.querySelectorAll("[nli-sync]").forEach(run)121+ g.addEventListener("online", all)122+ d.readyState == "loading" ? d.addEventListener("DOMContentLoaded", all) : all()123+})()@@ -1,10 +1,10 @@
11 # ☿️ mercury.js - signed HTTP22 3-mercury.js is a miniature rexi.js with a signature on every request: each call carries a NIP-98 `Authorization` header over its URL, method and body hash, and Blossom helpers sign kind 24242 tokens to upload, list and delete blobs. It composes with fixi.js through `fx:config`, so any fixi request under `nhg-sign` is signed, and with every nobel signer through `nhy:config`, so krypton.js, chlorine.js or a NIP-07 extension does the signing. It also runs in Deno, where it signs with `globalThis.nostr` or krypton.js.3+mercury.js is a miniature rexi.js with a signature on every request: each call carries a NIP-98 `Authorization` header over its URL, method and body hash, and Blossom helpers sign kind 24242 tokens to upload, list and delete blobs. It composes with fixi.js through `fx:config`, so any fixi request under `nhg-sign` is signed, and with every nobel signer through `nhy:config`, so helium.js, chlorine.js or a NIP-07 extension does the signing. It also runs in Deno, where it signs with `globalThis.nostr` or helium.js.44 55 ```html66 <script src="fixi.js"></script>7-<script src="krypton.js"></script>7+<script src="helium.js"></script>88 <script src="mercury.js"></script>99 1010 <button fx-action="/api/like" fx-method="post" nhg-sign>Like</button>@@ -9,7 +9,7 @@
99 return r1010 }1111 let sign = async(event, elt)=>{12- let cfg = {elt, event:{created_at:Date.now() / 1e3 | 0, content:"", ...event}, sign:(e)=>(g.nostr ?? g.krypton.signer).signEvent(e)}12+ let cfg = {elt, event:{created_at:Date.now() / 1e3 | 0, content:"", ...event}, sign:(e)=>(g.nostr ?? g.helium.signer).signEvent(e)}1313 send(elt, "nhy:config", {cfg})1414 if (!send(elt, "nhg:sign", {cfg})) throw new DOMException("nhg:sign cancelled", "AbortError")1515 return "Nostr " + btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(await cfg.sign(cfg.event)))))@@ -1,6 +1,6 @@
11 # 👻 oganesson.js - live and ephemeral22 3-oganesson.js is a miniature of the live layer of a big chat app: who's here, who's typing, where their cursor is. Any element can send an ephemeral event (kinds 20000-29999) on a heartbeat or on input, and any element can show the latest one per person from a template, removing it when it goes stale or its author leaves. It publishes and subscribes through xenon.js, signs through the same `nhy:config` hook (so krypton.js or any other signer applies), and argon.js keeps none of it: argon keeps what lasts, oganesson handles what vanishes.3+oganesson.js is a miniature of the live layer of a big chat app: who's here, who's typing, where their cursor is. Any element can send an ephemeral event (kinds 20000-29999) on a heartbeat or on input, and any element can show the latest one per person from a template, removing it when it goes stale or its author leaves. It publishes and subscribes through xenon.js, signs through the same `nhy:config` hook (so helium.js or any other signer applies), and argon.js keeps none of it: argon keeps what lasts, oganesson handles what vanishes.44 55 ```html66 <main nog-room="lobby">@@ -60,7 +60,7 @@ Presence and typing follow the convention shared by Block's Buzz and tinyrelay.
6060 6161 ## Caveats6262 63-- Every heartbeat, keystroke burst and cursor move is a signature. A NIP-07 extension may prompt each time, so this works best with krypton.js's local key or an extension set to auto-approve these kinds.63+- Every heartbeat, keystroke burst and cursor move is a signature. A NIP-07 extension may prompt each time, so this works best with helium.js's local key or an extension set to auto-approve these kinds.6464 - Buzz and tinyrelay members-only rooms may require NIP-42 AUTH, which a future family library will handle.6565 6666 Requires xenon.js.@@ -94,8 +94,9 @@
9494 w.oganesson = {send:fire, leave, mine}9595 doc.addEventListener("nog:process", (evt)=>process(evt.target))9696 doc.addEventListener("visibilitychange", ()=>doc.hidden ? leave() : senders.forEach((s)=>s.__nb?.()))97- doc.addEventListener("DOMContentLoaded", ()=>{97+ let boot = ()=>{9898 new MutationObserver((recs)=>recs.forEach((r)=>r.addedNodes.forEach(process))).observe(doc.documentElement, {childList:1, subtree:1})9999 process(doc.body)100- })100+ }101+ doc.readyState == "loading" ? doc.addEventListener("DOMContentLoaded", boot) : boot()101102 })()@@ -77,8 +77,9 @@
7777 d.__nrn_mo = new MutationObserver((recs)=>recs.forEach((r)=>r.type == "attributes" ? init(r.target) : r.addedNodes.forEach(process)))7878 d.addEventListener("nhy:event", (evt)=>ingest(evt.detail.event))7979 d.addEventListener("nxe:publish", (evt)=>ingest(evt.detail.cfg.event))80- d.addEventListener("DOMContentLoaded", ()=>{80+ let boot = ()=>{8181 d.__nrn_mo.observe(d.documentElement, {childList:1, subtree:1, attributes:1, attributeFilter:["nrn-profile"]})8282 process(d.body)83- })83+ }84+ d.readyState == "loading" ? d.addEventListener("DOMContentLoaded", boot) : boot()8485 })()@@ -1,13 +1,13 @@
11 // Dev server: static files plus a tiny in-memory nostr relay at /relay.2-// The relay reuses krypton.js to check signatures and argon.js to match filters.2+// The relay reuses helium.js to check signatures and argon.js to match filters.33 import { serveDir } from "jsr:@std/http@1/file-server";4-import "./krypton/krypton.js";4+import "./helium/helium.js";55 import "./argon/argon.js";66 77 type Event = { id: string; pubkey: string; kind: number; created_at: number; tags: string[][]; content: string; sig: string };88 type Filter = Record<string, unknown> & { limit?: number };99 // deno-lint-ignore no-explicit-any10-const { krypton, argon } = globalThis as any;10+const { helium, argon } = globalThis as any;1111 1212 export function serve(port = 8000) {1313 const events: Event[] = [];@@ -34,7 +34,7 @@ export function serve(port = 8000) {
3434 } else if (type === "CLOSE") {3535 client.subs.delete(a);3636 } else if (type === "EVENT") {37- const ok = await krypton.check(a);37+ const ok = await helium.check(a);3838 send(["OK", a?.id, ok, ok ? "" : "invalid: bad id or signature"]);3939 if (!ok || events.some((e) => e.id === a.id)) return;4040 if (a.kind < 20000 || a.kind >= 30000) events.push(a);@@ -2,19 +2,19 @@
22 import { assert, assertEquals, assertRejects } from "jsr:@std/assert@1";33 import { chromium } from "npm:playwright@1.58";44 import { serve } from "../server.ts";5-import "../krypton/krypton.js";5+import "../helium/helium.js";66 import "../fluorine/fluorine.js";77 88 localStorage.removeItem("ncl:session");99 await import("../chlorine/chlorine.js");10-const { krypton, fluorine, chlorine } = globalThis;10+const { helium, fluorine, chlorine } = globalThis;1111 const opts = { sanitizeResources: false, sanitizeOps: false };1212 1313 const server = serve(0);1414 const base = `http://localhost:${server.addr.port}`;1515 const relay = Deno.env.get("NCL_RELAY") ?? `ws://localhost:${server.addr.port}/relay`;1616 const cfg = await Deno.makeTempDir();17-const sk = krypton.generate(), user = krypton.pub(sk);17+const sk = helium.generate(), user = helium.pub(sk);1818 1919 function spawn(...args) {2020 const p = new Deno.Command("nak", { args: ["--config-path", cfg, ...args], stdout: "piped", stderr: "piped" }).spawn();@@ -52,9 +52,9 @@ Deno.test({ name: "chlorine connects to nak bunker via bunker://", ignore: !hasN
5252 const e = await nostr.signEvent({ kind: 1, created_at: Math.floor(Date.now() / 1e3), tags: [["t", "nobel"]], content: "hi from chlorine" });5353 assertEquals(e.pubkey, user);5454 assertEquals(e.content, "hi from chlorine");55- assert(await krypton.check(e));55+ assert(await helium.check(e));5656 57- const other = krypton.generate(), otherPk = krypton.pub(other);57+ const other = helium.generate(), otherPk = helium.pub(other);5858 const ct = await nostr.nip44.encrypt(otherPk, "secret ⚡ text");5959 assertEquals(await fluorine.decrypt(ct, await fluorine.key(other, user)), "secret ⚡ text");6060 assertEquals(await nostr.nip44.decrypt(otherPk, ct), "secret ⚡ text");@@ -83,14 +83,14 @@ Deno.test({ name: "chlorine nostrconnect:// flow with nak bunker connect", ignor
8383 assertEquals(await done, user);8484 assertEquals(chlorine.session.remote, user);8585 const e = await nostr.signEvent({ kind: 1, content: "via nostrconnect" });86- assert(await krypton.check(e));86+ assert(await helium.check(e));8787 assertEquals(e.pubkey, user);8888 chlorine.disconnect();8989 });9090 9191 Deno.test({ name: "chlorine waits through auth_url and emits ncl:auth", ...opts }, async () => {9292 // A scripted remote signer whose user key differs from its own key.93- const rsk = krypton.generate(), rpk = krypton.pub(rsk), usk = krypton.generate();93+ const rsk = helium.generate(), rpk = helium.pub(rsk), usk = helium.generate();9494 const ws = new WebSocket(relay);9595 await new Promise((r) => ws.onopen = r);9696 ws.send(JSON.stringify(["REQ", "s", { kinds: [24133], "#p": [rpk], limit: 0 }]));@@ -98,21 +98,21 @@ Deno.test({ name: "chlorine waits through auth_url and emits ncl:auth", ...opts
9898 const [t, , e] = JSON.parse(m.data);9999 if (t != "EVENT") return;100100 const k = await fluorine.key(rsk, e.pubkey), req = JSON.parse(await fluorine.decrypt(e.content, k));101- const reply = async (o) => ws.send(JSON.stringify(["EVENT", await krypton.finish({ kind: 24133, created_at: Math.floor(Date.now() / 1e3), tags: [["p", e.pubkey]], content: await fluorine.encrypt(JSON.stringify({ id: req.id, ...o }), k) }, rsk)]));101+ const reply = async (o) => ws.send(JSON.stringify(["EVENT", await helium.finish({ kind: 24133, created_at: Math.floor(Date.now() / 1e3), tags: [["p", e.pubkey]], content: await fluorine.encrypt(JSON.stringify({ id: req.id, ...o }), k) }, rsk)]));102102 if (req.method == "connect") return reply({ result: req.params[1] == "s3" ? "ack" : "", error: req.params[1] == "s3" ? undefined : "bad secret" });103- if (req.method == "get_public_key") return reply({ result: krypton.pub(usk) });103+ if (req.method == "get_public_key") return reply({ result: helium.pub(usk) });104104 if (req.method == "sign_event") {105105 await reply({ result: "auth_url", error: "https://signer.example/approve" });106106 await new Promise((r) => setTimeout(r, 300));107- return reply({ result: JSON.stringify(await krypton.finish(JSON.parse(req.params[0]), usk)) });107+ return reply({ result: JSON.stringify(await helium.finish(JSON.parse(req.params[0]), usk)) });108108 }109109 };110- assertEquals(await chlorine.connect(`bunker://${rpk}?relay=${encodeURIComponent(relay)}&secret=s3`), krypton.pub(usk));110+ assertEquals(await chlorine.connect(`bunker://${rpk}?relay=${encodeURIComponent(relay)}&secret=s3`), helium.pub(usk));111111 const auth = once("auth");112112 const e = await nostr.signEvent({ kind: 1, content: "needs approval" });113113 assertEquals((await auth).url, "https://signer.example/approve");114- assertEquals(e.pubkey, krypton.pub(usk));115- assert(await krypton.check(e));114+ assertEquals(e.pubkey, helium.pub(usk));115+ assert(await helium.check(e));116116 chlorine.disconnect();117117 ws.close();118118 });@@ -132,7 +132,7 @@ Deno.test({ name: "chlorine in the browser: form, xenon publish, reload, nostrco
132132 const page = await browser.newPage(), errors = [];133133 page.on("pageerror", (e) => errors.push(e.message));134134 const html = `<!doctype html><title>chlorine page</title>135- <script src="/krypton/krypton.js"></script><script src="/fluorine/fluorine.js"></script>135+ <script src="/helium/helium.js"></script><script src="/fluorine/fluorine.js"></script>136136 <script src="/chlorine/chlorine.js"></script><script src="/hydrogen/hydrogen.js"></script><script src="/xenon/xenon.js"></script>137137 <form id="login" ncl-connect><input name="bunker"><button>Connect</button></form>138138 <p ncl-relay="${relay}"><a id="nc" ncl-nostrconnect ncl-name="page">scan</a></p>@@ -160,7 +160,7 @@ Deno.test({ name: "chlorine in the browser: form, xenon publish, reload, nostrco
160160 assertEquals(await page.evaluate(() => document.documentElement.getAttribute("ncl-connected")), user);161161 assertEquals(await page.evaluate(() => document.querySelector("#nc").getAttribute("href")), null);162162 const e = await page.evaluate(() => nostr.signEvent({ kind: 1, content: "after reload" }));163- assert(await krypton.check(e));163+ assert(await helium.check(e));164164 });165165 await t.step("ncl-disconnect logs out, then nostrconnect logs back in", async () => {166166 await page.click("#out");@@ -25,7 +25,7 @@ Deno.test({ name: "demo: post, render, profile, like, cache", sanitizeResources:
2525 try {2626 await page.goto(url);2727 28- await t.step("krypton signs a note, xenon publishes it and renders it", async () => {28+ await t.step("helium signs a note, xenon publishes it and renders it", async () => {2929 await page.fill("#compose textarea", "hello <b>nobel</b> https://example.com/x");3030 await page.fill("#compose input", "nobel");3131 await page.click("#compose button");@@ -1,9 +1,9 @@
11 import { assert, assertEquals, assertRejects } from "jsr:@std/assert@1";22 import { v2 } from "npm:nostr-tools@2.17.0/nip44";3-import "../krypton/krypton.js";3+import "../helium/helium.js";44 import "../fluorine/fluorine.js";55 6-const { krypton, fluorine } = globalThis;6+const { helium, fluorine } = globalThis;77 const V = JSON.parse(await Deno.readTextFile(new URL("./nip44.vectors.json", import.meta.url))).v2;88 const hex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");99 const bytes = (h) => Uint8Array.from(h.match(/../g), (x) => parseInt(x, 16));@@ -32,9 +32,9 @@ Deno.test("padded lengths match vectors", () => {
3232 3333 Deno.test("encrypt/decrypt vectors", async () => {3434 for (const v of V.valid.encrypt_decrypt) {35- const ck = await fluorine.key(v.sec1, krypton.pub(v.sec2));35+ const ck = await fluorine.key(v.sec1, helium.pub(v.sec2));3636 assertEquals(hex(ck), v.conversation_key);37- assertEquals(hex(await fluorine.key(v.sec2, krypton.pub(v.sec1))), v.conversation_key);37+ assertEquals(hex(await fluorine.key(v.sec2, helium.pub(v.sec1))), v.conversation_key);3838 assertEquals(await fluorine.encrypt(v.plaintext, ck, bytes(v.nonce)), v.payload);3939 assertEquals(await fluorine.decrypt(v.payload, ck), v.plaintext);4040 }@@ -61,9 +61,9 @@ Deno.test("invalid message lengths are rejected", async () => {
6161 6262 Deno.test("round trips with nostr-tools", async () => {6363 for (let i = 0; i < 20; i++) {64- const a = krypton.generate(), b = krypton.generate();65- const ck = await fluorine.key(a, krypton.pub(b));66- assertEquals(hex(ck), hex(v2.utils.getConversationKey(bytes(b), krypton.pub(a))));64+ const a = helium.generate(), b = helium.generate();65+ const ck = await fluorine.key(a, helium.pub(b));66+ assertEquals(hex(ck), hex(v2.utils.getConversationKey(bytes(b), helium.pub(a))));6767 const text = "héllo 🦄 ".repeat(1 + i * 37);6868 assertEquals(v2.decrypt(await fluorine.encrypt(text, ck), ck), text);6969 assertEquals(await fluorine.decrypt(v2.encrypt(text, ck), ck), text);@@ -71,14 +71,14 @@ Deno.test("round trips with nostr-tools", async () => {
7171 });7272 7373 Deno.test("nip44 signer shape", async () => {74- const a = krypton.generate(), b = krypton.generate();74+ const a = helium.generate(), b = helium.generate();7575 const A = fluorine.nip44(a), B = fluorine.nip44(b);76- const c = await A.encrypt(krypton.pub(b), "secret");77- assertEquals(await B.decrypt(krypton.pub(a), c), "secret");78- assertEquals(await A.decrypt(krypton.pub(b), c), "secret");79- assert(krypton.signer.nip44);80- const local = krypton.key();81- const d = await krypton.signer.nip44.encrypt(krypton.pub(b), "to b");82- assertEquals(await B.decrypt(krypton.pub(local), d), "to b");83- assertEquals(await krypton.signer.nip44.decrypt(krypton.pub(b), await B.encrypt(krypton.pub(local), "to local")), "to local");76+ const c = await A.encrypt(helium.pub(b), "secret");77+ assertEquals(await B.decrypt(helium.pub(a), c), "secret");78+ assertEquals(await A.decrypt(helium.pub(b), c), "secret");79+ assert(helium.signer.nip44);80+ const local = helium.key();81+ const d = await helium.signer.nip44.encrypt(helium.pub(b), "to b");82+ assertEquals(await B.decrypt(helium.pub(local), d), "to b");83+ assertEquals(await helium.signer.nip44.decrypt(helium.pub(b), await B.encrypt(helium.pub(local), "to local")), "to local");8484 });@@ -2,9 +2,9 @@
22 import { assert, assertEquals } from "jsr:@std/assert@1";33 import { chromium } from "npm:playwright@1.58";44 import { serve } from "../server.ts";5-import "../krypton/krypton.js";5+import "../helium/helium.js";66 7-const { krypton } = globalThis;7+const { helium } = globalThis;88 const opts = { sanitizeResources: false, sanitizeOps: false };99 const TINY = Deno.env.get("NAU_TINY") ?? "ws://localhost:7447";1010 const CONTAINER = Deno.env.get("NAU_CONTAINER") ?? "noble-tiny";@@ -20,8 +20,8 @@ async function chromiumPath() {
2020 } catch { return undefined; }2121 }2222 23-// A fresh private tenant (owner writes, members read) owned by a new krypton key. Skipped without the container.24-const owner = krypton.generate(), ownerPk = krypton.pub(owner);23+// A fresh private tenant (owner writes, members read) owned by a new helium key. Skipped without the container.24+const owner = helium.generate(), ownerPk = helium.pub(owner);2525 const name = `gold${Date.now().toString(36)}`;2626 const made = await new Deno.Command("docker", { args: ["exec", CONTAINER, "tiny", "tenant", "create", "--name", name, "--owner", ownerPk, "--template", "private"] })2727 .output().then((o) => o.success, () => false);@@ -29,7 +29,7 @@ const relay = `${TINY}/r/${name}`;
2929 3030 // Seed one note as the owner (tinyrelay accepts owner writes without AUTH; only reads need it).3131 async function seed(content) {32- const ws = new WebSocket(relay), e = await krypton.finish({ kind: 1, created_at: Date.now() / 1e3 | 0, tags: [], content }, owner);32+ const ws = new WebSocket(relay), e = await helium.finish({ kind: 1, created_at: Date.now() / 1e3 | 0, tags: [], content }, owner);3333 await new Promise((r) => ws.onopen = r);3434 const ok = new Promise((r) => ws.onmessage = (m) => JSON.parse(m.data)[0] === "OK" && r(JSON.parse(m.data)[2]));3535 ws.send(JSON.stringify(["EVENT", e]));@@ -38,14 +38,14 @@ async function seed(content) {
3838 }3939 4040 const page = (key, cfg = {}) => `<!doctype html><script>41-localStorage["nkr:key"] = ${JSON.stringify(key)}41+localStorage["nhe:key"] = ${JSON.stringify(key)}4242 window.nauCfg = ${JSON.stringify(cfg)}4343 window.__log = []; window.__wire = []4444 for (const t of ["auth", "authed", "error"]) document.addEventListener("nau:" + t, (e) => __log.push([t, e.detail.pubkey ?? e.detail.error?.message ?? e.detail.challenge]))4545 document.addEventListener("nhy:message", (e) => __wire.push(e.detail.msg.slice(0, 2).concat(e.detail.msg[0] == "OK" ? e.detail.msg.slice(2) : e.detail.msg[0] == "CLOSED" ? [] : [])))4646 document.addEventListener("nxe:published", (e) => window.__results = e.detail.cfg.results)4747 </script>48-<script src="/krypton/krypton.js"></script><script src="/gold/gold.js"></script><script src="/hydrogen/hydrogen.js"></script><script src="/xenon/xenon.js"></script>48+<script src="/helium/helium.js"></script><script src="/gold/gold.js"></script><script src="/hydrogen/hydrogen.js"></script><script src="/xenon/xenon.js"></script>4949 <body>5050 <ol id="feed" nhy-relay="${relay}" nxe-filter='{"kinds":[1],"limit":10}'><template><li>{content}</li></template></ol>5151 <form id="post" nhy-relay="${relay}" nxe-publish nxe-tags='[["-"]]'><input name="content"><button>Post</button></form>`;@@ -99,7 +99,7 @@ Deno.test({ name: "gold: NIP-42 on a members-only tinyrelay tenant", ignore: !ma
9999 });100100 101101 await t.step("non-member: AUTH once, then clean failures without loops", async () => {102- const p = await open(page(krypton.generate()));102+ const p = await open(page(helium.generate()));103103 await p.fill("#post input", "intruder");104104 await p.click("#post button");105105 await p.waitForFunction(() => window.__results, null, { timeout: 8000 });@@ -116,8 +116,8 @@ Deno.test({ name: "gold: NIP-42 on a members-only tinyrelay tenant", ignore: !ma
116116 117117 await t.step("still refused after AUTH: one retry, then xenon reports the failure", async () => {118118 // AUTH as someone else, so the owner's protected event stays auth-required after the retry.119- const other = JSON.stringify(krypton.generate());120- const p = await open(page(owner).replace("<body>", `<script>document.addEventListener("nhy:config", (e) => e.detail.cfg.event?.kind == 22242 && (e.detail.cfg.sign = (ev) => krypton.finish(ev, ${other})))</script><body>`));119+ const other = JSON.stringify(helium.generate());120+ const p = await open(page(owner).replace("<body>", `<script>document.addEventListener("nhy:config", (e) => e.detail.cfg.event?.kind == 22242 && (e.detail.cfg.sign = (ev) => helium.finish(ev, ${other})))</script><body>`));121121 await p.fill("#post input", "wrong identity");122122 await p.click("#post button");123123 await p.waitForFunction(() => window.__results, null, { timeout: 8000 });@@ -180,10 +180,10 @@ Deno.test({ name: "gold: challenge that arrives after the auth-required refusal"
180180 if (t == "REQ" && !authed) {181181 send(["CLOSED", a, "auth-required: who are you"]);182182 setTimeout(() => send(["AUTH", "late-challenge"]), 300);183- } else if (t == "REQ") send(["EVENT", a, await krypton.finish({ kind: 1, created_at: 1, tags: [], content: "for " + authed }, owner)]);183+ } else if (t == "REQ") send(["EVENT", a, await helium.finish({ kind: 1, created_at: 1, tags: [], content: "for " + authed }, owner)]);184184 else if (t == "AUTH") {185185 auths++;186- const ok = await krypton.check(a) && a.kind == 22242 && a.tags.some((x) => x[0] == "challenge" && x[1] == "late-challenge");186+ const ok = await helium.check(a) && a.kind == 22242 && a.tags.some((x) => x[0] == "challenge" && x[1] == "late-challenge");187187 if (ok) authed = a.pubkey;188188 send(["OK", a.id, ok, ok ? "" : "invalid"]);189189 }@@ -1,62 +1,44 @@
1-// Reconciles helium's initiator against bindws's Negentropy responder (a port of go-nostr's).2-import { assertEquals } from "jsr:@std/assert@1";3-import { sha256 } from "npm:@noble/hashes@1.8.0/sha2";1+import { assert, assertEquals } from "jsr:@std/assert@1";2+import { schnorr } from "npm:@noble/curves@1.9.7/secp256k1";3+import { finalizeEvent, verifyEvent } from "npm:nostr-tools@2.17.0/pure";44 import "../helium/helium.js";55 66 const { helium } = globalThis;7-const peer = new URL("../../bindws/src/negentropy.ts", import.meta.url);8-const hasPeer = await Deno.stat(peer).then(() => true, () => false);9-const { Negentropy, hexToBytes, bytesToHex } = hasPeer ? await import(peer.href) : {};7+const hex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");8+const rand = (n = 32) => crypto.getRandomValues(new Uint8Array(n));109 11-const rid = () => [...crypto.getRandomValues(new Uint8Array(32))].map((x) => x.toString(16).padStart(2, "0")).join("");12-const item = (t, id = rid()) => ({ t, id, b: hexToBytes(id) });13-const sorted = (xs) => [...xs].sort((a, b) => a.t - b.t || (a.id < b.id ? -1 : 1));10+Deno.test("public keys match @noble/curves", () => {11+ for (let i = 0; i < 20; i++) {12+ const sk = helium.generate();13+ assertEquals(helium.pub(sk), hex(schnorr.getPublicKey(sk)));14+ }15+});1416 15-async function reconcile(mine, theirs) {16- const client = helium.session(sorted(mine));17- const server = new Negentropy(theirs.map((x) => ({ timestamp: x.t, id: x.b })), (b) => sha256(b));18- let msg = await client.start(), rounds = 0;19- const have = new Set(), need = new Set();20- for (;;) {21- const { reply } = server.reconcile(hexToBytes(msg));22- const r = await client.reconcile(bytesToHex(reply));23- r.have.forEach((x) => have.add(x));24- r.need.forEach((x) => need.add(x));25- rounds++;26- if (!r.reply) break;27- msg = r.reply;17+Deno.test("signatures match @noble/curves byte for byte", async () => {18+ for (let i = 0; i < 10; i++) {19+ const sk = rand(), m = rand(), aux = rand();20+ const ours = await helium.sign(m, sk, aux);21+ assertEquals(hex(ours), hex(schnorr.sign(m, sk, aux)));22+ assert(await helium.verify(ours, m, schnorr.getPublicKey(sk)));2823 }29- return { have, need, rounds };30-}24+});3125 32-function check(mine, theirs, got) {33- const m = new Set(mine.map((x) => x.id)), t = new Set(theirs.map((x) => x.id));34- assertEquals([...got.have].sort(), [...m].filter((x) => !t.has(x)).sort());35- assertEquals([...got.need].sort(), [...t].filter((x) => !m.has(x)).sort());36-}26+Deno.test("verify rejects tampered signatures", async () => {27+ const sk = rand(), m = rand(), pk = schnorr.getPublicKey(sk);28+ const sig = schnorr.sign(m, sk);29+ assert(await helium.verify(sig, m, pk));30+ sig[5] ^= 1;31+ assert(!(await helium.verify(sig, m, pk)));32+ assert(!(await helium.verify(sig.slice(0, 40), m, pk)));33+});3734 38-Deno.test({ name: "helium reconciles with bindws negentropy", ignore: !hasPeer }, async (t) => {39- const cases = {40- "both empty": [0, 0, 0],41- "only mine": [40, 0, 0],42- "only theirs": [0, 40, 0],43- "around the 32 id list threshold": [31, 33, 16],44- "identical 3000": [0, 0, 3000],45- "5000 each, mostly shared": [300, 200, 4500],46- "small diff in 20000": [3, 5, 20000],47- };48- for (const [name, [a, b, shared]] of Object.entries(cases)) {49- await t.step(name, async () => {50- const common = Array.from({ length: shared }, (_, i) => item(1_700_000_000 + (i % 997)));51- const mine = [...common, ...Array.from({ length: a }, (_, i) => item(1_700_000_000 + i * 7))];52- const theirs = [...common, ...Array.from({ length: b }, (_, i) => item(1_700_000_000 + i * 3))];53- check(mine, theirs, await reconcile(mine, theirs));54- });55- }56- await t.step("shared timestamps force id prefix bounds", async () => {57- const same = Array.from({ length: 2000 }, () => item(1_700_000_000));58- const mine = [...same, ...Array.from({ length: 10 }, () => item(1_700_000_000))];59- const theirs = [...same.slice(50), ...Array.from({ length: 10 }, () => item(1_700_000_000))];60- check(mine, theirs, await reconcile(mine, theirs));61- });35+Deno.test("events interoperate with nostr-tools", async () => {36+ const sk = helium.generate();37+ const tmpl = { kind: 1, created_at: 1700000000, tags: [["t", "nobel"]], content: "hello \"nostr\" ✨" };38+ const ours = await helium.finish(tmpl, sk);39+ assert(verifyEvent({ ...ours }));40+ const theirs = finalizeEvent({ ...tmpl }, Uint8Array.from(sk.match(/../g), (x) => parseInt(x, 16)));41+ assertEquals(ours.id, theirs.id);42+ assert(await helium.check(theirs));43+ assert(!(await helium.check({ ...theirs, content: "tampered" })));6244 });@@ -53,7 +53,7 @@ Deno.test({ name: "iodine: NIP-17 private messages", sanitizeResources: false, s
5353 page.on("pageerror", (e) => errors.push(e.message));5454 await page.goto(url);5555 await page.waitForFunction(() => document.querySelector("#inbox").hasAttribute("nxe-filter"));56- const pk = await page.evaluate(() => krypton.signer.getPublicKey());56+ const pk = await page.evaluate(() => helium.signer.getPublicKey());5757 return { page, pk, npub: await page.textContent("#npub") };5858 };5959 const items = (page, sel = "#inbox") => page.locator(`${sel} li`);@@ -63,7 +63,7 @@ Deno.test({ name: "iodine: NIP-17 private messages", sanitizeResources: false, s
6363 const plain = "meet at <b>noon</b>";6464 6565 await t.step("B's kind 10050 DM relays are looked up", async () => {66- const r = await B.page.evaluate(async (rs) => hydrogen.pub(await krypton.signer.signEvent({ kind: 10050, created_at: Date.now() / 1e3 | 0, tags: [["relay", rs]], content: "" }), nhyCfg.relays), relay + "?dm");66+ const r = await B.page.evaluate(async (rs) => hydrogen.pub(await helium.signer.signEvent({ kind: 10050, created_at: Date.now() / 1e3 | 0, tags: [["relay", rs]], content: "" }), nhyCfg.relays), relay + "?dm");6767 assert(r[0].ok);6868 assertEquals(await A.page.evaluate((pk) => iodine.relays(pk), B.pk), [relay + "?dm"]);6969 assertEquals(await A.page.evaluate((pk) => iodine.relays(pk), C.pk), []);@@ -155,7 +155,7 @@ Deno.test({ name: "iodine: NIP-17 private messages", sanitizeResources: false, s
155155 const [w] = await fetchRelay(relay, { kinds: [1059], "#p": [pk] });156156 const r = nip59.unwrapEvent(w, sk);157157 assertEquals([r.pubkey, r.kind, r.content, r.tags], [A.pk, 14, "hi to nostr-tools", [["p", pk]]]);158- const w2 = await A.page.evaluate(async (pk) => iodine.wrap({ pubkey: await krypton.signer.getPublicKey(), created_at: 1, kind: 14, tags: [], content: "x", id: "0".repeat(64) }, pk), pk);158+ const w2 = await A.page.evaluate(async (pk) => iodine.wrap({ pubkey: await helium.signer.getPublicKey(), created_at: 1, kind: 14, tags: [], content: "x", id: "0".repeat(64) }, pk), pk);159159 assert(verifyEvent(w2) && w2.kind === 1059);160160 assertEquals(nip59.unwrapEvent(w2, sk).content, "x");161161 });@@ -1,44 +0,0 @@
1-import { assert, assertEquals } from "jsr:@std/assert@1";2-import { schnorr } from "npm:@noble/curves@1.9.7/secp256k1";3-import { finalizeEvent, verifyEvent } from "npm:nostr-tools@2.17.0/pure";4-import "../krypton/krypton.js";5-6-const { krypton } = globalThis;7-const hex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");8-const rand = (n = 32) => crypto.getRandomValues(new Uint8Array(n));9-10-Deno.test("public keys match @noble/curves", () => {11- for (let i = 0; i < 20; i++) {12- const sk = krypton.generate();13- assertEquals(krypton.pub(sk), hex(schnorr.getPublicKey(sk)));14- }15-});16-17-Deno.test("signatures match @noble/curves byte for byte", async () => {18- for (let i = 0; i < 10; i++) {19- const sk = rand(), m = rand(), aux = rand();20- const ours = await krypton.sign(m, sk, aux);21- assertEquals(hex(ours), hex(schnorr.sign(m, sk, aux)));22- assert(await krypton.verify(ours, m, schnorr.getPublicKey(sk)));23- }24-});25-26-Deno.test("verify rejects tampered signatures", async () => {27- const sk = rand(), m = rand(), pk = schnorr.getPublicKey(sk);28- const sig = schnorr.sign(m, sk);29- assert(await krypton.verify(sig, m, pk));30- sig[5] ^= 1;31- assert(!(await krypton.verify(sig, m, pk)));32- assert(!(await krypton.verify(sig.slice(0, 40), m, pk)));33-});34-35-Deno.test("events interoperate with nostr-tools", async () => {36- const sk = krypton.generate();37- const tmpl = { kind: 1, created_at: 1700000000, tags: [["t", "nobel"]], content: "hello \"nostr\" ✨" };38- const ours = await krypton.finish(tmpl, sk);39- assert(verifyEvent({ ...ours }));40- const theirs = finalizeEvent({ ...tmpl }, Uint8Array.from(sk.match(/../g), (x) => parseInt(x, 16)));41- assertEquals(ours.id, theirs.id);42- assert(await krypton.check(theirs));43- assert(!(await krypton.check({ ...theirs, content: "tampered" })));44-});@@ -0,0 +1,62 @@
1+// Reconciles lithium's initiator against bindws's Negentropy responder (a port of go-nostr's).2+import { assertEquals } from "jsr:@std/assert@1";3+import { sha256 } from "npm:@noble/hashes@1.8.0/sha2";4+import "../lithium/lithium.js";5+6+const { lithium } = globalThis;7+const peer = new URL("../../bindws/src/negentropy.ts", import.meta.url);8+const hasPeer = await Deno.stat(peer).then(() => true, () => false);9+const { Negentropy, hexToBytes, bytesToHex } = hasPeer ? await import(peer.href) : {};10+11+const rid = () => [...crypto.getRandomValues(new Uint8Array(32))].map((x) => x.toString(16).padStart(2, "0")).join("");12+const item = (t, id = rid()) => ({ t, id, b: hexToBytes(id) });13+const sorted = (xs) => [...xs].sort((a, b) => a.t - b.t || (a.id < b.id ? -1 : 1));14+15+async function reconcile(mine, theirs) {16+ const client = lithium.session(sorted(mine));17+ const server = new Negentropy(theirs.map((x) => ({ timestamp: x.t, id: x.b })), (b) => sha256(b));18+ let msg = await client.start(), rounds = 0;19+ const have = new Set(), need = new Set();20+ for (;;) {21+ const { reply } = server.reconcile(hexToBytes(msg));22+ const r = await client.reconcile(bytesToHex(reply));23+ r.have.forEach((x) => have.add(x));24+ r.need.forEach((x) => need.add(x));25+ rounds++;26+ if (!r.reply) break;27+ msg = r.reply;28+ }29+ return { have, need, rounds };30+}31+32+function check(mine, theirs, got) {33+ const m = new Set(mine.map((x) => x.id)), t = new Set(theirs.map((x) => x.id));34+ assertEquals([...got.have].sort(), [...m].filter((x) => !t.has(x)).sort());35+ assertEquals([...got.need].sort(), [...t].filter((x) => !m.has(x)).sort());36+}37+38+Deno.test({ name: "lithium reconciles with bindws negentropy", ignore: !hasPeer }, async (t) => {39+ const cases = {40+ "both empty": [0, 0, 0],41+ "only mine": [40, 0, 0],42+ "only theirs": [0, 40, 0],43+ "around the 32 id list threshold": [31, 33, 16],44+ "identical 3000": [0, 0, 3000],45+ "5000 each, mostly shared": [300, 200, 4500],46+ "small diff in 20000": [3, 5, 20000],47+ };48+ for (const [name, [a, b, shared]] of Object.entries(cases)) {49+ await t.step(name, async () => {50+ const common = Array.from({ length: shared }, (_, i) => item(1_700_000_000 + (i % 997)));51+ const mine = [...common, ...Array.from({ length: a }, (_, i) => item(1_700_000_000 + i * 7))];52+ const theirs = [...common, ...Array.from({ length: b }, (_, i) => item(1_700_000_000 + i * 3))];53+ check(mine, theirs, await reconcile(mine, theirs));54+ });55+ }56+ await t.step("shared timestamps force id prefix bounds", async () => {57+ const same = Array.from({ length: 2000 }, () => item(1_700_000_000));58+ const mine = [...same, ...Array.from({ length: 10 }, () => item(1_700_000_000))];59+ const theirs = [...same.slice(50), ...Array.from({ length: 10 }, () => item(1_700_000_000))];60+ check(mine, theirs, await reconcile(mine, theirs));61+ });62+});@@ -2,11 +2,11 @@
22 // Each relay gets a fresh key: some events only local, some only remote, some on both.33 import "npm:fake-indexeddb@6/auto";44 import { assertEquals } from "jsr:@std/assert@1";5-import "../krypton/krypton.js";6-import "../argon/argon.js";75 import "../helium/helium.js";6+import "../argon/argon.js";7+import "../lithium/lithium.js";88 9-const { krypton, argon, helium } = globalThis;9+const { helium, argon, lithium } = globalThis;1010 const relays = (Deno.env.get("NOBEL_RELAYS") ?? "").split(/\s+/).filter(Boolean);1111 const SIZE = Number(Deno.env.get("NOBEL_SIZE") ?? 60);1212 @@ -38,31 +38,31 @@ async function publish(url, events) {
3838 }3939 4040 for (const url of relays) {41- Deno.test({ name: `helium syncs with ${url}`, sanitizeResources: false, sanitizeOps: false }, async (t) => {41+ Deno.test({ name: `lithium syncs with ${url}`, sanitizeResources: false, sanitizeOps: false }, async (t) => {4242 await argon.remove();43- const sk = krypton.generate(), pk = krypton.pub(sk), now = Math.floor(Date.now() / 1000);43+ const sk = helium.generate(), pk = helium.pub(sk), now = Math.floor(Date.now() / 1000);4444 const make = (n, tag) => Promise.all(Array.from({ length: n }, (_, i) =>45- krypton.finish({ kind: 1, created_at: now - (i % 50), tags: [["t", "nobel-test"]], content: `${tag} ${i}` }, sk)));45+ helium.finish({ kind: 1, created_at: now - (i % 50), tags: [["t", "nobel-test"]], content: `${tag} ${i}` }, sk)));4646 const shared = await make(SIZE, "shared"), local = await make(Math.ceil(SIZE / 3), "local"), remote = await make(Math.ceil(SIZE / 4), "remote");4747 await publish(url, [...shared, ...remote]);4848 for (const e of [...shared, ...local]) await argon.put(e);4949 const filter = { authors: [pk] };5050 5151 await t.step("first sync pulls what is missing and pushes what the relay lacks", async () => {52- const r = await helium.sync(filter, url);52+ const r = await lithium.sync(filter, url);5353 assertEquals([r.have.length, r.need.length, r.pushed, r.pulled], [local.length, remote.length, local.length, remote.length]);5454 assertEquals((await argon.query([filter])).length, shared.length + local.length + remote.length);5555 });5656 5757 await t.step("second sync finds nothing to do", async () => {58- const r = await helium.sync(filter, url);58+ const r = await lithium.sync(filter, url);5959 assertEquals([r.have.length, r.need.length], [0, 0]);6060 });6161 6262 await t.step("pull mode never publishes", async () => {6363 const extra = await make(3, "pull-only");6464 for (const e of extra) await argon.put(e);65- const r = await helium.sync(filter, url, "pull");65+ const r = await lithium.sync(filter, url, "pull");6666 assertEquals([r.have.length, r.pushed, r.need.length], [3, 0, 0]);6767 });6868 });@@ -4,19 +4,19 @@ import { assert, assertEquals, assertRejects } from "jsr:@std/assert@1";
44 import * as nip98 from "npm:nostr-tools@2.17.0/nip98";55 import { verifyEvent } from "npm:nostr-tools@2.17.0/pure";66 import { chromium } from "npm:playwright@1.58";7-import "../krypton/krypton.js";7+import "../helium/helium.js";88 import "../mercury/mercury.js";99 10-const { krypton, mercury } = globalThis;10+const { helium, mercury } = globalThis;1111 const root = new URL("../", import.meta.url);1212 const hex = (b) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");1313 const sha = async (b) => hex(await crypto.subtle.digest("SHA-256", b));1414 const unpack = (h) => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(h.replace(/^Nostr /, "")), (c) => c.charCodeAt(0))));1515 const tag = (e, k) => e.tags.find((t) => t[0] === k)?.[1];1616 17-const sk = krypton.generate();18-krypton.key(sk);19-const pk = krypton.pub(sk);17+const sk = helium.generate();18+helium.key(sk);19+const pk = helium.pub(sk);2020 2121 // NIP-98: nostr-tools checks signature, kind, u, method and a 60s created_at window; we check the payload hash2222 // against the raw bytes, and nostr-tools' own payload check for JSON bodies.@@ -44,7 +44,7 @@ async function blossomCheck(req, t, x) {
4444 }4545 4646 let fixi;47-const page = `<!doctype html><script src="/fixi.js"></script><script src="/krypton/krypton.js"></script><script src="/mercury/mercury.js"></script>47+const page = `<!doctype html><script src="/fixi.js"></script><script src="/helium/helium.js"></script><script src="/mercury/mercury.js"></script>4848 <button id=x fx-action="/api/x" fx-method="post" nhg-sign fx-target="#out" fx-swap="innerHTML" name=n value=1>x</button>4949 <form fx-action="/api/x?y=2" fx-method="put" nhg-sign fx-target="#out" fx-swap="innerHTML"><input name=msg value="hé"><button id=f>f</button></form>5050 <div nhg-sign><button id=g fx-action="/api/x?q=1" fx-target="#out" fx-swap="innerHTML">g</button></div>@@ -56,7 +56,7 @@ async function handler(req) {
5656 try {5757 if (u.pathname === "/") return new Response(page, { headers: { "content-type": "text/html; charset=utf-8" } });5858 if (u.pathname === "/fixi.js") return new Response(fixi, { headers: { "content-type": "text/javascript" } });59- if (/^\/(krypton|mercury)\//.test(u.pathname)) return new Response(await Deno.readFile(new URL("." + u.pathname, root)), { headers: { "content-type": "text/javascript" } });59+ if (/^\/(helium|mercury)\//.test(u.pathname)) return new Response(await Deno.readFile(new URL("." + u.pathname, root)), { headers: { "content-type": "text/javascript" } });6060 if (u.pathname.startsWith("/api/")) {6161 const e = await nip98Check(req, body);6262 const form = /multipart|urlencoded/.test(req.headers.get("content-type")) ? Object.fromEntries(await new Request(req.url, { method: "POST", headers: req.headers, body }).formData()) : undefined;@@ -133,10 +133,10 @@ Deno.test({ name: "mercury: NIP-98 requests validate with nostr-tools", ...opts
133133 });134134 135135 await t.step("nhy:config can swap the signer; cancelling nhg:sign aborts and fires nhg:error", async () => {136- const other = krypton.generate();137- const swap = (e) => e.detail.cfg.sign = (ev) => krypton.finish(ev, other);136+ const other = helium.generate();137+ const swap = (e) => e.detail.cfg.sign = (ev) => helium.finish(ev, other);138138 globalThis.addEventListener("nhy:config", swap);139- assertEquals((await mercury.post(`${base}/api/e`, "x").json()).pubkey, krypton.pub(other));139+ assertEquals((await mercury.post(`${base}/api/e`, "x").json()).pubkey, helium.pub(other));140140 globalThis.removeEventListener("nhy:config", swap);141141 const seen = [];142142 const cancel = (e) => (seen.push(e.detail.cfg.event.kind), e.preventDefault());@@ -199,7 +199,7 @@ Deno.test({ name: "mercury: fixi requests under nhg-sign carry NIP-98", ...opts
199199 };200200 try {201201 await p.goto(base + "/");202- const me = await p.evaluate(() => krypton.pub(krypton.key()));202+ const me = await p.evaluate(() => helium.pub(helium.key()));203203 await t.step("a POST button signs its multipart body", async () => {204204 const r = await click("#x");205205 assertEquals([r.pubkey, r.method, r.u, r.form], [me, "POST", `${base}/api/x`, { n: "1" }]);@@ -230,7 +230,7 @@ Deno.test({ name: "mercury: fixi requests under nhg-sign carry NIP-98", ...opts
230230 // NHG_TINY=http://localhost:7447/r/<tenant> NHG_KEY=<owner secret hex>231231 const tiny = Deno.env.get("NHG_TINY"), tinyKey = Deno.env.get("NHG_KEY");232232 Deno.test({ name: "mercury: tinyrelay NIP-86 and Blossom", ignore: !tiny || !tinyKey, ...opts }, async () => {233- krypton.key(tinyKey);233+ helium.key(tinyKey);234234 try {235235 const rpc = (method, params = []) => mercury.post(tiny, { method, params }, { headers: { "content-type": "application/nostr+json+rpc" } }).json();236236 const r = await rpc("supportedmethods");@@ -242,6 +242,6 @@ Deno.test({ name: "mercury: tinyrelay NIP-86 and Blossom", ignore: !tiny || !tin
242242 assertEquals(d.sha256.length, 64);243243 assert(await mercury.remove(tiny, d.sha256, { server }));244244 } finally {245- krypton.key(sk);245+ helium.key(sk);246246 }247247 });@@ -1,4 +1,4 @@
1-// Two browsers (separate localStorage, so separate krypton keys) meet on demo/oganesson.html.1+// Two browsers (separate localStorage, so separate helium keys) meet on demo/oganesson.html.22 import { assert, assertEquals } from "jsr:@std/assert@1";33 import { chromium } from "npm:playwright@1.58";44 import { serve } from "../server.ts";@@ -40,8 +40,8 @@ Deno.test({ name: "oganesson: presence, typing, cursors, nothing stored", saniti
4040 };4141 try {4242 const a = await open(), b = await open();43- const pkA = await a.evaluate(() => krypton.signer.getPublicKey());44- const pkB = await b.evaluate(() => krypton.signer.getPublicKey());43+ const pkA = await a.evaluate(() => helium.signer.getPublicKey());44+ const pkB = await b.evaluate(() => helium.signer.getPublicKey());4545 assert(pkA !== pkB, "separate contexts have separate keys");4646 const got = (kind) => a.evaluate((k) => window.__got.filter((e) => e.kind === k), kind);4747