- commit
- 9f31ca0021600323745bda45e5c5cf709e092296
- parent
- d8ac77518ee0d4946ed1c304d7656f4a2ac8fb82
- author
- Dami <dami.etoile@gmail.com>
| R | test/ | 4 | +- |
| M | test/ | 13 | ++ |
| M | test/ | 41 | +------ |
| A | test/ | 33 | ++++++ |
| A | test/ | 15 | +++ |
| A | test/ | 47 | ++++++++ |
| A | test/ | 21 | ++++ |
| M | test/ | 70 | +----------- |
| A | test/ | 41 | +++++++ |
| A | test/ | 27 | +++++ |
| A | test/ | 39 | +++++++ |
| A | test/ | 19 | +++ |
| A | test/ | 47 | ++++++++ |
| D | test/ | 178 | ------------------------------ |
@@ -3,9 +3,7 @@
33 // bucket, so a host whose bucket is not R2 (docs/16) is checked here.44 import { describe, it, expect } from "vitest";55 import { sha256 } from "@noble/hashes/sha2.js";6-import { HTTP_URL, newEvent, newKey, now, pub, rand } from "./helpers.ts";7-8-const hex = (b: Uint8Array) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");6+import { newEvent, newKey, pub, now, rand, hex, HTTP_URL } from "./helpers.ts";97 108 // A Blossom token: kind 24242 with the verb and the hash, base64 in the header.119 function token(sk: Uint8Array, verb: "upload" | "get" | "list" | "delete", sha?: string): string {@@ -1,6 +1,7 @@
11 // Shared client for the black-box conformance suite. It speaks plain22 // NIP-01 over a websocket to whatever relay RELAY_URL names.33 import WebSocket from "ws";4+import { afterEach } from "vitest";45 import { finalizeEvent, generateSecretKey, getPublicKey, type Event as NostrEvent, type EventTemplate } from "nostr-tools/pure";56 import { sha256 } from "@noble/hashes/sha2.js";67 import { Negentropy, hexToBytes, type SyncItem } from "../../src/negentropy.ts";@@ -192,3 +193,15 @@ export function randHex(n = 64): string {
192193 while (s.length < n) s += Math.floor(Math.random() * 16).toString(16);193194 return s;194195 }196+197+// sockets returns a connect that keeps its clients and closes them after198+// each test.199+export function sockets() {200+ const open: Client[] = [];201+ afterEach(() => open.splice(0).forEach((c) => c.close()));202+ return async () => {203+ const c = await Client.connect();204+ open.push(c);205+ return c;206+ };207+}@@ -1,13 +1,7 @@
1-import { describe, it, expect, afterEach } from "vitest";2-import { Client, newEvent, newKey, pub, now, rand } from "./helpers.ts";1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, pub, now, rand, sockets } from "./helpers.ts";33 4-const open: Client[] = [];5-const connect = async () => {6- const c = await Client.connect();7- open.push(c);8- return c;9-};10-afterEach(() => open.splice(0).forEach((c) => c.close()));4+const connect = sockets();115 126 describe("NIP-01 core", () => {137 it("publishes, pushes live, dedups, rejects tampering, queries, replaces, limits, closes", async () => {@@ -99,32 +93,3 @@ describe("NIP-01 core", () => {
9993 await c.expect("EOSE");10094 });10195 });102-103-describe("NIP-09 deletion", () => {104- it("lets only the author delete, keeps deleted ids out, ignores strangers", async () => {105- const sk = newKey();106- const other = newKey();107- const alice = await connect();108- const stranger = await connect();109- const carol = await connect();110- const note = newEvent(sk, 1, "regret", [], now() - 100);111- await alice.publish(note);112-113- await stranger.publish(newEvent(other, 5, "", [["e", note.id]]));114- expect((await carol.req({ ids: [note.id] })).events.length).toBe(1);115-116- await alice.publish(newEvent(sk, 5, "", [["e", note.id], ["k", "1"]]));117- expect((await carol.req({ ids: [note.id] })).events.length).toBe(0);118- alice.send("EVENT", note);119- expect(await alice.expectOK(note.id, false)).toMatch(/^blocked:/);120- });121-122- it("deletes addressable versions up to the request time via a tags", async () => {123- const sk = newKey();124- const c = await connect();125- const t0 = now() - 100;126- await c.publish(newEvent(sk, 30023, "v1", [["d", "x"]], t0));127- await c.publish(newEvent(sk, 5, "", [["a", `30023:${pub(sk)}:x`]], t0 + 1));128- expect((await c.req({ kinds: [30023], authors: [pub(sk)], "#d": ["x"] })).events.length).toBe(0);129- });130-});@@ -0,0 +1,33 @@
1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, pub, now, sockets } from "./helpers.ts";3+4+const connect = sockets();5+6+describe("NIP-09 deletion", () => {7+ it("lets only the author delete, keeps deleted ids out, ignores strangers", async () => {8+ const sk = newKey();9+ const other = newKey();10+ const alice = await connect();11+ const stranger = await connect();12+ const carol = await connect();13+ const note = newEvent(sk, 1, "regret", [], now() - 100);14+ await alice.publish(note);15+16+ await stranger.publish(newEvent(other, 5, "", [["e", note.id]]));17+ expect((await carol.req({ ids: [note.id] })).events.length).toBe(1);18+19+ await alice.publish(newEvent(sk, 5, "", [["e", note.id], ["k", "1"]]));20+ expect((await carol.req({ ids: [note.id] })).events.length).toBe(0);21+ alice.send("EVENT", note);22+ expect(await alice.expectOK(note.id, false)).toMatch(/^blocked:/);23+ });24+25+ it("deletes addressable versions up to the request time via a tags", async () => {26+ const sk = newKey();27+ const c = await connect();28+ const t0 = now() - 100;29+ await c.publish(newEvent(sk, 30023, "v1", [["d", "x"]], t0));30+ await c.publish(newEvent(sk, 5, "", [["a", `30023:${pub(sk)}:x`]], t0 + 1));31+ expect((await c.req({ kinds: [30023], authors: [pub(sk)], "#d": ["x"] })).events.length).toBe(0);32+ });33+});@@ -0,0 +1,15 @@
1+import { describe, it, expect } from "vitest";2+import { HTTP_URL } from "./helpers.ts";3+4+describe("NIP-11", () => {5+ it("serves the information document with limitations", async () => {6+ const resp = await fetch(HTTP_URL, { headers: { Accept: "application/nostr+json" } });7+ expect(resp.status).toBe(200);8+ expect(resp.headers.get("content-type")).toContain("application/nostr+json");9+ const doc: any = await resp.json();10+ expect(typeof doc.name).toBe("string");11+ for (const n of [1, 9, 11, 40, 42, 45, 50, 62, 67, 70, 77]) expect(doc.supported_nips).toContain(n);12+ expect(doc.limitation.max_subid_length).toBeGreaterThanOrEqual(64);13+ expect(typeof doc.limitation.max_limit).toBe("number");14+ });15+});@@ -0,0 +1,47 @@
1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, pub, now, sockets } from "./helpers.ts";3+4+const connect = sockets();5+6+describe("NIP-17 / NIP-59 private kinds", () => {7+ it("serves gift wraps only to their recipients, stored and live, and lets recipients delete them", async () => {8+ const sender = newKey();9+ const recipient = newKey();10+ const stranger = newKey();11+ const s = await connect();12+ const wrap = newEvent(sender, 1059, "ciphertext", [["p", pub(recipient)]], now() - 100);13+ await s.publish(wrap);14+15+ const x = await connect();16+ x.send("REQ", "dm", { kinds: [1059], "#p": [pub(recipient)] });17+ await x.expectClosed("auth-required:");18+ let r = await x.req({ "#p": [pub(recipient)] });19+ expect(r.events).toEqual([]);20+ expect(r.hints).toContain("auth");21+ x.send("COUNT", "n", { "#p": [pub(recipient)] });22+ expect((await x.expect("COUNT"))[2].count).toBe(0);23+24+ await x.auth(stranger);25+ r = await x.req({ kinds: [1059], "#p": [pub(recipient)] });26+ expect(r.events).toEqual([]);27+ x.send("REQ", "live", { kinds: [1059], "#p": [pub(recipient)] });28+ await x.expect("EOSE");29+ await s.publish(newEvent(sender, 1059, "ciphertext2", [["p", pub(recipient)]], now() - 99));30+ await x.expectNothing();31+32+ const rc = await connect();33+ await rc.auth(recipient);34+ r = await rc.req({ kinds: [1059], "#p": [pub(recipient)] });35+ expect(r.events.length).toBe(2);36+ rc.send("REQ", "live", { kinds: [1059], "#p": [pub(recipient)] });37+ await rc.drain();38+ const wrap3 = newEvent(sender, 1059, "ciphertext3", [["p", pub(recipient)]], now() - 98);39+ await s.publish(wrap3);40+ expect((await rc.expect("EVENT"))[2].id).toBe(wrap3.id);41+42+ await rc.publish(newEvent(recipient, 5, "", [["e", wrap3.id]]));43+ expect((await rc.req({ ids: [wrap3.id] })).events).toEqual([]);44+ s.send("EVENT", wrap3);45+ expect(await s.expectOK(wrap3.id, false)).toMatch(/^blocked:/);46+ });47+});@@ -0,0 +1,21 @@
1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, now, sleep, sockets } from "./helpers.ts";3+4+const connect = sockets();5+6+describe("NIP-40 expiration", () => {7+ it("refuses expired events, serves unexpired ones, and stops serving after expiry", async () => {8+ const sk = newKey();9+ const c = await connect();10+ const dead = newEvent(sk, 1, "gone", [["expiration", String(now() - 1)]]);11+ c.send("EVENT", dead);12+ expect(await c.expectOK(dead.id, false)).toMatch(/expired/);13+ const soon = newEvent(sk, 1, "soon", [["expiration", String(now() + 2)]]);14+ await c.publish(soon);15+ expect((await c.req({ ids: [soon.id] })).events.length).toBe(1);16+ await sleep(2500);17+ expect((await c.req({ ids: [soon.id] })).events.length).toBe(0);18+ c.send("COUNT", "q", { ids: [soon.id] });19+ expect((await c.expect("COUNT"))[2].count).toBe(0);20+ });21+});@@ -1,13 +1,7 @@
1-import { describe, it, expect, afterEach } from "vitest";2-import { Client, newEvent, newKey, pub, now, RELAY_URL } from "./helpers.ts";1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, now, RELAY_URL, sockets } from "./helpers.ts";33 4-const open: Client[] = [];5-const connect = async () => {6- const c = await Client.connect();7- open.push(c);8- return c;9-};10-afterEach(() => open.splice(0).forEach((c) => c.close()));4+const connect = sockets();115 126 describe("NIP-42 AUTH", () => {137 it("refuses bad challenges, wrong relays, stale timestamps, wrong kinds; accepts several keys", async () => {@@ -34,61 +28,3 @@ describe("NIP-42 AUTH", () => {
3428 await c.auth(newKey());3529 });3630 });37-38-describe("NIP-70 protected events", () => {39- it("accepts ['-'] events only from a connection authenticated as the author", async () => {40- const sk = newKey();41- const c = await connect();42- const e = newEvent(sk, 1, "members only", [["-"]]);43- c.send("EVENT", e);44- expect(await c.expectOK(e.id, false)).toMatch(/^auth-required:/);45- await c.auth(newKey());46- c.send("EVENT", e);47- await c.expectOK(e.id, false);48- await c.auth(sk);49- await c.publish(e);50- });51-});52-53-describe("NIP-17 / NIP-59 private kinds", () => {54- it("serves gift wraps only to their recipients, stored and live, and lets recipients delete them", async () => {55- const sender = newKey();56- const recipient = newKey();57- const stranger = newKey();58- const s = await connect();59- const wrap = newEvent(sender, 1059, "ciphertext", [["p", pub(recipient)]], now() - 100);60- await s.publish(wrap);61-62- const x = await connect();63- x.send("REQ", "dm", { kinds: [1059], "#p": [pub(recipient)] });64- await x.expectClosed("auth-required:");65- let r = await x.req({ "#p": [pub(recipient)] });66- expect(r.events).toEqual([]);67- expect(r.hints).toContain("auth");68- x.send("COUNT", "n", { "#p": [pub(recipient)] });69- expect((await x.expect("COUNT"))[2].count).toBe(0);70-71- await x.auth(stranger);72- r = await x.req({ kinds: [1059], "#p": [pub(recipient)] });73- expect(r.events).toEqual([]);74- x.send("REQ", "live", { kinds: [1059], "#p": [pub(recipient)] });75- await x.expect("EOSE");76- await s.publish(newEvent(sender, 1059, "ciphertext2", [["p", pub(recipient)]], now() - 99));77- await x.expectNothing();78-79- const rc = await connect();80- await rc.auth(recipient);81- r = await rc.req({ kinds: [1059], "#p": [pub(recipient)] });82- expect(r.events.length).toBe(2);83- rc.send("REQ", "live", { kinds: [1059], "#p": [pub(recipient)] });84- await rc.drain();85- const wrap3 = newEvent(sender, 1059, "ciphertext3", [["p", pub(recipient)]], now() - 98);86- await s.publish(wrap3);87- expect((await rc.expect("EVENT"))[2].id).toBe(wrap3.id);88-89- await rc.publish(newEvent(recipient, 5, "", [["e", wrap3.id]]));90- expect((await rc.req({ ids: [wrap3.id] })).events).toEqual([]);91- s.send("EVENT", wrap3);92- expect(await s.expectOK(wrap3.id, false)).toMatch(/^blocked:/);93- });94-});@@ -0,0 +1,41 @@
1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, pub, now, randHex, sockets } from "./helpers.ts";3+4+const connect = sockets();5+6+describe("NIP-45 COUNT", () => {7+ it("counts across OR'd filters without double counting, and adds an hll sketch for tag filters", async () => {8+ const sk = newKey();9+ const c = await connect();10+ const target = randHex();11+ for (let i = 0; i < 3; i++) await c.publish(newEvent(sk, 1, "n", [["t", target]], now() - 10 + i));12+ await c.publish(newEvent(sk, 7, "+", [["e", target]], now() - 5));13+14+ c.send("COUNT", "n", { kinds: [1], authors: [pub(sk)] }, { kinds: [7], authors: [pub(sk)] });15+ expect((await c.expect("COUNT"))[2].count).toBe(4);16+ c.send("COUNT", "n", { kinds: [1], authors: [pub(sk)] }, { "#t": [target], authors: [pub(sk)] });17+ expect((await c.expect("COUNT"))[2].count).toBe(3);18+19+ c.send("COUNT", "r", { kinds: [7], "#e": [target] });20+ const res = (await c.expect("COUNT"))[2];21+ expect(res.count).toBe(1);22+ expect(res.hll).toMatch(/^[0-9a-f]{512}$/);23+ // Register: offset = nibble 32 of the target + 8; byte[offset] indexes,24+ // zero run after it plus one is the value.25+ const offset = parseInt(target[32], 16) + 8;26+ const pk = pub(sk);27+ const regIndex = parseInt(pk.substr(offset * 2, 2), 16);28+ const rest = pk.slice((offset + 1) * 2);29+ let zeros = 0;30+ for (const ch of rest) {31+ const n = parseInt(ch, 16);32+ if (n === 0) {33+ zeros += 4;34+ continue;35+ }36+ zeros += Math.clz32(n) - 28;37+ break;38+ }39+ expect(parseInt(res.hll.substr(regIndex * 2, 2), 16)).toBe(zeros + 1);40+ });41+});@@ -0,0 +1,27 @@
1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, pub, now, rand, sockets } from "./helpers.ts";3+4+const connect = sockets();5+6+describe("NIP-50 search", () => {7+ it("matches words in content, case-insensitively, ignores extensions, is injection-safe, and matches live", async () => {8+ const sk = newKey();9+ const c = await connect();10+ const w = rand();11+ const texts = [`Purple ${w}elephants dance`, `${w}orange juice`, `the ${w}elephant in the room`];12+ for (const [i, t] of texts.entries()) await c.publish(newEvent(sk, 1, t, [], now() - 10 + i));13+ await c.publish(newEvent(sk, 4, `${w}elephant encrypted`, [["p", pub(sk)]], now() - 5));14+15+ let r = await c.req({ search: `${w}elephant`, authors: [pub(sk)] });16+ expect(r.events.map((e) => e.content)).toEqual([texts[2]]);17+ r = await c.req({ search: `PURPLE include:spam dance ${w}elephants`, authors: [pub(sk)] });18+ expect(r.events.map((e) => e.content)).toEqual([texts[0]]);19+ r = await c.req({ search: `${w}juice OR NOT "`, authors: [pub(sk)] });20+ expect(r.hints).toContain("finish");21+22+ c.send("REQ", "live", { search: `${w}banana`, authors: [pub(sk)] });23+ await c.drain();24+ await c.publish(newEvent(sk, 1, `a ${w}Banana split`));25+ await c.expect("EVENT");26+ });27+});@@ -0,0 +1,39 @@
1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, pub, now, RELAY_URL, sockets } from "./helpers.ts";3+4+const connect = sockets();5+6+describe("NIP-62 vanish", () => {7+ it("deletes everything from the pubkey up to the request, plus gift wraps to it, and blocks resurrection", async () => {8+ const sk = newKey();9+ const other = newKey();10+ const c = await connect();11+ const t0 = now() - 100;12+ const note = newEvent(sk, 1, "regret", [], t0);13+ const meta = newEvent(sk, 0, "{}", [], t0);14+ const wrap = newEvent(other, 1059, "dm", [["p", pub(sk)]], t0);15+ const keep = newEvent(other, 1, "unrelated", [], t0);16+ for (const e of [note, meta, wrap, keep]) await c.publish(e);17+18+ let v = newEvent(sk, 62, "", [["relay", "wss://other.example"]], t0 + 1);19+ c.send("EVENT", v);20+ await c.expectOK(v.id, false);21+ expect((await c.req({ authors: [pub(sk)] })).events.length).toBe(2);22+23+ v = newEvent(sk, 62, "bye", [["relay", RELAY_URL]], t0 + 1);24+ await c.publish(v);25+ expect((await c.req({ authors: [pub(sk)] })).events).toEqual([]);26+ expect((await c.req({ ids: [keep.id] })).events.length).toBe(1);27+ const rc = await connect();28+ await rc.auth(sk);29+ expect((await rc.req({ kinds: [1059], "#p": [pub(sk)] })).events).toEqual([]);30+31+ c.send("EVENT", note);32+ expect(await c.expectOK(note.id, false)).toMatch(/^blocked:/);33+ await c.publish(newEvent(sk, 1, "fresh start", [], t0 + 2));34+35+ v = newEvent(other, 62, "", [["relay", "ALL_RELAYS"]], t0 + 3);36+ await c.publish(v);37+ expect((await c.req({ authors: [pub(other)] })).events).toEqual([]);38+ });39+});@@ -0,0 +1,19 @@
1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, sockets } from "./helpers.ts";3+4+const connect = sockets();5+6+describe("NIP-70 protected events", () => {7+ it("accepts ['-'] events only from a connection authenticated as the author", async () => {8+ const sk = newKey();9+ const c = await connect();10+ const e = newEvent(sk, 1, "members only", [["-"]]);11+ c.send("EVENT", e);12+ expect(await c.expectOK(e.id, false)).toMatch(/^auth-required:/);13+ await c.auth(newKey());14+ c.send("EVENT", e);15+ await c.expectOK(e.id, false);16+ await c.auth(sk);17+ await c.publish(e);18+ });19+});@@ -0,0 +1,47 @@
1+import { describe, it, expect } from "vitest";2+import { newEvent, newKey, pub, now, item, sockets } from "./helpers.ts";3+4+const connect = sockets();5+6+describe("NIP-77 negentropy", () => {7+ it("reconciles a client set against the relay's, respecting visibility", async () => {8+ const sk = newKey();9+ const recipient = newKey();10+ const c = await connect();11+ const t0 = now() - 1000;12+ const relayHas = [];13+ for (let i = 0; i < 100; i++) relayHas.push(await (async () => {14+ const e = newEvent(sk, 1, String(i), [], t0 + Math.floor(i / 3));15+ await c.publish(e);16+ return e;17+ })());18+ const local = [];19+ for (let i = 0; i < 5; i++) local.push(newEvent(sk, 1, "local " + i, [], t0 + 10 + i));20+ const wrap = newEvent(sk, 1059, "dm", [["p", pub(recipient)]], t0 + 5);21+ await c.publish(wrap);22+23+ const clientItems = [...relayHas.slice(0, 60), ...local].map(item);24+ const { have, need } = await c.sync(clientItems, { kinds: [1], authors: [pub(sk)] });25+ expect(have).toEqual(local.map((e) => e.id).sort());26+ expect(need).toEqual(relayHas.slice(60).map((e) => e.id).sort());27+28+ // Strangers never learn the gift wrap; the recipient does.29+ let r = await c.sync(relayHas.map(item), { authors: [pub(sk)] });30+ expect(r.need).toEqual([]);31+ const rc = await connect();32+ await rc.auth(recipient);33+ r = await rc.sync(relayHas.map(item), { authors: [pub(sk)] });34+ expect(r.need).toEqual([wrap.id]);35+36+ // Error paths.37+ c.send("NEG-MSG", "nope", "61");38+ expect((await c.expect("NEG-ERR"))[2]).toMatch(/^closed:/);39+ c.send("NEG-OPEN", "bad", {}, "zz");40+ expect((await c.expect("NEG-ERR"))[2]).toMatch(/^invalid:/);41+ const x = await connect();42+ x.send("NEG-OPEN", "dm", { kinds: [1059] }, "61");43+ expect((await x.expect("NEG-ERR"))[2]).toMatch(/^auth-required:/);44+ x.send("NEG-OPEN", "v", { kinds: [1], authors: [pub(sk)] }, "62");45+ expect((await x.expect("NEG-MSG"))[2]).toBe("61");46+ });47+});@@ -1,178 +0,0 @@
1-import { describe, it, expect, afterEach } from "vitest";2-import { Client, newEvent, newKey, pub, now, rand, randHex, sleep, item, RELAY_URL, HTTP_URL } from "./helpers.ts";3-4-const open: Client[] = [];5-const connect = async () => {6- const c = await Client.connect();7- open.push(c);8- return c;9-};10-afterEach(() => open.splice(0).forEach((c) => c.close()));11-12-describe("NIP-11", () => {13- it("serves the information document with limitations", async () => {14- const resp = await fetch(HTTP_URL, { headers: { Accept: "application/nostr+json" } });15- expect(resp.status).toBe(200);16- expect(resp.headers.get("content-type")).toContain("application/nostr+json");17- const doc: any = await resp.json();18- expect(typeof doc.name).toBe("string");19- for (const n of [1, 9, 11, 40, 42, 45, 50, 62, 67, 70, 77]) expect(doc.supported_nips).toContain(n);20- expect(doc.limitation.max_subid_length).toBeGreaterThanOrEqual(64);21- expect(typeof doc.limitation.max_limit).toBe("number");22- });23-});24-25-describe("NIP-45 COUNT", () => {26- it("counts across OR'd filters without double counting, and adds an hll sketch for tag filters", async () => {27- const sk = newKey();28- const c = await connect();29- const target = randHex();30- for (let i = 0; i < 3; i++) await c.publish(newEvent(sk, 1, "n", [["t", target]], now() - 10 + i));31- await c.publish(newEvent(sk, 7, "+", [["e", target]], now() - 5));32-33- c.send("COUNT", "n", { kinds: [1], authors: [pub(sk)] }, { kinds: [7], authors: [pub(sk)] });34- expect((await c.expect("COUNT"))[2].count).toBe(4);35- c.send("COUNT", "n", { kinds: [1], authors: [pub(sk)] }, { "#t": [target], authors: [pub(sk)] });36- expect((await c.expect("COUNT"))[2].count).toBe(3);37-38- c.send("COUNT", "r", { kinds: [7], "#e": [target] });39- const res = (await c.expect("COUNT"))[2];40- expect(res.count).toBe(1);41- expect(res.hll).toMatch(/^[0-9a-f]{512}$/);42- // Register: offset = nibble 32 of the target + 8; byte[offset] indexes,43- // zero run after it plus one is the value.44- const offset = parseInt(target[32], 16) + 8;45- const pk = pub(sk);46- const regIndex = parseInt(pk.substr(offset * 2, 2), 16);47- const rest = pk.slice((offset + 1) * 2);48- let zeros = 0;49- for (const ch of rest) {50- const n = parseInt(ch, 16);51- if (n === 0) {52- zeros += 4;53- continue;54- }55- zeros += Math.clz32(n) - 28;56- break;57- }58- expect(parseInt(res.hll.substr(regIndex * 2, 2), 16)).toBe(zeros + 1);59- });60-});61-62-describe("NIP-40 expiration", () => {63- it("refuses expired events, serves unexpired ones, and stops serving after expiry", async () => {64- const sk = newKey();65- const c = await connect();66- const dead = newEvent(sk, 1, "gone", [["expiration", String(now() - 1)]]);67- c.send("EVENT", dead);68- expect(await c.expectOK(dead.id, false)).toMatch(/expired/);69- const soon = newEvent(sk, 1, "soon", [["expiration", String(now() + 2)]]);70- await c.publish(soon);71- expect((await c.req({ ids: [soon.id] })).events.length).toBe(1);72- await sleep(2500);73- expect((await c.req({ ids: [soon.id] })).events.length).toBe(0);74- c.send("COUNT", "q", { ids: [soon.id] });75- expect((await c.expect("COUNT"))[2].count).toBe(0);76- });77-});78-79-describe("NIP-50 search", () => {80- it("matches words in content, case-insensitively, ignores extensions, is injection-safe, and matches live", async () => {81- const sk = newKey();82- const c = await connect();83- const w = rand();84- const texts = [`Purple ${w}elephants dance`, `${w}orange juice`, `the ${w}elephant in the room`];85- for (const [i, t] of texts.entries()) await c.publish(newEvent(sk, 1, t, [], now() - 10 + i));86- await c.publish(newEvent(sk, 4, `${w}elephant encrypted`, [["p", pub(sk)]], now() - 5));87-88- let r = await c.req({ search: `${w}elephant`, authors: [pub(sk)] });89- expect(r.events.map((e) => e.content)).toEqual([texts[2]]);90- r = await c.req({ search: `PURPLE include:spam dance ${w}elephants`, authors: [pub(sk)] });91- expect(r.events.map((e) => e.content)).toEqual([texts[0]]);92- r = await c.req({ search: `${w}juice OR NOT "`, authors: [pub(sk)] });93- expect(r.hints).toContain("finish");94-95- c.send("REQ", "live", { search: `${w}banana`, authors: [pub(sk)] });96- await c.drain();97- await c.publish(newEvent(sk, 1, `a ${w}Banana split`));98- await c.expect("EVENT");99- });100-});101-102-describe("NIP-62 vanish", () => {103- it("deletes everything from the pubkey up to the request, plus gift wraps to it, and blocks resurrection", async () => {104- const sk = newKey();105- const other = newKey();106- const c = await connect();107- const t0 = now() - 100;108- const note = newEvent(sk, 1, "regret", [], t0);109- const meta = newEvent(sk, 0, "{}", [], t0);110- const wrap = newEvent(other, 1059, "dm", [["p", pub(sk)]], t0);111- const keep = newEvent(other, 1, "unrelated", [], t0);112- for (const e of [note, meta, wrap, keep]) await c.publish(e);113-114- let v = newEvent(sk, 62, "", [["relay", "wss://other.example"]], t0 + 1);115- c.send("EVENT", v);116- await c.expectOK(v.id, false);117- expect((await c.req({ authors: [pub(sk)] })).events.length).toBe(2);118-119- v = newEvent(sk, 62, "bye", [["relay", RELAY_URL]], t0 + 1);120- await c.publish(v);121- expect((await c.req({ authors: [pub(sk)] })).events).toEqual([]);122- expect((await c.req({ ids: [keep.id] })).events.length).toBe(1);123- const rc = await connect();124- await rc.auth(sk);125- expect((await rc.req({ kinds: [1059], "#p": [pub(sk)] })).events).toEqual([]);126-127- c.send("EVENT", note);128- expect(await c.expectOK(note.id, false)).toMatch(/^blocked:/);129- await c.publish(newEvent(sk, 1, "fresh start", [], t0 + 2));130-131- v = newEvent(other, 62, "", [["relay", "ALL_RELAYS"]], t0 + 3);132- await c.publish(v);133- expect((await c.req({ authors: [pub(other)] })).events).toEqual([]);134- });135-});136-137-describe("NIP-77 negentropy", () => {138- it("reconciles a client set against the relay's, respecting visibility", async () => {139- const sk = newKey();140- const recipient = newKey();141- const c = await connect();142- const t0 = now() - 1000;143- const relayHas = [];144- for (let i = 0; i < 100; i++) relayHas.push(await (async () => {145- const e = newEvent(sk, 1, String(i), [], t0 + Math.floor(i / 3));146- await c.publish(e);147- return e;148- })());149- const local = [];150- for (let i = 0; i < 5; i++) local.push(newEvent(sk, 1, "local " + i, [], t0 + 10 + i));151- const wrap = newEvent(sk, 1059, "dm", [["p", pub(recipient)]], t0 + 5);152- await c.publish(wrap);153-154- const clientItems = [...relayHas.slice(0, 60), ...local].map(item);155- const { have, need } = await c.sync(clientItems, { kinds: [1], authors: [pub(sk)] });156- expect(have).toEqual(local.map((e) => e.id).sort());157- expect(need).toEqual(relayHas.slice(60).map((e) => e.id).sort());158-159- // Strangers never learn the gift wrap; the recipient does.160- let r = await c.sync(relayHas.map(item), { authors: [pub(sk)] });161- expect(r.need).toEqual([]);162- const rc = await connect();163- await rc.auth(recipient);164- r = await rc.sync(relayHas.map(item), { authors: [pub(sk)] });165- expect(r.need).toEqual([wrap.id]);166-167- // Error paths.168- c.send("NEG-MSG", "nope", "61");169- expect((await c.expect("NEG-ERR"))[2]).toMatch(/^closed:/);170- c.send("NEG-OPEN", "bad", {}, "zz");171- expect((await c.expect("NEG-ERR"))[2]).toMatch(/^invalid:/);172- const x = await connect();173- x.send("NEG-OPEN", "dm", { kinds: [1059] }, "61");174- expect((await x.expect("NEG-ERR"))[2]).toMatch(/^auth-required:/);175- x.send("NEG-OPEN", "v", { kinds: [1], authors: [pub(sk)] }, "62");176- expect((await x.expect("NEG-MSG"))[2]).toBe("61");177- });178-});