clone

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

ops: relay.mjs checks, plans, pushes or pulls a relay's configuration file, with the plan being importconfig's dry run; the guide for scripts and agents describes the file, its schema and the four verbs

Dami committed 3d315de
commit
3d315deb586e18f601016fb5e1085e8ab4a4a02e
parent
dc6f3869478379e5bc35721f62d544c5a08ba655
author
Dami <dami.etoile@gmail.com>
3 files changed, +97 -0
Mdocs/12-develop-extend.md1+
Mdocs/13-scripts-and-agents.md17++++++
Ascripts/ops/relay.mjs79++++++++++++++++++++++++++++++
M docs/12-develop-extend.md +1 -0
M docs/13-scripts-and-agents.md +17 -0
@@ -164,6 +164,23 @@ The bridge takes the same header. `POST /events` answers `{ event_id, accepted,
164164 165165 - `deleterelay name`: the relay's own name, typed, is the confirmation.166166 167+## The configuration file168+169+`exportconfig` is a document, and a document can live in a repository. The format is `bind.ws/relay-config/2`: `policy`, `members`, `bans`, `addresses`, `banned_events`, `kinds` and `retention`, each a section, and a section the file leaves out is left alone when it is applied, so a file may carry the rules and nothing about people. The schema is at `https://bind.ws/relay-config.schema.json`; put it in `$schema` and the editor checks the file as you type. JSONC is fine: comments and trailing commas.170+171+Four verbs, in `scripts/ops/relay.mjs`, with `RELAY_SK` holding the owner's key as hex or nsec:172+173+```174+node scripts/ops/relay.mjs check relay.jsonc # the schema and the relay's own parser, offline175+node scripts/ops/relay.mjs plan relay.jsonc wss://name.bind.ws # what applying would change; touches nothing176+node scripts/ops/relay.mjs push relay.jsonc wss://name.bind.ws # the plan, then the change177+node scripts/ops/relay.mjs pull wss://name.bind.ws relay.json # the relay as it is, into a file178+```179+180+`check` fails on anything a relay would drop, since a silently dropped entry is a mistake in a file under source control. `plan` is `importconfig` with `{ dryRun: true }`: the answer lists the changes section by section with a `summary` line each, plus the entries the relay would not take. `pull` then `git diff` shows drift between the file and the relay.181+182+The templates in the repository's `relay-templates/` folder are files in this format with a `template` block for the title and the blurb; the console offers them as presets. A template is a good start for a file of your own.183+167184 ## Limits worth knowing168185 169186 - Leases: five a minute per address and 60 a minute overall at the apex.
A scripts/ops/relay.mjs +79 -0
@@ -0,0 +1,79 @@
1+// A relay's configuration as a file in your repository: check it, see what2+// applying it would change, apply it, or pull the relay's current one.3+//4+// node scripts/ops/relay.mjs check <file.jsonc>5+// node scripts/ops/relay.mjs plan <file.jsonc> <wss://name.bind.ws>6+// node scripts/ops/relay.mjs push <file.jsonc> <wss://name.bind.ws>7+// node scripts/ops/relay.mjs pull <wss://name.bind.ws> [file.json]8+//9+// plan, push and pull sign as the owner: RELAY_SK holds the secret key, hex10+// or nsec. check needs no relay. plan is importconfig with dryRun, so it11+// touches nothing; push prints the same plan and then applies it. The12+// schema the file may reference is at https://bind.ws/relay-config.schema.json.13+import { writeFileSync } from "node:fs";14+import { finalizeEvent, getPublicKey } from "nostr-tools/pure";15+import { getToken } from "nostr-tools/nip98";16+import { decode } from "nostr-tools/nip19";17+import { checkConfig, describe, readConfig } from "../check/config.mjs";18+19+const [cmd, a, b] = process.argv.slice(2);20+const usage = () => {21+ console.error("usage: relay.mjs check <file> | plan <file> <url> | push <file> <url> | pull <url> [file]");22+ process.exit(2);23+};24+25+function key() {26+ const raw = process.env.RELAY_SK ?? "";27+ if (/^[0-9a-f]{64}$/i.test(raw)) return Uint8Array.from(Buffer.from(raw, "hex"));28+ if (raw.startsWith("nsec")) return decode(raw).data;29+ console.error("RELAY_SK must be the owner's secret key, hex or nsec");30+ process.exit(2);31+}32+33+const httpURL = (url) => new URL(url.replace(/^ws(s?):\/\//, "http$1://")).origin + "/";34+35+async function rpc(url, sk, method, ...params) {36+ const payload = { method, params };37+ const body = JSON.stringify(payload);38+ const token = await getToken(url, "POST", (e) => finalizeEvent(e, sk), true, payload);39+ const resp = await fetch(url, { method: "POST", headers: { "content-type": "application/nostr+json+rpc", authorization: token }, body });40+ const json = await resp.json();41+ if (json.error) throw new Error(json.error);42+ return json.result;43+}44+45+function printPlan(plan) {46+ const { changes, warnings } = plan;47+ console.log(changes.summary.length ? changes.summary.map((l) => " " + l).join("\n") : " nothing would change");48+ if (warnings.length) console.log("not taken:\n" + warnings.map((w) => " " + w).join("\n"));49+}50+51+if (cmd === "check" && a) {52+ const cfg = await checkConfig(readConfig(a), a);53+ console.log(`${a}: ok\n ${describe(cfg).join("\n ")}`);54+} else if ((cmd === "plan" || cmd === "push") && a && b) {55+ const doc = readConfig(a);56+ await checkConfig(doc, a);57+ const url = httpURL(b);58+ const sk = key();59+ console.log(`${cmd === "plan" ? "would apply" : "applying"} ${a} to ${url} as ${getPublicKey(sk).slice(0, 8)}:`);60+ const plan = await rpc(url, sk, "importconfig", doc, { dryRun: true });61+ printPlan(plan);62+ if (cmd === "push") {63+ if (plan.changes.summary.length === 0) console.log("nothing to apply");64+ else {65+ await rpc(url, sk, "importconfig", doc);66+ console.log("applied");67+ }68+ }69+} else if (cmd === "pull" && a) {70+ const url = httpURL(a);71+ const sk = key();72+ const cfg = await rpc(url, sk, "exportconfig");73+ delete cfg.exported_at; // the file is what the relay says, not when74+ const text = JSON.stringify({ $schema: "https://bind.ws/relay-config.schema.json", ...cfg }, null, 2) + "\n";75+ if (b) {76+ writeFileSync(b, text);77+ console.log(`${b}: ${cfg.name} pulled from ${url}`);78+ } else process.stdout.write(text);79+} else usage();