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 / push-privacy.test.ts
push-privacy.test.ts 7.1K
1import { env, runInDurableObject } from "cloudflare:test";2import { describe, expect, it } from "vitest";3import { generateSecretKey } from "nostr-tools/pure";4import type { Relay } from "../../src/relay.ts";5import { KIND_PUSH_REGISTRATION } from "../../src/kinds.ts";6import { now, ev, pk, post, rpc } from "../helpers/relay.ts";7import { WS } from "../helpers/ws.ts";89describe("push registration privacy", () => {10 it("keeps registrations author-only in queries, counts, filters and sync", async () => {11 const owner = generateSecretKey();12 const other = generateSecretKey();13 const recipient = generateSecretKey();14 const host = "push-private.bind.ws";15 await rpc(host, owner, "claim");16 const mine = ev(owner, KIND_PUSH_REGISTRATION, "mine", [["d", "mine"], ["p", pk(recipient)]], now() - 10);17 const theirs = ev(other, KIND_PUSH_REGISTRATION, "theirs", [["d", "theirs"], ["p", pk(recipient)]], now() - 5);18 const note = ev(owner, 1, "public", [], now());1920 await runInDurableObject(env.RELAY.getByName("push-private"), async (r: Relay) => {21 const before = r.store.stats();22 expect(r.store.save(mine, now())).toBe("");23 expect(r.store.save(theirs, now())).toBe("");24 expect(r.store.save(note, now())).toBe("");2526 const filter = { kinds: [KIND_PUSH_REGISTRATION], tags: {} };27 const hidden = r.store.query(filter, { pubkeys: [], all: true }, 20, now());28 expect(hidden.rows).toEqual([]);29 expect(r.store.query(filter, { pubkeys: [pk(recipient)], all: true }, 20, now()).rows).toEqual([]);30 expect(r.store.query(filter, { pubkeys: [pk(owner)], all: true }, 20, now()).rows.map((x) => JSON.parse(x).id)).toEqual([mine.id]);31 expect(r.store.query({ ids: [theirs.id], tags: {} }, { pubkeys: [pk(owner)], all: true }, 20, now()).rows).toEqual([]);32 expect(r.store.count([filter], { pubkeys: [], all: true }, now())).toBe(0);33 expect(r.store.count([filter], { pubkeys: [pk(owner)], all: true }, now())).toBe(1);34 expect(r.store.countHLL(filter, { pubkeys: [], all: true }, 0, now()).count).toBe(0);35 expect(r.store.syncItems(filter, { pubkeys: [], all: true }, 20, now())).toEqual([]);36 expect(r.store.syncItems(filter, { pubkeys: [pk(owner)], all: true }, 20, now())).toEqual([{ timestamp: mine.created_at, id: expect.any(Uint8Array) }]);3738 // Internal jobs use all:true and sequence reads. Registrations stay out.39 expect(r.store.after(0, filter, 20, now()).map((x) => JSON.parse(x.raw).id)).toEqual([]);40 expect(r.store.recent(20, now()).map((x) => JSON.parse(x).id)).not.toContain(mine.id);41 expect(r.store.recent(20, now()).map((x) => JSON.parse(x).id)).not.toContain(theirs.id);42 expect(r.store.dumpPage(0, 20).map((x) => JSON.parse(x.raw).id)).not.toContain(mine.id);43 expect(r.store.dumpPage(0, 20).map((x) => JSON.parse(x.raw).id)).not.toContain(theirs.id);4445 expect(r.store.stats().events).toBe(before.events + 1);46 expect(r.store.kinds(0).map((x) => x.kind)).not.toContain(KIND_PUSH_REGISTRATION);47 expect(r.store.kindCounts(0).map((x) => x.kind)).not.toContain(KIND_PUSH_REGISTRATION);48 expect(r.store.kindStats().map((x) => x.kind)).not.toContain(KIND_PUSH_REGISTRATION);49 });50 });5152 it("keeps stored registrations private through the HTTP count and query bridge", async () => {53 const owner = generateSecretKey();54 const other = generateSecretKey();55 const recipient = generateSecretKey();56 const bystander = generateSecretKey();57 const host = "push-bridge-private.bind.ws";58 await rpc(host, owner, "claim");59 const mine = ev(owner, KIND_PUSH_REGISTRATION, "mine", [["d", "mine"], ["p", pk(recipient)]], now() - 10);60 const theirs = ev(other, KIND_PUSH_REGISTRATION, "theirs", [["d", "theirs"], ["p", pk(recipient)]], now() - 5);61 await runInDurableObject(env.RELAY.getByName("push-bridge-private"), async (r: Relay) => {62 // This bypasses feature and write gates so the read boundary is tested63 // even when push is switched off.64 expect(r.store.save(mine, now())).toBe("");65 expect(r.store.save(theirs, now())).toBe("");66 expect(r.store.count([{ kinds: [KIND_PUSH_REGISTRATION], tags: {} }], { pubkeys: [pk(other)] }, now())).toBe(1);67 expect(r.store.query({ kinds: [KIND_PUSH_REGISTRATION], tags: {} }, { pubkeys: [pk(other)] }, 20, now()).rows.map((x) => JSON.parse(x).id)).toEqual([theirs.id]);68 });6970 for (const [key, count] of [[owner, 1], [other, 1], [recipient, 0], [bystander, 0]] as const) {71 const result = await post(host, key, "/count", [{ kinds: [KIND_PUSH_REGISTRATION] }]);72 expect(result.status, JSON.stringify(result)).toBe(200);73 expect(result.body.count, JSON.stringify({ key: pk(key), owner: pk(owner), other: pk(other), recipient: pk(recipient) })).toBe(count);74 const query = await post(host, key, "/query", [{ kinds: [KIND_PUSH_REGISTRATION] }]);75 expect(query.status, JSON.stringify(query)).toBe(200);76 expect(query.body.map((e: { id: string }) => e.id)).toEqual(key === owner ? [mine.id] : key === other ? [theirs.id] : []);77 }78 });7980 it("keeps stored registrations private for authenticated websocket reads and live delivery", async () => {81 const owner = generateSecretKey();82 const other = generateSecretKey();83 const recipient = generateSecretKey();84 const bystander = generateSecretKey();85 const host = "push-socket-private.bind.ws";86 await rpc(host, owner, "claim");87 const mine = ev(owner, KIND_PUSH_REGISTRATION, "mine", [["d", "mine"], ["p", pk(recipient)]], now() - 10);88 const theirs = ev(other, KIND_PUSH_REGISTRATION, "theirs", [["d", "theirs"], ["p", pk(recipient)]], now() - 5);89 await runInDurableObject(env.RELAY.getByName("push-socket-private"), async (r: Relay) => {90 expect(r.store.save(mine, now())).toBe("");91 expect(r.store.save(theirs, now())).toBe("");92 });9394 const ownerSocket = await WS.connect(host);95 const otherSocket = await WS.connect(host);96 const recipientSocket = await WS.connect(host);97 const bystanderSocket = await WS.connect(host);98 await ownerSocket.auth(owner, host);99 await otherSocket.auth(other, host);100 await recipientSocket.auth(recipient, host);101 await bystanderSocket.auth(bystander, host);102 expect((await ownerSocket.open("mine", { kinds: [KIND_PUSH_REGISTRATION] })).events.map((e) => e.id)).toEqual([mine.id]);103 expect((await otherSocket.open("other", { kinds: [KIND_PUSH_REGISTRATION] })).events.map((e) => e.id)).toEqual([theirs.id]);104 expect((await recipientSocket.open("recipient", { kinds: [KIND_PUSH_REGISTRATION] })).events).toEqual([]);105 expect((await bystanderSocket.open("bystander", { kinds: [KIND_PUSH_REGISTRATION] })).events).toEqual([]);106107 const live = ev(owner, KIND_PUSH_REGISTRATION, "live", [["d", "live"]]);108 await runInDurableObject(env.RELAY.getByName("push-socket-private"), async (r: Relay) => {109 expect(r.store.save(live, now())).toBe("");110 r.broadcast(live);111 });112 expect((await ownerSocket.recvOr(200))?.[2]?.id).toBe(live.id);113 expect(await otherSocket.recvOr(200)).toBeNull();114 expect(await recipientSocket.recvOr(200)).toBeNull();115 expect(await bystanderSocket.recvOr(200)).toBeNull();116 });117});