clone

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

main
nobel / test / iodine.test.js
iodine.test.js 8.7K
1// Drives demo/iodine.html in headless Chromium: NIP-17 private messages between separate browser contexts.2import { assert, assertEquals } from "jsr:@std/assert@1";3import { chromium } from "npm:playwright@1.58";4import * as nip17 from "npm:nostr-tools@2.17.0/nip17";5import * as nip59 from "npm:nostr-tools@2.17.0/nip59";6import { generateSecretKey, getPublicKey, verifyEvent } from "npm:nostr-tools@2.17.0/pure";7import { serve } from "../server.ts";89// Use CHROMIUM if set, else the newest headless shell already in the Playwright cache.10async function chromiumPath() {11 if (Deno.env.get("CHROMIUM")) return Deno.env.get("CHROMIUM");12 const cache = `${Deno.env.get("HOME")}/Library/Caches/ms-playwright`;13 try {14 const dirs = [...Deno.readDirSync(cache)].map((d) => d.name).filter((n) => n.startsWith("chromium_headless_shell-")).sort();15 const dir = `${cache}/${dirs.at(-1)}`;16 const arch = [...Deno.readDirSync(dir)][0].name;17 return `${dir}/${arch}/chrome-headless-shell`;18 } catch { return undefined; }19}2021// Raw REQ against the relay, collecting stored events until EOSE.22function fetchRelay(url, filter) {23 return new Promise((res, rej) => {24 const ws = new WebSocket(url), out = [];25 ws.onopen = () => ws.send(JSON.stringify(["REQ", "t", filter]));26 ws.onerror = rej;27 ws.onmessage = (m) => {28 const [t, , e] = JSON.parse(m.data);29 if (t === "EVENT") out.push(e);30 if (t === "EOSE") res(out, ws.close());31 };32 });33}3435function publishRelay(url, event) {36 return new Promise((res, rej) => {37 const ws = new WebSocket(url);38 ws.onopen = () => ws.send(JSON.stringify(["EVENT", event]));39 ws.onerror = rej;40 ws.onmessage = (m) => res(JSON.parse(m.data)[2], ws.close());41 });42}4344Deno.test({ name: "iodine: NIP-17 private messages", sanitizeResources: false, sanitizeOps: false }, async (t) => {45 const server = serve(0);46 const relay = `ws://localhost:${server.addr.port}/relay`;47 const url = `http://localhost:${server.addr.port}/demo/iodine.html`;48 const browser = await chromium.launch({ executablePath: await chromiumPath() });49 const errors = [];50 const open = async () => {51 const ctx = await browser.newContext();52 const page = await ctx.newPage();53 page.on("pageerror", (e) => errors.push(e.message));54 await page.goto(url);55 await page.waitForFunction(() => document.querySelector("#inbox").hasAttribute("nxe-filter"));56 const pk = await page.evaluate(() => helium.signer.getPublicKey());57 return { page, pk, npub: await page.textContent("#npub") };58 };59 const items = (page, sel = "#inbox") => page.locator(`${sel} li`);60 try {61 const A = await open(), B = await open(), C = await open();62 assert(A.pk !== B.pk && B.pk !== C.pk, "separate contexts, separate keys");63 const plain = "meet at <b>noon</b>";6465 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 helium.signer.signEvent({ kind: 10050, created_at: Date.now() / 1e3 | 0, tags: [["relay", rs]], content: "" }), nhyCfg.relays), relay + "?dm");67 assert(r[0].ok);68 assertEquals(await A.page.evaluate((pk) => iodine.relays(pk), B.pk), [relay + "?dm"]);69 assertEquals(await A.page.evaluate((pk) => iodine.relays(pk), C.pk), []);70 });7172 await t.step("A sends to B through the form", async () => {73 await A.page.fill("#send input", B.npub);74 await A.page.fill("#send textarea", plain);75 await A.page.click("#send button");76 await A.page.waitForFunction(() => /^sent/.test(document.querySelector("#log").textContent), null, { timeout: 8000 })77 .catch(async (e) => { throw new Error(`status: ${await A.page.textContent("#log")}`, { cause: e }); });78 const log = await A.page.textContent("#log");79 assert(log.includes(`ok ${relay}?dm`), "B's wrap goes to B's DM relay: " + log);80 assert(log.includes(`ok ${relay},`) || log.endsWith(`ok ${relay}`), "A's copy falls back to the element's relays: " + log);81 assertEquals(await A.page.inputValue("#send textarea"), "", "form resets");82 });8384 await t.step("B's inbox shows the message with A as author", async () => {85 await B.page.waitForSelector("#inbox li");86 const li = items(B.page).first();87 assertEquals(await li.locator("p").textContent(), plain, "content is escaped text");88 assertEquals(await li.locator("p b").count(), 0);89 assertEquals(await li.locator("b").first().textContent(), A.npub);90 });9192 await t.step("A's own inbox shows A's copy", async () => {93 await A.page.waitForSelector("#inbox li");94 assertEquals(await items(A.page).first().locator("p").textContent(), plain);95 assertEquals(await items(A.page).first().locator("b").first().textContent(), A.npub);96 assertEquals(await items(A.page).count(), 1);97 });9899 await t.step("C sees nothing, and the relay holds only random pubkeys and ciphertext", async () => {100 await C.page.waitForTimeout(500);101 assertEquals(await items(C.page).count(), 0);102 const wraps = await fetchRelay(relay, { kinds: [1059] });103 assertEquals(wraps.length, 2);104 assertEquals(new Set(wraps.map((w) => w.tags[0][1])), new Set([A.pk, B.pk]));105 for (const w of wraps) {106 assert(![A.pk, B.pk, C.pk].includes(w.pubkey), "wrap signed by a random key");107 assert(!w.content.includes("noon") && !w.content.includes(A.pk));108 assert(verifyEvent(w));109 assert(w.created_at <= Date.now() / 1e3 + 1 && w.created_at > Date.now() / 1e3 - 2 * 86400 - 5);110 }111 assertEquals((await fetchRelay(relay, { kinds: [13, 14] })).length, 0);112 assertEquals(await C.page.evaluate((w) => iodine.unwrap(w), wraps[0]), undefined, "C cannot unwrap");113 });114115 await t.step("argon caches no messages (nar-ignore)", async () => {116 for (const p of [A.page, B.page]) {117 assertEquals(await p.evaluate(async () => (await argon.query([{ kinds: [13, 14, 1059] }])).length), 0);118 }119 });120121 await t.step("nio-with filters conversations", async () => {122 await C.page.evaluate((to) => iodine.send(to, "hi from C"), B.npub);123 await B.page.waitForFunction(() => document.querySelectorAll("#inbox li").length === 2);124 assertEquals(await items(B.page).first().locator("p").textContent(), "hi from C", "newest first");125 await B.page.evaluate(([a, c]) => {126 document.querySelector("main").insertAdjacentHTML("beforeend",127 `<ol id="withA" nio-inbox nio-with="${a}"><template><li><p>{content}</p></li></template></ol>` +128 `<ol id="withC" nio-inbox nio-with="${c}"><template><li><p>{content}</p></li></template></ol>`);129 }, [A.npub, C.pk]);130 await B.page.waitForSelector("#withA li");131 await B.page.waitForSelector("#withC li");132 await B.page.waitForTimeout(300);133 assertEquals(await items(B.page, "#withA").allTextContents(), [plain]);134 assertEquals(await items(B.page, "#withC").allTextContents(), ["hi from C"]);135 // A's own copy is "to B", so it belongs to A's conversation with B136 await A.page.evaluate((b) => document.querySelector("main").insertAdjacentHTML("beforeend",137 `<ol id="withB" nio-inbox nio-with="${b}"><template><li><p>{content}</p></li></template></ol>`), B.pk);138 await A.page.waitForSelector("#withB li");139 assertEquals(await items(A.page, "#withB").allTextContents(), [plain]);140 });141142 await t.step("nostr-tools gift wraps unwrap in iodine", async () => {143 const sk = generateSecretKey(), pk = getPublicKey(sk);144 const w = nip17.wrapEvent(sk, { publicKey: B.pk }, "hi from nostr-tools");145 const r = await B.page.evaluate((w) => iodine.unwrap(w), w);146 assertEquals([r.pubkey, r.kind, r.content, r.tags[0]], [pk, 14, "hi from nostr-tools", ["p", B.pk]]);147 assert(await publishRelay(relay, w));148 await B.page.waitForFunction(() => document.querySelector("#inbox li p").textContent === "hi from nostr-tools");149 });150151 await t.step("iodine gift wraps unwrap in nostr-tools", async () => {152 const sk = generateSecretKey(), pk = getPublicKey(sk);153 const res = await A.page.evaluate((pk) => iodine.send(pk, "hi to nostr-tools", [nhyCfg.relays[0]]), pk);154 assert(res.every((r) => r.ok));155 const [w] = await fetchRelay(relay, { kinds: [1059], "#p": [pk] });156 const r = nip59.unwrapEvent(w, sk);157 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 helium.signer.getPublicKey(), created_at: 1, kind: 14, tags: [], content: "x", id: "0".repeat(64) }, pk), pk);159 assert(verifyEvent(w2) && w2.kind === 1059);160 assertEquals(nip59.unwrapEvent(w2, sk).content, "x");161 });162163 assertEquals(errors, []);164 } finally {165 await browser.close();166 await server.shutdown();167 }168});