clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

main
bindws / test / helpers / relay.ts
relay.ts 3.7K
1// Shared by the Durable Object tests: keys and events, the NIP-86 management2// call, the information document, the HTTP bridge, and the alarm.3import { SELF, env, runInDurableObject } from "cloudflare:test";4import { finalizeEvent, getPublicKey, type Event } from "nostr-tools/pure";5import { getToken } from "nostr-tools/nip98";6import type { Relay } from "../../src/relay.ts";7import type { Job } from "../../src/jobs.ts";89export const now = () => Math.floor(Date.now() / 1000);10export const ev = (sk: Uint8Array, kind: number, content: string, tags: string[][] = [], created_at = now()): Event => finalizeEvent({ kind, content, tags, created_at }, sk);11export const pk = (sk: Uint8Array) => getPublicKey(sk);12export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));13export const tagsOf = (e: Event, name: string) => e.tags.filter((t) => t[0] === name);1415// nip98 signs a request for the exact URL, with the body when there is one.16export const nip98 = (sk: Uint8Array, url: string, method = "GET", body?: unknown) => getToken(url, method, (e) => finalizeEvent(e, sk), true, body as object | undefined);1718// rpc calls the NIP-86 management API as the given key, or unsigned.19export async function rpc(host: string, sk: Uint8Array | null, method: string, ...params: unknown[]) {20 const url = `http://${host}/`;21 const payload = { method, params };22 const headers: Record<string, string> = { "content-type": "application/nostr+json+rpc" };23 if (sk) headers.authorization = await nip98(sk, url, "POST", payload);24 // Background alarms may own admission between management calls. Retry only25 // this transient refusal; quota, rate and inventory cooldown errors surface.26 for (let attempt = 0; ; attempt++) {27 const resp = await SELF.fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });28 const result = { status: resp.status, ...(await resp.json<any>()) };29 if (result.status !== 429 || result.error !== "restricted: relay operation in progress; retry" || attempt >= 20) return result;30 await sleep(50);31 }32}3334// info reads the NIP-11 document.35export const info = async (host: string) => (await SELF.fetch(`http://${host}/`, { headers: { accept: "application/nostr+json" } })).json<any>();3637// get fetches a path, signed when a key is given.38export async function get(host: string, path: string, sk: Uint8Array | null = null) {39 const url = `http://${host}${path}`;40 const headers: Record<string, string> = {};41 if (sk) headers.authorization = await nip98(sk, url);42 return SELF.fetch(url, { headers });43}4445// post sends a JSON body to a bridge path, signed when a key is given. The46// answer is parsed when there is one.47export async function post(host: string, sk: Uint8Array | null, path: string, body: unknown, headers: Record<string, string> = {}) {48 const url = `http://${host}${path}`;49 headers = { "content-type": "application/json", ...headers };50 if (sk) headers.authorization = await nip98(sk, url, "POST", body);51 const resp = await SELF.fetch(url, { method: "POST", headers, body: JSON.stringify(body) });52 const text = await resp.text();53 return { status: resp.status, body: text ? JSON.parse(text) : null };54}5556// alarm fires the relay's alarm by hand.57export const alarm = (name: string) => runInDurableObject(env.RELAY.getByName(name), async (r: Relay) => r.alarm());5859// drive fires the alarm until no job is running or due, and returns the list.60export async function drive(host: string, owner: Uint8Array): Promise<Job[]> {61 const name = host.split(".")[0];62 for (let i = 0; i < 80; i++) {63 await alarm(name);64 const jobs = (await rpc(host, owner, "listjobs")).result as Job[];65 if (!jobs.some((j) => j.running || (j.nextRun > 0 && j.nextRun <= now()))) return jobs;66 await sleep(25);67 }68 throw new Error("jobs did not settle");69}