clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / scripts / conformance / grasp08.test.ts
grasp08.test.ts 6.3K
1// GRASP-08 interoperability uses stock Git Smart HTTP with one reusable,2// repository-root NIP-98 proof shared through git's extraHeader option.3import { execFile } from "node:child_process";4import { mkdtemp, rm, writeFile } from "node:fs/promises";5import { tmpdir } from "node:os";6import { promisify } from "node:util";7import { describe, expect, it } from "vitest";8import { finalizeEvent, getPublicKey } from "nostr-tools/pure";9import { npubEncode } from "nostr-tools/nip19";10import { getToken } from "nostr-tools/nip98";11import { sha256 } from "@noble/hashes/sha2.js";12import { hexToBytes } from "@noble/hashes/utils.js";13import { Client, HTTP_URL, RELAY_URL, newKey, now } from "./helpers.ts";1415const exec = promisify(execFile);16const KIND_REPO = 30617;17const KIND_REPO_STATE = 30618;18const OWNER_SK = process.env.CLAIM_SK ? hexToBytes(process.env.CLAIM_SK) : sha256(new TextEncoder().encode("tiny conformance owner"));1920async function git(cwd: string, ...args: string[]): Promise<string> {21 const result = await exec("git", args, {22 cwd,23 env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },24 });25 return result.stdout.trim();26}2728function auth(sk: Uint8Array, root: string): string {29 const event = finalizeEvent({ kind: 27235, created_at: now(), content: "", tags: [["u", root], ["method", "GET"]] }, sk);30 return `Nostr ${Buffer.from(JSON.stringify(event)).toString("base64")}`;31}3233async function rpc(method: string, params: unknown[] = []): Promise<any> {34 const payload = { method, params };35 const token = await getToken(HTTP_URL, "POST", event => finalizeEvent(event, OWNER_SK), true, payload);36 const response = await fetch(HTTP_URL, {37 method: "POST",38 headers: { "Content-Type": "application/nostr+json+rpc", Authorization: token },39 body: JSON.stringify(payload),40 });41 const result: any = await response.json();42 if (!response.ok || result.error) throw new Error(JSON.stringify(result));43 return result.result;44}4546async function waitForRefs(url: string, authorization: string): Promise<void> {47 for (let attempt = 0; attempt < 60; attempt++) {48 const response = await fetch(`${url}/info/refs?service=git-upload-pack`, { headers: { authorization } });49 const text = await response.text();50 if (response.status === 200 && text.includes("# service=git-upload-pack")) return;51 await new Promise(resolve => setTimeout(resolve, 100));52 }53 throw new Error("GRASP-08 repository did not become available");54}5556describe("GRASP-08 stock Git", () => {57 it("reuses one repository-root proof for clone, fetch and push", async ({ skip }) => {58 if (process.env.TINY_GRASP08 !== "1") skip();59 const policy = await rpc("getpolicy");60 policy.features = { ...policy.features, grasp: true, grasp08: true };61 policy.reads = "members";62 await rpc("setpolicy", [policy]);63 const infoResponse = await fetch(HTTP_URL, { headers: { accept: "application/nostr+json" } });64 const info: any = await infoResponse.json();65 expect(info.supported_grasps).toContain("GRASP-08");6667 const root = await mkdtemp(`${tmpdir()}/tiny-grasp08-`);68 const clone = await mkdtemp(`${tmpdir()}/tiny-grasp08-clone-`);69 const sk = OWNER_SK;70 const pubkey = getPublicKey(sk);71 const npub = npubEncode(pubkey);72 const identifier = `stock-${Math.random().toString(36).slice(2, 10)}`;73 const repoURL = `${HTTP_URL}/${npub}/${encodeURIComponent(identifier)}.git`;74 const proof = auth(sk, repoURL);75 const outsiderProof = auth(newKey(), repoURL);76 for (const service of ["git-upload-pack", "git-receive-pack"]) {77 const endpoint = `${repoURL}/info/refs?service=${service}`;78 const anonymous = await fetch(endpoint);79 expect(anonymous.status).toBe(401);80 expect(await anonymous.text()).toBe("");81 expect(anonymous.headers.get("www-authenticate")).toBe(`Nostr method="GET"`);82 const outsider = await fetch(endpoint, { headers: { authorization: outsiderProof } });83 expect(outsider.status).toBe(401);84 expect(await outsider.text()).toBe("");85 expect(outsider.headers.get("www-authenticate")).toBe(`Nostr method="GET"`);86 }87 const c = await Client.connect(RELAY_URL);88 try {89 await c.auth(OWNER_SK, RELAY_URL);90 await git(root, "init", "--initial-branch=main");91 await git(root, "config", "user.email", "grasp08-conformance@example.com");92 await git(root, "config", "user.name", "GRASP-08 conformance");93 await writeFile(`${root}/README.md`, "private GRASP-08 repository\n");94 await git(root, "add", "README.md");95 await git(root, "commit", "-m", "initial");96 const first = await git(root, "rev-parse", "HEAD");97 const createdAt = now();98 await c.publish(finalizeEvent({ kind: KIND_REPO, content: "", created_at: createdAt, tags: [["d", identifier], ["clone", repoURL], ["relays", RELAY_URL], ["maintainers", pubkey], ["private", "true"]] }, sk));99 await c.publish(finalizeEvent({ kind: KIND_REPO_STATE, content: "", created_at: createdAt + 1, tags: [["d", identifier], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", first]] }, sk));100 const header = `Authorization: ${proof}`;101 await waitForRefs(repoURL, proof);102 await git(root, "-c", `http.extraHeader=${header}`, "push", repoURL, "HEAD:refs/heads/main");103 await git(clone, "-c", `http.extraHeader=${header}`, "clone", repoURL, ".");104 expect(await git(clone, "rev-parse", "HEAD")).toBe(first);105106 await writeFile(`${root}/README.md`, "private GRASP-08 incremental repository\n");107 await git(root, "add", "README.md");108 await git(root, "commit", "-m", "incremental");109 const second = await git(root, "rev-parse", "HEAD");110 await c.publish(finalizeEvent({ kind: KIND_REPO_STATE, content: "", created_at: createdAt + 2, tags: [["d", identifier], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", second]] }, sk));111 await waitForRefs(repoURL, proof);112 await git(root, "-c", `http.extraHeader=${header}`, "push", repoURL, "HEAD:refs/heads/main");113 await git(clone, "-c", `http.extraHeader=${header}`, "fetch", repoURL, "refs/heads/main:refs/remotes/origin/main");114 expect(await git(clone, "rev-parse", "refs/remotes/origin/main")).toBe(second);115 } finally {116 c.close();117 await Promise.all([rm(root, { recursive: true, force: true }), rm(clone, { recursive: true, force: true })]);118 }119 }, 120_000);120});