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 / site-mirror.test.ts
site-mirror.test.ts 5.5K
1import { SELF, env, runInDurableObject } from "cloudflare:test";2import { describe, it, expect } from "vitest";3import { generateSecretKey } from "nostr-tools/pure";4import { sha256 } from "@noble/hashes/sha2.js";5import { bytesToHex } from "../../src/negentropy.ts";6import { siteLabel } from "../../src/sites.ts";7import { queueMirrors, remoteSiteBlob } from "../../src/site-mirror.ts";8import { ev, pk, rpc, now, drive } from "../helpers/relay.ts";9import { upload } from "../helpers/media.ts";10import { WS } from "../helpers/ws.ts";1112const text = "<!doctype html><h1>from Blossom</h1>";13const hash = bytesToHex(sha256(new TextEncoder().encode(text)));1415describe("site mirrors", () => {16 it("proxies missing files using manifest servers then BUD-03, verifying hashes and forwarding metadata", async () => {17 const name = "site-proxy", host = name + ".bind.ws", sk = generateSecretKey();18 await rpc(host, sk, "claim");19 await rpc(host, sk, "setpolicy", { features: { sites: { mirror: false } } });20 const calls: string[] = [];21 await runInDurableObject(env.RELAY.getByName(name), (r) => {22 r.fetcher = async (url) => {23 calls.push(url);24 return url.startsWith("https://bad.test") ? new Response("wrong bytes") : new Response(text, { headers: { "content-type": "text/html; charset=utf-8", "content-length": String(text.length) } });25 };26 });27 const e = ev(sk, 15128, "", [["path", "/index.html", hash], ["server", "https://bad.test"]]);28 const c = await WS.connect(host);29 expect((await c.ok(ev(sk, 10063, "", [["server", "https://good.test"]]))).ok).toBe(true);30 expect((await c.ok(e)).ok).toBe(true);31 await runInDurableObject(env.RELAY.getByName(name), (r) => r.syncSites());32 const url = "http://" + siteLabel(e) + ".bind.ws/";33 const response = await SELF.fetch(url);34 expect(response.status).toBe(200);35 expect(await response.text()).toBe(text);36 expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8");37 expect(response.headers.get("content-length")).toBe(String(text.length));38 expect(calls.slice(-2)).toEqual(["https://bad.test/" + hash, "https://good.test/" + hash]);39 expect((await rpc(host, sk, "listblobs")).result).toEqual([]);40 await drive(host, sk);41 expect((await rpc(host, sk, "listjobs")).result).toEqual([]);42 c.ws.close();43 });4445 it("mirrors each unique file once as the manifest author, including sibling relays", async () => {46 const name = "site-mirror", host = name + ".bind.ws", sk = generateSecretKey();47 const origin = "site-origin.bind.ws";48 await rpc(host, sk, "claim"); await rpc(origin, sk, "claim");49 const file = await upload(origin, sk, text);50 const e = ev(sk, 35128, "", [["d", "blog"], ["path", "/index.html", file.sha], ["path", "/alias.html", file.sha], ["server", "https://" + origin]]);51 const c = await WS.connect(host); expect((await c.ok(e)).ok).toBe(true);52 const jobs = await drive(host, sk);53 expect(jobs.find((j) => j.kind === "mirror")?.last).toMatchObject({ blobs: 1, error: "" });54 const blobs = (await rpc(host, sk, "listblobs")).result;55 expect(blobs).toHaveLength(1); expect(blobs[0]).toMatchObject({ sha256: file.sha, uploader: pk(sk), size: text.length });56 await rpc(origin, sk, "setpolicy", { reads: "members" });57 const response = await SELF.fetch("http://" + siteLabel(e) + ".bind.ws/");58 expect(response.status).toBe(200); expect(await response.text()).toBe(text);59 c.ws.close();60 });6162 it("does not resurrect a deleted manifest or a moderator-blocked blob", async () => {63 const name = "site-deleted", host = name + ".bind.ws", sk = generateSecretKey();64 await rpc(host, sk, "claim");65 const e = ev(sk, 15128, "", [["path", "/index.html", hash], ["server", "https://good.test"]]);66 await runInDurableObject(env.RELAY.getByName(name), async (r) => {67 r.accept(e, null);68 await queueMirrors(r);69 r.store.deleteEvent(e.id);70 r.fetcher = async () => { throw new Error("must not fetch deleted site"); };71 });72 const jobs = await drive(host, sk);73 expect(jobs.find((j) => j.kind === "mirror")?.last?.blobs).toBe(0);74 await runInDurableObject(env.RELAY.getByName(name), async (r) => {75 r.settings.setEvent(hash, "ban", "removed", now());76 expect(await remoteSiteBlob(r, e, e.tags[0])).toBeNull();77 });78 expect((await rpc(host, sk, "listblobs")).result).toEqual([]);79 });8081 it("rejects private-address redirects, oversize bodies and hash mismatches, and infers type only without both headers", async () => {82 const name = "site-origins", host = name + ".bind.ws", sk = generateSecretKey();83 await rpc(host, sk, "claim");84 const e = ev(sk, 15128, "", [["path", "/index.html", hash], ["server", "https://good.test"]]);85 await runInDurableObject(env.RELAY.getByName(name), async (r) => {86 let calls = 0;87 r.fetcher = async () => { calls++; return new Response(null, { status: 302, headers: { location: "https://127.0.0.1/secret" } }); };88 expect(await remoteSiteBlob(r, e, e.tags[0])).toBeNull(); expect(calls).toBe(1);89 r.fetcher = async () => new Response(text, { headers: { "content-length": String(100 * 1024 * 1024) } });90 expect(await remoteSiteBlob(r, e, e.tags[0])).toBeNull();91 r.fetcher = async () => new Response("corrupted");92 expect(await remoteSiteBlob(r, e, e.tags[0])).toBeNull();93 r.fetcher = async () => new Response(new TextEncoder().encode(text));94 expect((await remoteSiteBlob(r, e, e.tags[0]))?.type).toBe("text/html; charset=utf-8");95 });96 });97});