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 / site-addresses.test.ts
site-addresses.test.ts 11.5K
1import { SELF, env, runInDurableObject } from "cloudflare:test";2import { describe, expect, it } from "vitest";3import { generateSecretKey } from "nostr-tools/pure";4import { aggregate, siteLabel } from "../../src/sites.ts";5import { upload } from "../helpers/media.ts";6import { ev, nip98, now, pk, rpc } from "../helpers/relay.ts";7import { WS } from "../helpers/ws.ts";89describe("site web addresses", () => {10 it("maps the root and browser directory paths to manifest events", async () => {11 const name = "site-addresses", host = `${name}.bind.ws`, sk = generateSecretKey();12 await rpc(host, sk, "claim");13 const file = await upload(host, sk, "site address");14 const c = await WS.connect(host);15 const site = ev(sk, 15128, "", [["path", "/index.html", file.sha], ["path", "/docs/index.html", file.sha], ["path", "/404.html", file.sha]]);16 const named = ev(sk, 35128, "", [["d", "docs"], ["path", "/index.html", file.sha]]);17 const snapshot = ev(sk, 5128, "", [["path", "/index.html", file.sha], ["a", `15128:${site.pubkey}:`], ["x", aggregate({ tags: [["path", "/index.html", file.sha], ["a", `15128:${site.pubkey}:`]] }), "aggregate"]]);18 expect(await c.ok(site)).toMatchObject({ ok: true });19 expect(await c.ok(named)).toMatchObject({ ok: true });20 expect(await c.ok(snapshot)).toMatchObject({ ok: true });21 await runInDurableObject(env.RELAY.getByName(name), (relay) => relay.syncSites());22 const address = `http://${siteLabel(site)}.bind.ws/.well-known/nostr.json`;23 const root = await SELF.fetch(address + "?path=%2F");24 expect(root.status).toBe(200);25 expect(root.headers.get("access-control-allow-origin")).toBe("*");26 expect(root.headers.get("cache-control")).toContain("no-store");27 expect(await root.json()).toEqual({ "/": { filter: { authors: [site.pubkey], kinds: [15128], limit: 1 }, relays: [`wss://${name}.bind.ws`] } });28 expect(await (await SELF.fetch(address + "?path=%2Fdocs")).json()).toEqual({ "/docs": { filter: { authors: [site.pubkey], kinds: [15128], limit: 1 }, relays: [`wss://${name}.bind.ws`] } });29 expect(await (await SELF.fetch(address + "?path=%2Fmissing.html")).json()).toEqual({});30 const namedURL = `http://${siteLabel(named)}.bind.ws/.well-known/nostr.json?path=%2F`;31 expect(await (await SELF.fetch(namedURL)).json()).toEqual({ "/": { filter: { authors: [named.pubkey], kinds: [35128], "#d": ["docs"], limit: 1 }, relays: [`wss://${name}.bind.ws`] } });32 const snapshotURL = `http://${siteLabel(snapshot)}.bind.ws/.well-known/nostr.json?path=%2F`;33 expect(await (await SELF.fetch(snapshotURL)).json()).toEqual({ "/": { filter: { ids: [snapshot.id], limit: 1 }, relays: [`wss://${name}.bind.ws`] } });34 const head = await SELF.fetch(address + "?path=%2F", { method: "HEAD" });35 expect(head.status).toBe(200);36 expect(await head.text()).toBe("");37 expect((await SELF.fetch(address + "?path=%2F", { method: "POST" })).status).toBe(405);38 expect((await SELF.fetch(address + "?path=%2F&path=%2Fdocs")).status).toBe(400);39 expect((await SELF.fetch(address + "?path=%2F%2e%2e%2Fx")).status).toBe(400);40 c.ws.close();41 });4243 it("keeps NIP-05 names and read authentication separate from path discovery", async () => {44 const name = "site-address-auth", host = `${name}.bind.ws`, sk = generateSecretKey();45 await rpc(host, sk, "claim");46 const c = await WS.connect(host);47 const site = ev(sk, 35128, "", [["d", "docs"], ["path", "/index.html", "ab".repeat(32)]]);48 expect((await c.ok(site)).ok).toBe(true);49 await rpc(host, sk, "setpolicy", { reads: "members" });50 const address = `http://${siteLabel(site)}.bind.ws/.well-known/nostr.json`;51 expect((await SELF.fetch(address + "?name=nobodyq7")).status).toBe(401);52 expect((await SELF.fetch(address + "?path=%2F")).status).toBe(401);53 const options = await SELF.fetch(address + "?path=%2F", { method: "OPTIONS" });54 expect(options.status).toBe(204);55 expect(options.headers.get("access-control-allow-origin")).toBe("*");56 c.ws.close();57 });5859 it("requires an exact NIP-98 URL and applies the site read rule", async () => {60 const name = "site-address-proof", host = `${name}.bind.ws`, owner = generateSecretKey(), outsider = generateSecretKey();61 await rpc(host, owner, "claim");62 const c = await WS.connect(host);63 const site = ev(owner, 15128, "", [["path", "/index.html", "ab".repeat(32)]]);64 expect((await c.ok(site)).ok).toBe(true); c.ws.close();65 await rpc(host, owner, "setpolicy", { reads: "members" });66 const url = `http://${siteLabel(site)}.bind.ws/.well-known/nostr.json?path=%2F`;67 const bad = await SELF.fetch(url, { headers: { authorization: await nip98(outsider, url + "x") } });68 expect(bad.status).toBe(401);69 const denied = await SELF.fetch(url, { headers: { authorization: await nip98(outsider, url) } });70 expect(denied.status).toBe(403);71 const allowed = await SELF.fetch(url, { headers: { authorization: await nip98(owner, url) } });72 expect(allowed.status).toBe(200);73 });7475 it("keeps encoded filenames ahead of their decoded spelling and resolves encoded directories", async () => {76 const name = "site-address-escaped", host = `${name}.bind.ws`, sk = generateSecretKey();77 await rpc(host, sk, "claim");78 const file = await upload(host, sk, "escaped"), blocked = await upload(host, sk, "blocked escaped");79 const c = await WS.connect(host);80 const site = ev(sk, 15128, "", [["path", "/hello world.html", file.sha], ["path", "/hello%20world.html", blocked.sha], ["path", "/docs space/index.html", file.sha], ["path", "/only%20escaped/index.html", file.sha], ["path", "/100%.html", file.sha]]);81 expect((await c.ok(site)).ok).toBe(true); c.ws.close();82 const url = `http://${siteLabel(site)}.bind.ws/.well-known/nostr.json`;83 const encoded = await SELF.fetch(url + "?path=%2Fhello%2520world.html");84 expect(encoded.status).toBe(200);85 expect(Object.keys(await encoded.json())).toEqual(["/hello%20world.html"]);86 const directory = await SELF.fetch(url + "?path=%2Fdocs%2520space%2F");87 expect(directory.status).toBe(200);88 expect(Object.keys(await directory.json())).toEqual(["/docs%20space/"]);89 expect(await (await SELF.fetch(`http://${siteLabel(site)}.bind.ws/docs%20space/`)).text()).toBe("escaped");90 expect(await (await SELF.fetch(url + "?path=%2Fonly%2520escaped%2F")).json()).toEqual({});91 expect(Object.keys(await (await SELF.fetch(url + "?path=%2F100%2525.html")).json())).toEqual(["/100%25.html"]);92 // The encoded filename wins even when the decoded tag comes first.93 await runInDurableObject(env.RELAY.getByName(name), (relay) => { relay.settings.setEvent(blocked.sha, "ban", "moderated"); });94 expect(await (await SELF.fetch(url + "?path=%2Fhello%2520world.html")).json()).toEqual({});95 expect((await SELF.fetch(`http://${siteLabel(site)}.bind.ws/hello%20world.html`)).status).toBe(404);96 });9798 it("preserves custom site targets, canonical relay hints and stale-edge isolation", async () => {99 const name = "site-address-custom", host = `${name}.bind.ws`, sk = generateSecretKey();100 const custom = "site.ad-custom.test";101 await rpc(host, sk, "claim");102 const file = await upload(host, sk, "custom site");103 const c = await WS.connect(host), site = ev(sk, 15128, "", [["path", "/index.html", file.sha]]);104 expect(await c.ok(site)).toMatchObject({ ok: true }); c.ws.close();105 const label = siteLabel(site);106 // The edge still has its old relay-only mapping when the local target changes.107 await env.HOSTS.put(custom, name);108 await runInDurableObject(env.RELAY.getByName(name), (relay) => {109 relay.settings.update({ customHosts: [{ host: custom, id: "custom-1", site: label, at: now(), status: "active", sslStatus: "active" }] });110 });111 const url = `http://${custom}/.well-known/nostr.json?path=%2F`;112 const expected = { "/": { filter: { kinds: [15128], authors: [pk(sk)], limit: 1 }, relays: [`wss://${host}`] } };113 expect(await (await SELF.fetch(url, { headers: { accept: "application/nostr+json" } })).json()).toEqual(expected);114 for (const path of ["/people", "/e/" + site.id, "/.well-known/nostr.json?name=alice"]) expect((await SELF.fetch(`http://${custom}${path}`)).status).toBe(404);115 expect((await SELF.fetch(`http://${custom}/`, { headers: { upgrade: "websocket" } })).status).toBe(405);116 const localURL = `http://${label}.localhost:8787/.well-known/nostr.json?path=%2F`;117 expect(await (await SELF.fetch(localURL)).json()).toEqual({ "/": { ...expected["/"], relays: [`ws://${name}.localhost:8787`] } });118 // A stale site edge mapping must not reopen either a removed site or the relay.119 const stale = "stale.ad-custom.test";120 await env.HOSTS.put(stale, JSON.stringify({ name, site: label }));121 expect((await SELF.fetch(`http://${stale}/.well-known/nostr.json?path=%2F`)).status).toBe(404);122 });123124 it("keeps hosted nostr.json bytes for name lookups without exposing the relay directory", async () => {125 const name = "site-address-names", host = `${name}.bind.ws`, sk = generateSecretKey();126 await rpc(host, sk, "claim");127 await rpc(host, sk, "setmember", pk(sk), { name: "relay-owner" });128 const bytes = JSON.stringify({ names: { website: "ab".repeat(32) } });129 const file = await upload(host, sk, bytes);130 const c = await WS.connect(host);131 const site = ev(sk, 15128, "", [["path", "/index.html", file.sha], ["path", "/.well-known/nostr.json", file.sha]]);132 expect(await c.ok(site)).toMatchObject({ ok: true }); c.ws.close();133 const url = `http://${siteLabel(site)}.bind.ws/.well-known/nostr.json`;134 for (const query of ["", "?name=website", "?name=relay-owner&path=%2F"]) expect(await (await SELF.fetch(url + query)).text()).toBe(bytes);135 const mapping = await (await SELF.fetch(url + "?path=%2F")).json<Record<string, unknown>>();136 expect(Object.keys(mapping)).toEqual(["/"]);137 expect(JSON.stringify(mapping)).not.toContain("relay-owner");138 });139140 it("hides unavailable manifests and switched-off sites even with a cached host route", async () => {141 const name = "site-address-hidden", host = `${name}.bind.ws`, sk = generateSecretKey();142 await rpc(host, sk, "claim");143 const file = await upload(host, sk, "live site");144 const c = await WS.connect(host), site = ev(sk, 15128, "", [["path", "/index.html", file.sha]]);145 expect(await c.ok(site)).toMatchObject({ ok: true }); c.ws.close();146 const label = siteLabel(site), url = `http://${label}.bind.ws/.well-known/nostr.json?path=%2F`;147 expect((await SELF.fetch(url)).status).toBe(200);148 await rpc(host, sk, "setpolicy", { features: { sites: false } });149 expect((await SELF.fetch(url)).status).toBe(404);150 await rpc(host, sk, "setpolicy", { features: { sites: true } });151 await runInDurableObject(env.RELAY.getByName(name), (relay) => { relay.store.hidden.add(site.id); });152 expect((await SELF.fetch(url)).status).toBe(404);153 await runInDurableObject(env.RELAY.getByName(name), (relay) => {154 relay.store.hidden.delete(site.id);155 relay.sql.exec("UPDATE events SET expires=? WHERE id=?", now() - 1, site.id);156 });157 expect((await SELF.fetch(url)).status).toBe(404);158 await runInDurableObject(env.RELAY.getByName(name), (relay) => {159 relay.sql.exec("UPDATE events SET expires=0 WHERE id=?", site.id);160 relay.settings.update({ owner: "", lease: { holder: "", until: now() - 1 } });161 });162 expect((await SELF.fetch(url)).status).toBe(404);163 await runInDurableObject(env.RELAY.getByName(name), (relay) => {164 relay.settings.update({ owner: pk(sk), lease: null });165 relay.sql.exec("DELETE FROM events WHERE id=?", site.id);166 });167 expect((await SELF.fetch(url)).status).toBe(404);168 });169});