tinyrelaycommit

Commit 9a365ae16763

browse files at this commit | history

1diff --git a/docs/personal-relay.md b/docs/personal-relay.md
2index 4b0bf1b..74ac27a 100644
3--- a/docs/personal-relay.md
4+++ b/docs/personal-relay.md
5@@ -52,15 +52,17 @@ The owner is `npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5`.
6
7 ## Repository hosting
8
9-The relay hosts Git repositories over GRASP. Publishing needs the owner's signature on the repository announcement, on every ref state, and on each push, so pushes go through `scripts/publish-repository.mjs` rather than plain `git push`. The script signs with a bunker URL from Amber or with a local key and never sends a private key to the relay.
10+The relay hosts Git repositories over GRASP. Publishing needs the owner's signature on the announcement, on every ref state, and on each push, so use `nak git` with a remote signer rather than plain `git push`. Amber shows a bunker QR code under Applications; scan it into the shell with the webcam so the key stays on the phone:
11
12 ```sh
13-git remote add tiny https://tiny.tailbe516a.ts.net/<owner npub>/tinyrelay.git
14-node scripts/publish-repository.mjs --remote tiny --sec bunker://... --name tiny --description "self-hosted Nostr relay"
15-node scripts/publish-repository.mjs --remote tiny --sec bunker://... --state-only
16+export NOSTR_SECRET_KEY="$(imagesnap -q -w 2 /tmp/amber.jpg && zbarimg -q --raw /tmp/amber.jpg)"
17+export NOSTR_CLIENT_KEY="$(nak key generate)"
18+nak git init --identifier tinyrelay --name tiny --owner <owner npub> \
19+ --grasp-servers tiny.tailbe516a.ts.net --relays wss://tiny.tailbe516a.ts.net
20+nak git push --tags
21 ```
22
23-Run the first form once to announce the repository. Run the second after new commits: it publishes the signed state for the local branches and tags, pushes them, and waits until the relay advertises the new refs. Anyone can clone from the remote URL without signing.
24+Keep the same client key so Amber remembers the approval. Run `nak git push` again after new commits. Anyone can clone from `https://tiny.tailbe516a.ts.net/<owner npub>/tinyrelay.git` without signing.
25
26 ## Web operations
27
28diff --git a/scripts/publish-repository.mjs b/scripts/publish-repository.mjs
29deleted file mode 100644
30index 6f1df35..0000000
31--- a/scripts/publish-repository.mjs
32+++ /dev/null
33@@ -1,129 +0,0 @@
34-#!/usr/bin/env node
35-// Publish the current Git repository to a tiny relay over GRASP-01.
36-//
37-// node scripts/publish-repository.mjs --relay wss://relay.example --sec bunker://... [options]
38-//
39-// The script announces the repository (kind 30617), publishes the signed ref
40-// state (kind 30618) from the local branches and tags, then pushes through a
41-// local NIP-98 signing proxy. Run it again after new commits: the relay only
42-// accepts pushes that match the latest signed state.
43-//
44-// Options:
45-// --relay URL relay websocket URL (default: derived from --remote)
46-// --remote NAME git remote whose URL names the relay and identifier
47-// --identifier NAME repository identifier (default: remote path or directory name)
48-// --sec VALUE nsec, hex secret, or bunker:// URL; also NOSTR_SECRET_KEY
49-// --name TEXT repository display name for the announcement
50-// --description TEXT repository description for the announcement
51-// --web URL web page to advertise
52-// --private announce a private repository
53-// --state-only skip the announcement and only publish state and push
54-// --force force-update relay branches that diverged from local ones
55-import { execFile } from "node:child_process";
56-import { basename } from "node:path";
57-import { promisify } from "node:util";
58-import WebSocket from "ws";
59-import { finalizeEvent, getPublicKey } from "nostr-tools/pure";
60-import { npubEncode, decode } from "nostr-tools/nip19";
61-import { BunkerSigner, parseBunkerInput } from "nostr-tools/nip46";
62-import { generateSecretKey } from "nostr-tools/pure";
63-import { hexToBytes } from "@noble/hashes/utils.js";
64-import { startGitSigningProxy } from "./git-signing-proxy.mjs";
65-
66-globalThis.WebSocket = WebSocket;
67-const exec = promisify(execFile);
68-const args = process.argv.slice(2);
69-const option = (name, fallback) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : fallback; };
70-const flag = name => args.includes(name);
71-const git = async (...argv) => (await exec("git", argv, { env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } })).stdout.trim();
72-
73-async function signerFrom(value) {
74- if (!value) throw Error("Pass --sec with an nsec, hex secret or bunker:// URL, or set NOSTR_SECRET_KEY.");
75- if (value.startsWith("bunker://")) {
76- const pointer = await parseBunkerInput(value);
77- if (!pointer) throw Error("The bunker URL could not be parsed.");
78- const bunker = BunkerSigner.fromBunker(generateSecretKey(), pointer);
79- await bunker.connect();
80- const pubkey = await bunker.getPublicKey();
81- return { pubkey, sign: template => bunker.signEvent(template), close: () => bunker.close() };
82- }
83- const secret = value.startsWith("nsec1") ? decode(value).data : hexToBytes(value);
84- return { pubkey: getPublicKey(secret), sign: async template => finalizeEvent(template, secret), close: async () => {} };
85-}
86-
87-// publish sends one event and waits for OK, answering a NIP-42 AUTH
88-// challenge with the same signer when the relay asks for it.
89-function publish(relayURL, signer, event) {
90- return new Promise((resolve, reject) => {
91- const socket = new WebSocket(relayURL);
92- const timer = setTimeout(() => { socket.close(); reject(Error("relay did not acknowledge " + event.kind)); }, 30_000);
93- const finish = (err) => { clearTimeout(timer); socket.close(); err ? reject(err) : resolve(); };
94- let retried = false;
95- socket.on("open", () => socket.send(JSON.stringify(["EVENT", event])));
96- socket.on("error", finish);
97- socket.on("message", async raw => {
98- const message = JSON.parse(raw);
99- if (message[0] === "AUTH" && !retried) {
100- retried = true;
101- const auth = await signer.sign({ kind: 22242, created_at: Math.floor(Date.now() / 1000), content: "", tags: [["relay", relayURL], ["challenge", message[1]]] });
102- socket.send(JSON.stringify(["AUTH", auth]));
103- socket.send(JSON.stringify(["EVENT", event]));
104- return;
105- }
106- if (message[0] !== "OK" || message[1] !== event.id) return;
107- if (message[2]) finish();
108- else if (String(message[3]).startsWith("auth-required:") && !retried) return;
109- else finish(Error(`relay rejected kind ${event.kind}: ${message[3]}`));
110- });
111- });
112-}
113-
114-async function refsMatch(cloneURL, expected) {
115- const response = await fetch(cloneURL + "/info/refs?service=git-upload-pack");
116- if (!response.ok) return false;
117- const text = await response.text();
118- return expected.every(([ref, oid]) => text.includes(`${oid} ${ref}`));
119-}
120-
121-async function main() {
122- const remoteName = option("--remote");
123- const remoteURL = remoteName ? await git("remote", "get-url", remoteName) : "";
124- const relayURL = option("--relay") || (remoteURL && new URL(remoteURL).origin.replace(/^http/, "ws"));
125- if (!relayURL) throw Error("Pass --relay or --remote.");
126- const httpBase = relayURL.replace(/^ws/, "http").replace(/\/$/, "");
127- const identifier = option("--identifier") || (remoteURL ? decodeURIComponent(basename(new URL(remoteURL).pathname).replace(/\.git$/, "")) : basename(await git("rev-parse", "--show-toplevel")));
128- const signer = await signerFrom(option("--sec", process.env.NOSTR_SECRET_KEY));
129- try {
130- const npub = npubEncode(signer.pubkey);
131- const path = `/${npub}/${encodeURIComponent(identifier)}.git`;
132- const cloneURL = httpBase + path;
133- const head = await git("symbolic-ref", "HEAD");
134- const refs = (await git("for-each-ref", "--format=%(refname) %(objectname)", "refs/heads", "refs/tags")).split("\n").filter(Boolean).map(line => line.split(" "));
135- if (!refs.length) throw Error("The repository has no branches or tags to publish.");
136- const now = Math.floor(Date.now() / 1000);
137-
138- if (!flag("--state-only")) {
139- const tags = [["d", identifier], ["clone", cloneURL], ["relays", relayURL], ["maintainers", signer.pubkey]];
140- for (const name of ["name", "description", "web"]) if (option("--" + name)) tags.push([name, option("--" + name)]);
141- if (flag("--private")) tags.push(["private", "true"]);
142- await publish(relayURL, signer, await signer.sign({ kind: 30617, created_at: now, content: "", tags }));
143- console.log("announced " + identifier);
144- }
145- await publish(relayURL, signer, await signer.sign({ kind: 30618, created_at: now + 1, content: "", tags: [["d", identifier], ["HEAD", "ref: " + head], ...refs] }));
146- console.log(`published state for ${refs.length} refs, HEAD ${head}`);
147-
148- const proxy = await startGitSigningProxy(httpBase, signer.sign);
149- try {
150- await exec("git", ["push", ...(flag("--force") ? ["--force"] : []), proxy.url + path, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"], { stdio: "inherit", maxBuffer: 1 << 26 });
151- } finally {
152- await proxy.close();
153- }
154- for (let attempt = 0; attempt < 40 && !(await refsMatch(cloneURL, refs)); attempt++) await new Promise(r => setTimeout(r, 500));
155- if (!(await refsMatch(cloneURL, refs))) throw Error("the relay has not advertised the pushed refs yet");
156- console.log("clone URL: " + cloneURL);
157- } finally {
158- await signer.close();
159- }
160-}
161-
162-main().then(() => process.exit(0), err => { console.error(err.message); process.exit(1); });

About

ownerc01503e8035f
visibilitypublic
head9a365ae16763

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs