clone

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

dependabot/npm_and_yarn/npm-f01a459b0f
bindws / scripts / ops / git-capacity.mjs
git-capacity.mjs 11.5K
1// Disposable GRASP-01 capacity smoke. Provisions one disposable relay, applies2// restrictive limits, proves rejection, raises policy, then proves an3// incremental Git push and clone. A mode-0600 manifest is written before4// network mutation for recovery.5//6// Run: npm run test:git-capacity7// For a local Wrangler relay: CAPACITY_RELAY_URL=http://127.0.0.1:8787 npm run test:git-capacity8// Recover: node scripts/ops/git-capacity.mjs cleanup <manifest.json>9import { execFile } from "node:child_process";10import { chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";11import { homedir, tmpdir } from "node:os";12import { join } from "node:path";13import { randomBytes } from "node:crypto";14import { promisify } from "node:util";15import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools/pure";16import { getToken } from "nostr-tools/nip98";17import { npubEncode } from "nostr-tools/nip19";18import WebSocket from "ws";1920const exec = promisify(execFile);21const blobMiB = Number(process.env.CAPACITY_BLOB_MIB ?? 5);22const commits = Number(process.env.CAPACITY_COMMITS ?? 4);23const largeMiB = Number(process.env.CAPACITY_LARGE_MIB ?? 37);24const suppliedRelayURL = process.env.CAPACITY_RELAY_URL;25const timeoutMs = 30_000;26if (!Number.isInteger(blobMiB) || blobMiB < 5 || blobMiB > 32) throw new Error("CAPACITY_BLOB_MIB must be 5..32");27if (!Number.isInteger(commits) || commits < 4 || commits > 32) throw new Error("CAPACITY_COMMITS must be 4..32");28if (!Number.isInteger(largeMiB) || largeMiB < 0 || largeMiB > 128) throw new Error("CAPACITY_LARGE_MIB must be 0..128");2930const http = (url) => url.replace(/^ws(s?):\/\//, "http$1://").replace(/\/$/, "");31const ws = (url) => url.replace(/^http(s?):\/\//, "ws$1://").replace(/\/$/, "");32const now = () => Math.floor(Date.now() / 1000);33const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));3435async function rpc(url, sk, method, ...params) {36 const endpoint = `${http(url)}/`;37 const payload = { method, params };38 const authorization = await getToken(endpoint, "POST", (event) => finalizeEvent(event, sk), true, payload);39 const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/nostr+json+rpc", authorization }, body: JSON.stringify(payload), signal: AbortSignal.timeout(120_000) });40 const body = await response.json();41 if (!response.ok || body.error || body.result?.error) throw new Error(String(body.error ?? body.result?.error ?? `RPC ${response.status}`));42 return body.result;43}4445async function saveManifest(path, value) {46 const temporary = `${path}.tmp`;47 await writeFile(temporary, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 });48 await chmod(temporary, 0o600);49 await rename(temporary, path);50}5152async function git(cwd, ...args) {53 const result = await exec("git", args, { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, maxBuffer: 1024 * 1024, timeout: 300_000 });54 return result.stdout.trim();55}5657const repositoryPath = (npub, identifier) => `/${npub}/${encodeURIComponent(identifier)}.git`;58const event = (sk, kind, tags, content = "", createdAt = now()) => finalizeEvent({ kind, tags, content, created_at: createdAt }, sk);5960async function publish(url, sk, value) {61 const socket = new WebSocket(ws(url));62 await new Promise((resolve, reject) => { socket.once("open", resolve); socket.once("error", reject); });63 try {64 const result = await new Promise((resolve, reject) => {65 const timer = setTimeout(() => reject(new Error("timed out publishing event")), timeoutMs);66 socket.on("message", (raw) => { const message = JSON.parse(raw.toString()); if (message[0] === "OK" && message[1] === value.id) { clearTimeout(timer); resolve(message); } });67 socket.send(JSON.stringify(["EVENT", value]));68 });69 if (!result[2]) throw new Error(`event rejected: ${result[3] ?? "unknown relay error"}`);70 } finally { socket.close(); }71}7273async function waitForRepository(url) {74 for (let attempt = 0; attempt < 60; attempt++) {75 const response = await fetch(`${url}/info/refs?service=git-upload-pack`);76 const text = await response.text();77 if (response.status === 200 && text.includes("# service=git-upload-pack")) return;78 await sleep(250);79 }80 throw new Error("repository did not become available");81}8283async function assertNoMainRef(url) {84 const response = await fetch(`${url}/info/refs?service=git-upload-pack`);85 if (/[0-9a-f]{40} refs\/heads\/main(?:\x00|\n)/.test(await response.text())) throw new Error("rejected push advanced refs");86}8788async function announce(url, sk, repoURL, identifier, tip, createdAt) {89 await publish(url, sk, event(sk, 30617, [["d", identifier], ["clone", repoURL], ["relays", ws(url)], ["maintainers", getPublicKey(sk)]], "", createdAt));90 await publish(url, sk, event(sk, 30618, [["d", identifier], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", tip]], "", createdAt + 1));91}9293async function createHistory(root) {94 await git(root, "init", "--initial-branch=main");95 await git(root, "config", "user.email", "capacity-smoke@example.invalid");96 await git(root, "config", "user.name", "bind.ws capacity smoke");97 for (let index = 1; index <= commits; index++) {98 await writeFile(`${root}/payload-${index}.bin`, randomBytes(blobMiB * 1024 * 1024));99 await git(root, "add", ".");100 await git(root, "commit", "-m", `capacity fixture ${index}`);101 }102 if (largeMiB) {103 await writeFile(`${root}/large-payload.bin`, randomBytes(largeMiB * 1024 * 1024));104 await git(root, "add", ".");105 await git(root, "commit", "-m", `capacity fixture ${largeMiB} MiB object`);106 }107}108109async function pushHistory(root, repoURL, url, sk, identifier, createdAt, stopAfter) {110 const tips = (await git(root, "rev-list", "--reverse", "HEAD")).split("\n").filter(Boolean);111 for (const [index, tip] of tips.entries()) {112 if (index >= stopAfter) break;113 await publish(url, sk, event(sk, 30618, [["d", identifier], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", tip]], "", createdAt + 2 + index));114 await git(root, "push", repoURL, `${tip}:refs/heads/main`);115 }116}117118const LOW_GIT = { maxRepositories: 16, maxRelayBytes: 320 * 1024 * 1024, maxPackBytes: 4 * 1024 * 1024, maxObjectBytes: 4 * 1024 * 1024, maxObjects: 4096, maxRawBytes: 16 * 1024 * 1024, maxTransactionRawBytes: 16 * 1024 * 1024, maxTransactionObjects: 4096, maxCompressedBytes: 16 * 1024 * 1024, maxMetadataBytes: 16 * 1024 * 1024, maxRefs: 1024, maxGraphEdges: 65536, maxFetchBytes: 16 * 1024 * 1024 };119const HIGH_GIT = { maxRepositories: 128, maxRelayBytes: 6 * 1024 * 1024 * 1024, maxPackBytes: 95 * 1024 * 1024, maxObjectBytes: 64 * 1024 * 1024, maxObjects: 100000, maxRawBytes: 64 * 1024 * 1024 * 1024, maxTransactionRawBytes: 64 * 1024 * 1024, maxTransactionObjects: 20000, maxCompressedBytes: 6 * 1024 * 1024 * 1024, maxMetadataBytes: 1024 * 1024 * 1024, maxRefs: 4096, maxGraphEdges: 1000000, maxFetchBytes: 6 * 1024 * 1024 * 1024 };120function assertGitPolicy(policy, expected, label) {121 for (const [key, value] of Object.entries(expected)) if (policy?.git?.[key] !== value) throw new Error(`${label} policy did not apply ${key}`);122}123124async function cleanupManifest(path) {125 const manifest = JSON.parse(await readFile(path, "utf8"));126 if (!manifest?.url || !manifest?.secret || !manifest?.slug || !/^(?:[a-z0-9-]+\.bind\.ws|localhost|127\.0\.0\.1)$/.test(new URL(manifest.url).hostname)) throw new Error("invalid capacity manifest");127 const sk = Uint8Array.from(Buffer.from(manifest.secret, "hex"));128 const policy = await rpc(manifest.url, sk, "getpolicy");129 if (policy.owner !== getPublicKey(sk)) throw new Error("capacity manifest owner mismatch");130 const result = await rpc(manifest.url, sk, "deleterelay", manifest.slug);131 if (result?.deleted !== true) throw new Error("relay deletion was not acknowledged");132 await rm(path, { force: true });133 console.log("capacity relay deleted");134}135136async function main() {137 if (process.argv[2] === "cleanup") {138 if (!process.argv[3] || process.argv[4]) throw new Error("usage: git-capacity.mjs cleanup <manifest.json>");139 return cleanupManifest(process.argv[3]);140 }141 if (process.argv[2]) throw new Error("usage: git-capacity.mjs [cleanup <manifest.json>]");142 const root = await mkdtemp(`${tmpdir()}/bindws-capacity-root-`);143 const clone = await mkdtemp(`${tmpdir()}/bindws-capacity-clone-`);144 const sk = generateSecretKey();145 const owner = npubEncode(getPublicKey(sk));146 const identifier = `capacity-${randomBytes(6).toString("hex")}`;147 const slug = suppliedRelayURL ? (process.env.CAPACITY_RELAY_SLUG ?? "dev-capacity") : `capacity-${randomBytes(6).toString("hex")}`;148 const url = suppliedRelayURL ?? `https://${slug}.bind.ws`;149 const manifestRoot = join(homedir(), ".local", "share", "bindws", "git-capacity-tests", slug);150 const manifestPath = join(manifestRoot, "manifest.json");151 await mkdir(manifestRoot, { recursive: true, mode: 0o700 });152 const manifest = { version: 1, slug, url, secret: Buffer.from(sk).toString("hex"), identifier, state: "planned" };153 await saveManifest(manifestPath, manifest);154 const repoURL = `${http(url)}${repositoryPath(owner, identifier)}`;155 const createdAt = now();156 try {157 const claimed = await rpc(url, sk, "claim");158 if (!claimed?.claimed || claimed.owner !== getPublicKey(sk)) throw new Error("capacity relay claim failed");159 manifest.state = "claimed";160 await saveManifest(manifestPath, manifest);161 await rpc(url, sk, "setpolicy", { reads: "open", features: { grasp: true }, git: LOW_GIT });162 assertGitPolicy(await rpc(url, sk, "getpolicy"), LOW_GIT, "restrictive");163 await createHistory(root);164 const tips = (await git(root, "rev-list", "--reverse", "HEAD")).split("\n").filter(Boolean);165 await announce(url, sk, repoURL, identifier, tips[0], createdAt);166 await waitForRepository(repoURL);167 let rejected = false;168 try { await pushHistory(root, repoURL, url, sk, identifier, createdAt, 1); } catch { rejected = true; }169 if (!rejected) throw new Error("restrictive policy accepted the over-capacity push");170 await assertNoMainRef(repoURL);171 await rpc(url, sk, "setpolicy", { git: HIGH_GIT });172 assertGitPolicy(await rpc(url, sk, "getpolicy"), HIGH_GIT, "raised");173 await pushHistory(root, repoURL, url, sk, identifier, createdAt, commits + (largeMiB ? 1 : 0));174 await waitForRepository(repoURL);175 await git(clone, "clone", repoURL, ".");176 await git(clone, "fsck", "--full");177 await git(clone, "bundle", "create", "capacity-backup.bundle", "--all");178 await git(clone, "bundle", "verify", "capacity-backup.bundle");179 const expected = await git(root, "rev-parse", "HEAD");180 if (await git(clone, "rev-parse", "HEAD") !== expected) throw new Error("raised-policy clone did not reach expected tip");181 const sample = await readFile(`${clone}/${largeMiB ? "large-payload.bin" : `payload-${commits}.bin`}`);182 const expectedBytes = (largeMiB || blobMiB) * 1024 * 1024;183 if (sample.length !== expectedBytes) throw new Error("raised-policy clone has truncated content");184 console.log(`PASS: restrictive policy rejected >${blobMiB} MiB; raised policy stored ${commits} x ${blobMiB} MiB${largeMiB ? ` plus ${largeMiB} MiB` : ""} and cloned cleanly`);185 } finally {186 try {187 await cleanupManifest(manifestPath);188 await rm(manifestRoot, { recursive: true, force: true });189 } catch (error) {190 console.error(`Cleanup requires recovery using ${manifestPath}: ${error.message}`);191 process.exitCode = 1;192 }193 await Promise.all([rm(root, { recursive: true, force: true }), rm(clone, { recursive: true, force: true })]);194 }195}196197main().catch((error) => { console.error(`FAIL: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; });