clone

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

dependabot/github_actions/actions-9b61906d8b
bindws / test / object / succession.test.ts
succession.test.ts 7.5K
1// Succession: an heir and a dead-man's switch. The owner's presence is2// recorded on signed actions; silence past the delay starts a month of3// warnings; silence through the month hands the relay to the heir.4import { env, runInDurableObject } from "cloudflare:test";5import { describe, it, expect } from "vitest";6import { generateSecretKey } from "nostr-tools/pure";7import { unwrapEvent } from "nostr-tools/nip59";8import type { Relay } from "../../src/relay.ts";9import { now, pk, rpc, info, alarm } from "../helpers/relay.ts";10import { WS } from "../helpers/ws.ts";1112const DAY = 86400;1314// inbox reads the wraps addressed to a key and opens them with it.15async function inbox(host: string, sk: Uint8Array): Promise<string[]> {16 const c = await WS.connect(host);17 await c.auth(sk, host);18 const wraps = await c.req({ kinds: [1059], "#p": [pk(sk)] });19 return wraps.map((w) => unwrapEvent(w, sk).content);20}2122// silence backdates the owner's last sign-in and holds the hourly presence23// write so the test's own owner calls do not refresh it.24async function silence(name: string, days: number) {25 await runInDurableObject(env.RELAY.getByName(name), async (r: Relay, state) => {26 await state.storage.put("ownerSeenAt", now() - days * DAY);27 r.succession.seenWrite = Date.now();28 });29}3031describe("succession", () => {32 it("names an heir who must be a member, with an allowed delay", async () => {33 const host = "heirs.bind.ws";34 const owner = generateSecretKey();35 const heir = generateSecretKey();36 await rpc(host, owner, "claim");37 let r = await rpc(host, owner, "setsuccession", { heir: pk(heir), afterDays: 90 });38 expect(r.status).toBe(400);39 expect(r.error).toMatch(/member first/);40 await rpc(host, owner, "setmember", pk(heir), {});41 r = await rpc(host, owner, "setsuccession", { heir: pk(heir), afterDays: 10 });42 expect(r.error).toMatch(/afterDays/);43 r = await rpc(host, owner, "setsuccession", { heir: pk(heir), afterDays: 90 });44 expect(r.status).toBe(200);45 expect(r.result.succession).toEqual({ heir: pk(heir), afterDays: 90 });46 expect(r.result.warning).toBeNull();47 expect(r.result.handoverAt).toBeGreaterThan(now() + 119 * DAY);48 expect((await rpc(host, owner, "getpolicy")).result.notify.succession).toBe(true);49 // The heir may read the status; a moderator may not; the plan stays out of exports.50 const mod = generateSecretKey();51 await rpc(host, owner, "setmember", pk(mod), { role: "moderator" });52 expect((await rpc(host, heir, "successionstatus")).result.succession.heir).toBe(pk(heir));53 expect((await rpc(host, mod, "successionstatus")).status).toBe(403);54 expect((await rpc(host, owner, "exportconfig")).result.policy.succession).toBeUndefined();55 expect((await rpc(host, owner, "clearsuccession")).result).toBe(true);56 expect((await rpc(host, owner, "getpolicy")).result.succession).toBeNull();57 });5859 it("records the owner's presence on signed actions, not a member's", async () => {60 const host = "seen.bind.ws";61 const owner = generateSecretKey();62 const member = generateSecretKey();63 await rpc(host, owner, "claim");64 await rpc(host, owner, "setmember", pk(member), {});65 const seenAt = () => runInDurableObject(env.RELAY.getByName("seen"), async (_r: Relay, state) => (await state.storage.get<number>("ownerSeenAt")) ?? 0);66 expect(await seenAt()).toBeGreaterThan(now() - 5);67 const backdate = () => runInDurableObject(env.RELAY.getByName("seen"), async (r: Relay, state) => {68 await state.storage.put("ownerSeenAt", now() - 5 * DAY);69 r.succession.seenWrite = 0;70 });71 await backdate();72 const m = await WS.connect(host);73 await m.auth(member, host);74 expect(await seenAt()).toBeLessThan(now() - 4 * DAY);75 const o = await WS.connect(host);76 await o.auth(owner, host);77 expect(await seenAt()).toBeGreaterThan(now() - 5);78 await backdate();79 await rpc(host, owner, "stats");80 expect(await seenAt()).toBeGreaterThan(now() - 5);81 });8283 it("warns after the delay, stops when the owner shows up, and hands over after the month", async () => {84 const host = "estate.bind.ws";85 const name = "estate";86 const owner = generateSecretKey();87 const heir = generateSecretKey();88 await rpc(host, owner, "claim");89 await rpc(host, owner, "setmember", pk(heir), { name: "heir" });90 await rpc(host, owner, "setsuccession", { heir: pk(heir), afterDays: 90 });9192 // Silent for 60 days: nothing happens.93 await silence(name, 60);94 await alarm(name);95 expect((await info(host)).succession_pending).toBeUndefined();96 // Silent for 91 days: the warning month starts, the owner is told.97 await silence(name, 91);98 await alarm(name);99 let doc = await info(host);100 expect(doc.succession_pending).toMatch(/^\d{4}-\d{2}-\d{2}$/);101 let msgs = await inbox(host, owner);102 expect(msgs.length).toBe(1);103 expect(msgs[0]).toMatch(/not signed in .* 91 days/);104 // A second alarm the same week says nothing more.105 await alarm(name);106 expect((await inbox(host, owner)).length).toBe(1);107108 // The owner acts: the warning is called off.109 await runInDurableObject(env.RELAY.getByName(name), async (r: Relay) => {110 r.succession.seenWrite = 0;111 });112 let st = (await rpc(host, owner, "successionstatus")).result;113 expect(st.warning).toBeNull();114 expect(st.silentDays).toBe(0);115 expect((await info(host)).succession_pending).toBeUndefined();116117 // Silent again, through the whole month: the relay changes hands.118 await silence(name, 95);119 await alarm(name);120 expect((await rpc(host, heir, "successionstatus")).result.warning).not.toBeNull();121 await runInDurableObject(env.RELAY.getByName(name), async (r: Relay, state) => {122 const warn = { since: now() - 31 * DAY, lastNotified: now() - 31 * DAY };123 await state.storage.put("succession_warn", warn);124 r.succession.warn = warn;125 });126 await alarm(name);127 doc = await info(host);128 expect(doc.pubkey).toBe(pk(heir));129 expect(doc.succession_pending).toBeUndefined();130 const members = (await rpc(host, heir, "listmembers")).result.members as { pubkey: string; role?: string }[];131 expect(members.find((m) => m.pubkey === pk(owner))?.role).toBe("moderator");132 expect((await rpc(host, heir, "getpolicy")).result.succession).toBeNull();133 st = (await rpc(host, heir, "successionstatus")).result;134 expect(st.log).toEqual([{ at: expect.any(Number), from: pk(owner), to: pk(heir) }]);135 expect(st.ownerSeenAt).toBeGreaterThan(now() - 5);136 msgs = await inbox(host, owner);137 expect(msgs.some((m) => /now belongs to/.test(m))).toBe(true);138 expect((await inbox(host, heir)).some((m) => /is yours now/.test(m))).toBe(true);139 // The old owner is a moderator now: no more than that.140 expect((await rpc(host, owner, "setpolicy", { name: "mine again" })).status).toBe(403);141 });142143 it("drops the plan when the heir is no longer a member", async () => {144 const host = "orphan.bind.ws";145 const owner = generateSecretKey();146 const heir = generateSecretKey();147 await rpc(host, owner, "claim");148 await rpc(host, owner, "setmember", pk(heir), {});149 await rpc(host, owner, "setsuccession", { heir: pk(heir), afterDays: 90 });150 await rpc(host, owner, "removemember", pk(heir));151 await silence("orphan", 100);152 await alarm("orphan");153 expect((await rpc(host, owner, "getpolicy")).result.succession).toBeNull();154 expect((await inbox(host, owner)).some((m) => /no longer a member/.test(m))).toBe(true);155 expect((await info(host)).pubkey).toBe(pk(owner));156 });157});