clone

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

dependabot/npm_and_yarn/npm-f01a459b0f
bindws / test / object / identity.test.ts
identity.test.ts 10.1K
1// The relay's identity under NIP-43: its profile, the role definitions, the2// roster with roles, join and leave requests, and the promise that the roster3// and the NIP-29 group never disagree, whichever path changed the members.4import { SELF } from "cloudflare:test";5import { describe, it, expect } from "vitest";6import { generateSecretKey, getPublicKey } from "nostr-tools/pure";7import { ev, rpc, pk, tagsOf, info } from "../helpers/relay.ts";8import { WS } from "../helpers/ws.ts";910describe("relay identity and NIP-43 roster", () => {11 it("signs a roster after claiming and deltas on membership changes, advertised as self", async () => {12 const host = "roster.bind.ws";13 const owner = generateSecretKey();14 const bob = generateSecretKey();15 let info: any = await (await SELF.fetch(`http://${host}/`, { headers: { accept: "application/nostr+json" } })).json();16 expect(info.self).toBeUndefined();17 await rpc(host, owner, "claim");18 info = await (await SELF.fetch(`http://${host}/`, { headers: { accept: "application/nostr+json" } })).json();19 expect(info.self).toMatch(/^[0-9a-f]{64}$/);20 expect(info.supported_nips).toContain(43);21 const c = await WS.connect(host);22 c.send("REQ", "live", { kinds: [8000, 8001] });23 await c.expect("EOSE");24 let r = await c.req({ kinds: [13534] });25 expect(r.length).toBe(1);26 expect(r[0].pubkey).toBe(info.self);27 expect(r[0].tags).toEqual([["-"], ["member", getPublicKey(owner), "owner"]]);2829 await rpc(host, owner, "allowpubkey", getPublicKey(bob), "friend");30 const added = await c.expect("EVENT");31 expect(added[2].kind).toBe(8000);32 expect(added[2].tags).toContainEqual(["p", getPublicKey(bob)]);33 r = await c.req({ kinds: [13534] });34 expect(r[0].tags).toContainEqual(["member", getPublicKey(bob)]);35 await rpc(host, owner, "unrulepubkey", getPublicKey(bob));36 expect((await c.expect("EVENT"))[2].kind).toBe(8001);37 // Nobody can forge relay-signed kinds: a client-signed 13534 is just another replaceable event by another author.38 const forged = await c.ok(ev(bob, 13534, "", [["member", getPublicKey(bob), "owner"]]));39 expect(forged.ok).toBe(true);40 r = await c.req({ kinds: [13534], authors: [info.self] });41 expect(r.map((x) => [x.pubkey, x.tags]), JSON.stringify(r)).toEqual([[info.self, [["-"], ["member", getPublicKey(owner), "owner"]]]]);42 });43});4445describe("relay profile and roles", () => {46 it("signs a kind 0 for the self key, role definitions, and roles in the roster", async () => {47 const host = "profile.bind.ws";48 const owner = generateSecretKey();49 await rpc(host, owner, "claim");50 const self = (await info(host)).self as string;51 const c = await WS.connect(host);5253 let profile = await c.req({ kinds: [0], authors: [self] });54 expect(profile.length).toBe(1);55 expect(JSON.parse(profile[0].content)).toEqual({ name: "profile", about: "" });56 await rpc(host, owner, "changerelayname", "Pizza");57 await rpc(host, owner, "setpolicy", { description: "pies", icon: "https://img.example/pizza.png" });58 profile = await c.req({ kinds: [0], authors: [self] });59 expect(profile.length).toBe(1);60 expect(JSON.parse(profile[0].content)).toEqual({ name: "Pizza", about: "pies", picture: "https://img.example/pizza.png" });6162 const roles = await c.req({ kinds: [33534], authors: [self] });63 expect(roles.map((e) => tagsOf(e, "d")[0][1]).sort()).toEqual(["moderator", "owner"]);64 for (const r of roles) {65 expect(tagsOf(r, "-").length).toBe(1);66 expect(tagsOf(r, "label")[0][1]).toBe(tagsOf(r, "d")[0][1]);67 expect(tagsOf(r, "description")[0][1]).not.toBe("");68 }6970 const alice = generateSecretKey();71 await rpc(host, owner, "setmember", pk(alice), {});72 let roster = (await c.req({ kinds: [13534], authors: [self] }))[0];73 expect(roster.tags).toContainEqual(["member", pk(owner), "owner"]);74 expect(roster.tags).toContainEqual(["member", pk(alice)]);7576 // A role change must show in the roster and the admins list alike.77 await rpc(host, owner, "setmember", pk(alice), { role: "moderator" });78 roster = (await c.req({ kinds: [13534], authors: [self] }))[0];79 expect(roster.tags).toContainEqual(["member", pk(alice), "moderator"]);80 const admins = (await c.req({ kinds: [39001], authors: [self] }))[0];81 expect(admins.tags).toContainEqual(["p", pk(alice), "moderator"]);82 });83});8485describe("roster and group agreement", () => {86 it("keeps 13534, 39001 and 39002 in step with the member table through every path", async () => {87 const host = "instep.bind.ws";88 const owner = generateSecretKey();89 const alice = generateSecretKey();90 const bob = generateSecretKey();91 const carol = generateSecretKey();92 const dave = generateSecretKey();93 const erin = generateSecretKey();94 const h = ["h", "instep"];95 await rpc(host, owner, "claim");96 const self = (await info(host)).self as string;97 const c = await WS.connect(host);9899 // expected: pubkey -> role, maintained by the test as it walks the paths100 const want = new Map<string, string>([[pk(owner), "owner"]]);101 let step = 0;102 async function check() {103 step++;104 const [roster] = await c.req({ kinds: [13534], authors: [self] });105 const [admins] = await c.req({ kinds: [39001], authors: [self] });106 const [members] = await c.req({ kinds: [39002], authors: [self] });107 const fromRoster = new Map(tagsOf(roster, "member").map((t) => [t[1], t[2] ?? "member"]));108 expect(fromRoster, `roster at step ${step}`).toEqual(want);109 expect(new Set(tagsOf(members, "p").map((t) => t[1])), `members at step ${step}`).toEqual(new Set(want.keys()));110 const wantAdmins = new Map([...want].filter(([, r]) => r !== "member"));111 expect(new Map(tagsOf(admins, "p").map((t) => [t[1], t[2]])), `admins at step ${step}`).toEqual(wantAdmins);112 const listed = (await rpc(host, sk(want), "listallowedpubkeys")).result.map((m: any) => m.pubkey).sort();113 expect(listed, `listmembers at step ${step}`).toEqual([...want.keys()].filter((p) => want.get(p) !== "owner").sort());114 }115 const keys = { [pk(owner)]: owner, [pk(alice)]: alice, [pk(bob)]: bob, [pk(carol)]: carol, [pk(dave)]: dave, [pk(erin)]: erin };116 const sk = (m: Map<string, string>) => keys[[...m].find(([, r]) => r === "owner")![0]];117 await check();118119 await rpc(host, owner, "setmember", pk(alice), {});120 want.set(pk(alice), "member");121 await check();122123 expect((await c.ok(ev(bob, 9021, "", [h]))).ok).toBe(true);124 want.set(pk(bob), "member");125 await check();126127 await rpc(host, owner, "setmember", pk(alice), { role: "moderator" });128 want.set(pk(alice), "moderator");129 await check();130131 expect((await c.ok(ev(bob, 9022, "", [h]))).ok).toBe(true);132 want.delete(pk(bob));133 await check();134135 await rpc(host, owner, "setmember", pk(carol), {});136 await rpc(host, owner, "banpubkey", pk(carol), "spam");137 await check();138139 await rpc(host, owner, "setmember", pk(dave), {});140 await rpc(host, owner, "removemember", pk(dave));141 await check();142143 expect((await c.ok(ev(owner, 9000, "", [h, ["p", pk(dave), "moderator"]]))).ok).toBe(true);144 want.set(pk(dave), "moderator");145 await check();146 expect((await c.ok(ev(owner, 9001, "", [h, ["p", pk(dave)]]))).ok).toBe(true);147 want.delete(pk(dave));148 await check();149150 expect((await rpc(host, owner, "transferowner", pk(alice))).status).toBe(200);151 want.set(pk(alice), "owner");152 want.set(pk(owner), "moderator");153 await check();154155 const cfg = (await rpc(host, alice, "exportconfig")).result;156 cfg.members = [{ pubkey: pk(erin), name: null, note: "" }];157 expect((await rpc(host, alice, "importconfig", cfg)).status).toBe(200);158 want.delete(pk(owner));159 want.set(pk(erin), "member");160 await check();161 // The import announced who came and who went.162 expect((await c.req({ kinds: [8000], authors: [self], "#p": [pk(erin)] })).length).toBe(1);163 expect((await c.req({ kinds: [8001], authors: [self], "#p": [pk(owner)] })).length).toBe(1);164 expect((await c.req({ kinds: [9001], authors: [self], "#p": [pk(owner)] })).length).toBe(1);165 });166});167168describe("NIP-43 join and leave requests", () => {169 it("admits with a claim, refuses bad codes, and revokes on request", async () => {170 const host = "fortythree.bind.ws";171 const owner = generateSecretKey();172 await rpc(host, owner, "claim");173 await rpc(host, owner, "setpolicy", { writes: "allowlist" });174 const code = (await rpc(host, owner, "createinvite", 3600, 0, "friends")).result.code as string;175 const sam = generateSecretKey();176 const c = await WS.connect(host);177178 // NIP-70: a "-" tagged request needs the socket authenticated.179 expect((await c.ok(ev(sam, 28934, "", [["-"], ["claim", code]]))).msg).toMatch(/^auth-required/);180 await c.auth(sam, host);181 let r = await c.ok(ev(sam, 28934, "", [["-"], ["claim", "nope"]]));182 expect(r.ok).toBe(false);183 expect(r.msg).toBe("restricted: that is an invalid invite code.");184 r = await c.ok(ev(sam, 28934, "", [["-"]]));185 expect(r.ok).toBe(false);186 expect(r.msg).toMatch(/^restricted: a join request needs a claim/);187 r = await c.ok(ev(sam, 28934, "", [["-"], ["claim", code]]));188 expect(r.ok).toBe(true);189 expect(r.msg).toBe("info: welcome to fortythree!");190 expect((await rpc(host, owner, "listallowedpubkeys")).result.map((m: any) => m.pubkey)).toContain(pk(sam));191 r = await c.ok(ev(sam, 28934, "", [["-"], ["claim", code]]));192 expect(r.ok).toBe(true);193 expect(r.msg).toMatch(/^duplicate: you are already a member/);194 // Requests are ephemeral: nothing was stored.195 expect((await c.req({ kinds: [28934] })).length).toBe(0);196197 r = await c.ok(ev(sam, 28936, "", [["-"]]));198 expect(r.ok).toBe(true);199 expect(r.msg).toBe("info: access revoked.");200 expect((await rpc(host, owner, "listallowedpubkeys")).result.map((m: any) => m.pubkey)).not.toContain(pk(sam));201 r = await c.ok(ev(sam, 28936, "", [["-"]]));202 expect(r.ok).toBe(true);203 expect(r.msg).toMatch(/^duplicate: you are not a member/);204 // The owner cannot leave.205 const o = await WS.connect(host);206 await o.auth(owner, host);207 expect((await o.ok(ev(owner, 28936, "", [["-"]]))).msg).toMatch(/^restricted: the owner cannot leave/);208 });209});