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 / bridge.test.ts
bridge.test.ts 8.4K
1// Doors for a client without a socket: the HTTP bridge, and NIP-46 as2// transport, where kind 24133 passes the ownership and write gates and a3// subscription to it alone passes the read gate.4import { SELF } from "cloudflare:test";5import { describe, it, expect } from "vitest";6import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from "nostr-tools/pure";7import { getToken } from "nostr-tools/nip98";8import { ev, rpc, post } from "../helpers/relay.ts";9import { WS } from "../helpers/ws.ts";1011describe("HTTP bridge", () => {12 it("accepts events, answers queries and counts with NIP-98, applying the same gates", async () => {13 const host = "bridge.bind.ws";14 const owner = generateSecretKey();15 const other = generateSecretKey();16 await rpc(host, owner, "claim");17 const e = ev(owner, 1, "over http");18 let r = await post(host, owner, "/events", e);19 expect(r.status).toBe(200);20 expect(r.body).toEqual({ event_id: e.id, accepted: true, message: "" });21 r = await post(host, owner, "/events", e);22 expect(r.body.accepted).toBe(true);23 expect(r.body.message).toMatch(/^duplicate:/);2425 r = await post(host, other, "/query", [{ kinds: [1], authors: [getPublicKey(owner)] }]);26 expect(r.status).toBe(200);27 expect(r.body.map((x: Event) => x.id)).toEqual([e.id]);28 r = await post(host, other, "/count", [{ kinds: [1] }]);29 expect(r.body.count).toBe(1);3031 // Unsigned, wrong-URL, and non-JSON requests are refused cleanly.32 expect((await post(host, null, "/query", [{}])).status).toBe(401);33 const badUrlToken = await getToken("http://elsewhere.bind.ws/query", "POST", (x) => finalizeEvent(x, other), true, [{}] as any);34 const bad = await SELF.fetch(`http://${host}/query`, { method: "POST", headers: { authorization: badUrlToken }, body: "[{}]" });35 expect(bad.status).toBe(401);3637 // A subscriber on the socket sees bridge writes live.38 const c = await WS.connect(host);39 await c.open("live", { kinds: [1] });40 const e2 = ev(owner, 1, "pushed");41 await post(host, owner, "/events", e2);42 expect((await c.expect("EVENT"))[2].id).toBe(e2.id);4344 // Private kinds via the bridge follow the recipient rule; the signer counts as authenticated.45 const recipient = generateSecretKey();46 const wrap = ev(generateSecretKey(), 1059, "dm", [["p", getPublicKey(recipient)]]);47 await post(host, owner, "/events", wrap);48 r = await post(host, other, "/query", [{ kinds: [1059] }]);49 expect(r.status).toBe(200);50 expect(r.body).toEqual([]); // authenticated but not a party: silently filtered51 expect((await post(host, null, "/query", [{ kinds: [1059] }])).status).toBe(401);52 r = await post(host, recipient, "/query", [{ kinds: [1059] }]);53 expect(r.body.map((x: Event) => x.id)).toEqual([wrap.id]);54 });55});5657describe("NIP-46 transport", () => {58 it("carries kind 24133 on an unclaimed relay, live only, never stored", async () => {59 const host = "phone.bind.ws";60 const signerKey = generateSecretKey();61 const client = generateSecretKey();62 // A signer that names its key in the filter and never AUTHs, the way63 // Amethyst's bunker listens under a transport key.64 const listener = await WS.connect(host);65 expect((await listener.open("nc", { kinds: [24133], "#p": [getPublicKey(signerKey)] })).closed).toBe("");66 // A bystander subscribed to the kind alone, naming no key.67 const bystander = await WS.connect(host);68 expect((await bystander.open("nc", { kinds: [24133] })).closed).toBe("");6970 const sender = await WS.connect(host);71 const req = ev(client, 24133, "ciphertext", [["p", getPublicKey(signerKey)]]);72 expect(await sender.ok(req)).toEqual({ ok: true, msg: "" });73 const got = await listener.expect("EVENT");74 expect(got[1]).toBe("nc");75 expect(got[2].id).toBe(req.id);76 // The reply, addressed to the client, reaches a socket that AUTHed as the77 // client, and one that asks by the signer's key as author.78 const clientSock = await WS.connect(host);79 expect((await clientSock.open("nc", { kinds: [24133] })).closed).toBe("");80 await clientSock.auth(client, host);81 const byAuthor = await WS.connect(host);82 expect((await byAuthor.open("nc", { kinds: [24133], authors: [getPublicKey(signerKey)] })).closed).toBe("");83 const reply = ev(signerKey, 24133, "ciphertext", [["p", getPublicKey(client)]]);84 expect(await listener.ok(reply)).toEqual({ ok: true, msg: "" });85 expect((await clientSock.expect("EVENT"))[2].id).toBe(reply.id);86 expect((await byAuthor.expect("EVENT"))[2].id).toBe(reply.id);87 // The bystander saw neither; a later frame proves the queue is empty.88 expect((await bystander.open("probe", { kinds: [24133] })).closed).toBe("");89 // Anything else is still refused while unclaimed, and the request left no trace.90 expect((await sender.ok(ev(client, 1, "hello"))).msg).toMatch(/unclaimed/);91 const later = await WS.connect(host);92 expect((await later.open("q", { kinds: [24133] })).closed).toBe("");93 const info: any = await (await SELF.fetch(`http://${host}/`, { headers: { accept: "application/nostr+json" } })).json();94 expect(info.pubkey).toBeUndefined();95 });9697 it("passes the write policy but not bans", async () => {98 const host = "locked.bind.ws";99 const owner = generateSecretKey();100 await rpc(host, owner, "claim");101 await rpc(host, owner, "setpolicy", { writes: "owner" });102 const stranger = generateSecretKey();103 const c = await WS.connect(host);104 expect((await c.ok(ev(stranger, 1, "no"))).msg).toMatch(/only the relay owner/);105 expect((await c.ok(ev(stranger, 24133, "x", [["p", getPublicKey(owner)]]))).ok).toBe(true);106 await rpc(host, owner, "banpubkey", getPublicKey(stranger), "spam");107 const d = await WS.connect(host);108 expect((await d.ok(ev(stranger, 24133, "x", [["p", getPublicKey(owner)]]))).msg).toMatch(/banned/);109 expect((await rpc(host, owner, "stats")).result.kinds.map((k: { kind: number }) => k.kind)).not.toContain(24133);110 });111112 it("serves a subscription to 24133 alone under members-only reads, and nothing wider", async () => {113 const host = "private.bind.ws";114 const owner = generateSecretKey();115 await rpc(host, owner, "claim");116 await rpc(host, owner, "setpolicy", { reads: "members" });117 const c = await WS.connect(host);118 expect((await c.open("a", { kinds: [24133] })).closed).toBe("");119 expect((await c.open("b", { kinds: [24133], "#p": ["ab".repeat(32)] }, { kinds: [24133] })).closed).toBe("");120 expect((await c.open("c", { kinds: [24133, 1] })).closed).toMatch(/^auth-required/);121 expect((await c.open("d", { kinds: [24133] }, { kinds: [1] })).closed).toMatch(/^auth-required/);122 expect((await c.open("e", {})).closed).toMatch(/^auth-required/);123 // NIP-11 says so, and advertises the transport.124 const info: any = await (await SELF.fetch(`http://${host}/`, { headers: { accept: "application/nostr+json" } })).json();125 expect(info.limitation.auth_required).toBe(true);126 expect(info.supported_nips).toContain(46);127 // Amber's probe before it lists a relay: a throwaway key, no AUTH, a128 // subscription naming the key, and its own event echoed back.129 const probe = generateSecretKey();130 expect((await c.open("nc", { kinds: [24133], "#p": [getPublicKey(probe)] })).closed).toBe("");131 const self = ev(probe, 24133, "Test bunker event", [["p", getPublicKey(probe)]]);132 expect(await c.ok(self)).toEqual({ ok: true, msg: "" });133 expect((await c.expect("EVENT"))[2].id).toBe(self.id);134 // A signer's reply reaches the client that asked for it by key, unauthed.135 const client = generateSecretKey();136 const d = await WS.connect(host);137 expect((await d.open("nc", { kinds: [24133], "#p": [getPublicKey(client)] })).closed).toBe("");138 const reply = ev(owner, 24133, "ciphertext", [["p", getPublicKey(client)]]);139 expect(await c.ok(reply)).toEqual({ ok: true, msg: "" });140 expect((await d.expect("EVENT"))[2].id).toBe(reply.id);141 });142143 it("serves the signer library with a long cache", async () => {144 const resp = await SELF.fetch("http://any.bind.ws/signer.js?v=abc");145 expect(resp.status).toBe(200);146 expect(resp.headers.get("content-type")).toMatch(/javascript/);147 expect(resp.headers.get("cache-control")).toMatch(/max-age=604800/);148 const js = await resp.text();149 expect(js).toContain("NostrSigner");150 const page = await (await SELF.fetch("http://any.bind.ws/")).text();151 expect(page).toMatch(/window\.SIGNER_URL = "\/signer\.js\?v=[0-9a-f]{12}"/);152 });153});