clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

main
bindws / test / object / nip66.test.ts
nip66.test.ts 4.1K
1// NIP-66: the relay signs a kind 30166 discovery record about itself, under2// its primary URL, restating the NIP-11 document as tags, and re-signs it3// only when what it says would change.4import { SELF } from "cloudflare:test";5import { describe, it, expect } from "vitest";6import { finalizeEvent, generateSecretKey, type Event } from "nostr-tools/pure";7import { getToken } from "nostr-tools/nip98";8import { rpc, info } from "../helpers/relay.ts";910// query reads through the HTTP bridge as the owner, so the read rule never gets in the way.11async function query(host: string, sk: Uint8Array, filter: unknown): Promise<Event[]> {12 const url = `http://${host}/query`;13 const body = [filter];14 const authorization = await getToken(url, "POST", (e) => finalizeEvent(e, sk), true, body as any);15 const resp = await SELF.fetch(url, { method: "POST", headers: { "content-type": "application/json", authorization }, body: JSON.stringify(body) });16 expect(resp.status).toBe(200);17 return resp.json<Event[]>();18}1920const vals = (e: Event, name: string) => e.tags.filter((t) => t[0] === name).map((t) => t[1]);2122describe("NIP-66 discovery record", () => {23 it("is signed by the relay under its primary URL, restates NIP-11 and follows the rules", async () => {24 const host = "sixtysix.bind.ws";25 const owner = generateSecretKey();26 await rpc(host, owner, "claim");27 await rpc(host, owner, "setpolicy", { tags: ["bitcoin", "cats"], languageTags: ["en", "pt-BR", "pt"], reads: "auth" });28 await rpc(host, owner, "allowkind", 1);29 await rpc(host, owner, "disallowkind", 4);30 const doc = await info(host);31 expect(doc.supported_nips).toContain(66);32 const self = doc.self as string;33 expect(self).toMatch(/^[0-9a-f]{64}$/);3435 const found = await query(host, owner, { kinds: [30166], authors: [self] });36 expect(found.length).toBe(1);37 const e = found[0];38 expect(vals(e, "d")).toEqual([`wss://${host}/`]);39 expect(vals(e, "n")).toEqual(["clearnet"]);40 expect(vals(e, "T")).toEqual(["CommunityManagerRelays", "SearchRelays"]);41 expect(vals(e, "N")).toContain("1");42 expect(vals(e, "N")).toContain("66");43 expect(vals(e, "N")).toContain("43");44 expect(vals(e, "R").sort()).toEqual(["!payment", "!pow", "!writes", "auth"]);45 expect(vals(e, "t")).toEqual(["bitcoin", "cats"]);46 expect(e.tags.filter((t) => t[0] === "l")).toEqual([["l", "en", "ISO-639-1"], ["l", "pt", "ISO-639-1"]]);47 expect(vals(e, "k")).toEqual(["1", "!4"]);48 expect(e.tags[0]).toEqual(["-"]);49 const content = JSON.parse(e.content);50 expect(content.self).toBe(self);51 expect(content.tags).toEqual(["bitcoin", "cats"]);52 expect(content.limitation.auth_required).toBe(true);53 });5455 it("is re-signed when the rules change and left alone when they do not", async () => {56 const host = "sixtysix-again.bind.ws";57 const owner = generateSecretKey();58 await rpc(host, owner, "claim");59 const self = (await info(host)).self as string;60 const first = (await query(host, owner, { kinds: [30166], authors: [self] }))[0];61 expect(first).toBeDefined();62 expect(vals(first, "T")).toEqual(["PublicInbox", "SearchRelays"]);63 expect(vals(first, "R")).toContain("!auth");64 expect(vals(first, "R")).toContain("!pow");6566 // The same policy again: nothing to say, so nothing is signed.67 await rpc(host, owner, "setpolicy", { reads: "open" });68 const same = await query(host, owner, { kinds: [30166], authors: [self] });69 expect(same.length).toBe(1);70 expect(same[0].id).toBe(first.id);7172 // Proof of work and owner-only writes change the requirements and the type.73 await rpc(host, owner, "setpolicy", { minPow: 8, writes: "owner" });74 const next = await query(host, owner, { kinds: [30166], authors: [self] });75 expect(next.length).toBe(1);76 expect(next[0].id).not.toBe(first.id);77 expect(next[0].created_at).toBeGreaterThan(first.created_at);78 expect(vals(next[0], "R")).toContain("pow");79 expect(vals(next[0], "R")).toContain("writes");80 expect(vals(next[0], "T")).toEqual(["PublicOutbox", "SearchRelays"]);81 expect(JSON.parse(next[0].content).limitation.min_pow_difficulty).toBe(8);82 });83});