tinyrelayfile
tinyrelay/scripts/conformance/grasp08.test.ts/

scripts/conformance/grasp08.test.ts raw
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, now } from "./helpers.ts";
14
15const 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"));
19
20async 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}
27
28function 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}
32
33async 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}
45
46async 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}
55
56describe("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");
66
67 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 c = await Client.connect(RELAY_URL);
76 try {
77 await c.auth(OWNER_SK, RELAY_URL);
78 await git(root, "init", "--initial-branch=main");
79 await git(root, "config", "user.email", "grasp08-conformance@example.com");
80 await git(root, "config", "user.name", "GRASP-08 conformance");
81 await writeFile(`${root}/README.md`, "private GRASP-08 repository\n");
82 await git(root, "add", "README.md");
83 await git(root, "commit", "-m", "initial");
84 const first = await git(root, "rev-parse", "HEAD");
85 const createdAt = now();
86 await c.publish(finalizeEvent({ kind: KIND_REPO, content: "", created_at: createdAt, tags: [["d", identifier], ["clone", repoURL], ["relays", RELAY_URL], ["maintainers", pubkey], ["private", "true"]] }, sk));
87 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));
88 const header = `Authorization: ${proof}`;
89 await waitForRefs(repoURL, proof);
90 await git(root, "-c", `http.extraHeader=${header}`, "push", repoURL, "HEAD:refs/heads/main");
91 await git(clone, "-c", `http.extraHeader=${header}`, "clone", repoURL, ".");
92 expect(await git(clone, "rev-parse", "HEAD")).toBe(first);
93
94 await writeFile(`${root}/README.md`, "private GRASP-08 incremental repository\n");
95 await git(root, "add", "README.md");
96 await git(root, "commit", "-m", "incremental");
97 const second = await git(root, "rev-parse", "HEAD");
98 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));
99 await waitForRefs(repoURL, proof);
100 await git(root, "-c", `http.extraHeader=${header}`, "push", repoURL, "HEAD:refs/heads/main");
101 await git(clone, "-c", `http.extraHeader=${header}`, "fetch", repoURL, "refs/heads/main:refs/remotes/origin/main");
102 expect(await git(clone, "rev-parse", "refs/remotes/origin/main")).toBe(second);
103 } finally {
104 c.close();
105 await Promise.all([rm(root, { recursive: true, force: true }), rm(clone, { recursive: true, force: true })]);
106 }
107 }, 120_000);
108});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs