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 / fork.test.ts
fork.test.ts 5.6K
1// Fork a name: lease a new name reserved for a key, pull this relay into2// it, hand over the claim. Built from lease and jobs.3import { env, runInDurableObject } from "cloudflare:test";4import { describe, it, expect } from "vitest";5import { generateSecretKey, getPublicKey, type Event } from "nostr-tools/pure";6import type { Relay } from "../../src/relay.ts";7import { ADJECTIVES, ANIMALS } from "../../src/names.ts";8import { now, ev, rpc, info, post } from "../helpers/relay.ts";910// settle drives the new object's alarm until its pull has finished.11async function settle(name: string) {12 const stub = env.RELAY.getByName(name);13 for (let i = 0; i < 40; i++) {14 const done = await runInDurableObject(stub, async (r: Relay) => {15 await r.alarm();16 const jobs = await r.jobs();17 return jobs.length > 0 && jobs.every((j) => !j.running && j.nextRun === 0);18 });19 if (done) return;20 await new Promise((r) => setTimeout(r, 25));21 }22 throw new Error("the fork did not finish pulling");23}2425async function seed(host: string) {26 const owner = generateSecretKey();27 const friend = generateSecretKey();28 const member = generateSecretKey();29 await rpc(host, owner, "claim");30 await rpc(host, owner, "setmember", getPublicKey(member), { name: "bob", note: "a friend" });31 const e1 = ev(owner, 1, "mine", [], now() - 30);32 const e2 = ev(friend, 1, "theirs", [], now() - 20);33 const e3 = ev(owner, 30023, "an article", [["d", "a"]], now() - 10);34 for (const e of [e1, e2, e3]) expect((await post(host, e.pubkey === getPublicKey(owner) ? owner : friend, "/events", e)).body.accepted).toBe(true);35 return { owner, friend, member, e1, e2, e3 };36}3738describe("fork a name", () => {39 it("leases a chosen name for the caller, pulls everything and the people, and a claim converts it", async () => {40 const host = "origin.bind.ws";41 const { owner, member, e1, e2, e3 } = await seed(host);42 const r = await rpc(host, owner, "forkrelay", { name: "offshoot", people: true });43 expect(r.status, JSON.stringify(r)).toBe(200);44 expect(r.result.name).toBe("offshoot");45 expect(r.result.url).toBe("wss://offshoot.bind.ws");46 expect(r.result.holder).toBe(getPublicKey(owner));47 expect(r.result.expires_at).toBeGreaterThan(now() + 13 * 86400);48 expect(r.result.handover).toMatch(/offshoot\.bind\.ws/);4950 const doc = await info("offshoot.bind.ws");51 expect(doc.lease.holder).toBe(getPublicKey(owner));52 expect(doc.description).toMatch(/^Forked from origin\.bind\.ws/);53 await settle("offshoot");54 const q = await post("offshoot.bind.ws", owner, "/query", [{ kinds: [1, 30023] }]);55 expect(q.body.map((e: Event) => e.id).sort()).toEqual([e1.id, e2.id, e3.id].sort());5657 // Reserved: a stranger cannot claim it; the holder can, and the people came along.58 expect((await rpc("offshoot.bind.ws", generateSecretKey(), "claim")).status).toBe(403);59 expect((await rpc("offshoot.bind.ws", owner, "claim")).result.converted).toBe(true);60 const people = (await rpc("offshoot.bind.ws", owner, "listmembers")).result.members.filter((m: any) => m.role !== "owner");61 expect(people.map((m: any) => m.pubkey)).toEqual([getPublicKey(member)]);62 expect((await info("offshoot.bind.ws")).pubkey).toBe(getPublicKey(owner));63 });6465 it("picks a memorable name, hands it to another key, and respects the filter", async () => {66 const host = "origin2.bind.ws";67 const { owner, e1, e3, member } = await seed(host);68 const heir = generateSecretKey();69 const r = await rpc(host, owner, "forkrelay", { holder: getPublicKey(heir), filter: { authors: [getPublicKey(owner)] } });70 expect(r.status, JSON.stringify(r)).toBe(200);71 const [adj, animal] = r.result.name.split("-");72 if (animal) expect(ADJECTIVES.includes(adj) && ANIMALS.includes(animal)).toBe(true);73 else expect(ANIMALS.includes(r.result.name.slice(0, -2))).toBe(true);74 expect(r.result.holder).toBe(getPublicKey(heir));75 const h = r.result.name + ".bind.ws";76 await settle(r.result.name);77 const q = await post(h, heir, "/query", [{ kinds: [1, 30023] }]);78 expect(q.body.map((e: Event) => e.id).sort()).toEqual([e1.id, e3.id].sort());79 // No people asked for, none copied.80 expect((await rpc(h, owner, "claim")).status).toBe(403);81 expect((await rpc(h, heir, "claim")).result.claimed).toBe(true);82 const lm = await rpc(h, heir, "listmembers"); expect(lm.status, JSON.stringify(lm)).toBe(200); expect(lm.result.members.map((m: any) => m.pubkey)).not.toContain(getPublicKey(member));83 });8485 it("refuses a taken name, a bad name, a second fork within the hour, and anyone but the owner", async () => {86 const host = "origin3.bind.ws";87 const { owner, member } = await seed(host);88 const taken = generateSecretKey();89 await rpc("taken.bind.ws", taken, "claim");90 let r = await rpc(host, owner, "forkrelay", { name: "taken" });91 expect(r.status).toBe(409);92 expect(r.error).toMatch(/taken/);93 r = await rpc(host, owner, "forkrelay", { name: "no way" });94 expect(r.status).toBe(400);95 r = await rpc(host, owner, "forkrelay", { name: "origin3" });96 expect(r.status).toBe(400);97 // Refusals do not spend the hourly fork.98 r = await rpc(host, owner, "forkrelay", { name: "twig" });99 expect(r.status, JSON.stringify(r)).toBe(200);100 r = await rpc(host, owner, "forkrelay", { name: "twig2" });101 expect(r.status).toBe(409);102 expect(r.error).toMatch(/one fork an hour/);103 expect((await info("twig2.bind.ws")).lease).toBeUndefined();104 await rpc(host, owner, "setmember", getPublicKey(member), { role: "moderator" });105 expect((await rpc(host, member, "forkrelay", { name: "twig3" })).status).toBe(403);106 });107});