mycomesh-relay 0.1.7 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -26
- package/bin/mycomesh-relay.mjs +7 -0
- package/networks/mycomesh-testnet-ca.crt +25 -0
- package/networks/mycomesh-v11-sepolia.json +45 -0
- package/package.json +12 -6
- package/src/cli.mjs +176 -0
- package/bin/mycomesh-relay +0 -5
- package/src/relay.mjs +0 -255
package/README.md
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
# mycomesh-relay
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Install
|
|
3
|
+
Run an independent MycoMesh V11 Relay. Like a Bitcoin node, it needs no domain
|
|
4
|
+
name and no certificate authority: its self-signed TLS certificate is pinned in
|
|
5
|
+
the on-chain Relay directory, and every Consumer and Provider checks the pin.
|
|
6
|
+
Requires Node.js 20+, Docker, and ports 10443 and 10991 open.
|
|
8
7
|
|
|
9
8
|
```sh
|
|
10
9
|
npm install --global mycomesh-relay
|
|
11
|
-
mycomesh-relay
|
|
10
|
+
mycomesh-relay init # owner + signer keys, self-signed certificate for this machine's public IP
|
|
11
|
+
mycomesh-relay register # bind the signer, fund probes, announce the pinned endpoints on-chain
|
|
12
|
+
mycomesh-relay start --with-keeper
|
|
13
|
+
mycomesh-relay status
|
|
14
|
+
mycomesh-relay earnings # the Relay's share of every fee it dispatched
|
|
12
15
|
```
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`--no-browser` and open the printed loopback URL yourself.
|
|
18
|
-
|
|
19
|
-
```sh
|
|
20
|
-
mycomesh-relay --ref <reviewed-commit> --no-browser
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Docker Engine, Docker Compose V2 and GNU Make are still required. The npm
|
|
24
|
-
package is only the onboarding launcher; the Relay remains Docker-backed.
|
|
25
|
-
|
|
26
|
-
Settlement configuration is deliberately separate. Put the protected Relay
|
|
27
|
-
transaction signer and the public contract/RPC settings in the checkout's
|
|
28
|
-
`.env.deploy` according to the operator documentation. The public payout
|
|
29
|
-
address entered in the wizard is not the gas-funded transaction identity.
|
|
30
|
-
|
|
31
|
-
Use `--no-start` to download a checkout without starting it, and
|
|
32
|
-
`--source-dir /srv/mycomesh` to choose a persistent location. Keep the checkout
|
|
33
|
-
and named Docker volumes for upgrades; do not run `docker compose down -v`.
|
|
17
|
+
Once announced, Providers join the Relay automatically and Consumers discover it
|
|
18
|
+
from the chain. The Relay probes its Providers with free known-answer requests,
|
|
19
|
+
records re-gradable verdicts in the probe ledger, and helps run Provider-AI juries.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIEKTCCApGgAwIBAgIUDwYNmg71zXD2ycqGMXfAmTtb9E8wDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwKzEpMCcGA1UEAwwgTXljb01lc2ggSVAgdGVzdG5ldCBkZWRpY2F0ZWQgQ0Ew
|
|
4
|
+
HhcNMjYwOTE1MDM0NjQ3WhcNMjcwOTE1MDM1NjQ3WjArMSkwJwYDVQQDDCBNeWNv
|
|
5
|
+
TWVzaCBJUCB0ZXN0bmV0IGRlZGljYXRlZCBDQTCCAaIwDQYJKoZIhvcNAQEBBQAD
|
|
6
|
+
ggGPADCCAYoCggGBAKSXx/8p203JtQSav+7wt7xYfWURFe0DQODb3XNIc2YFe7j4
|
|
7
|
+
HDEiL47sDpBd4JY9NELjzA47vIRkRdLlfit5JKWy1+ozzlJ6G2qs9o3vFK+Xho8K
|
|
8
|
+
8D8Q6hXCUaaKPFAg6vONH0OHnCMaWmq+3gKzdgId1ci4QopjAVvaxUj/CcpFVl1E
|
|
9
|
+
JdGjVhj+SVWE/fKb5Ullbw6UIhjpAbH4BmsOHJLfPJuw94lFWHgCJgcMWF8qxeTU
|
|
10
|
+
fL4Kwp6hUAgH0BOiD4NsNTn4lIFnqwzb+Cu6vqSdd/poTOMvKj7sjl+tZdphtCB5
|
|
11
|
+
DOZr066mAKlY91goM1BUD2y8ozBiDDUmJSeGp3eUy8Iuch2A9sEupDpt0z/cvs2W
|
|
12
|
+
BXqJdbvVEMY+TFVRPkSmG2FfnWvzoHx5QD7o4dYLIrN/nD5y44NypAdpR1n1IHs0
|
|
13
|
+
8EF1Lae80FHM3Jwt3mLqnhAvwKqMfAr71gFxolbLhO/O2RWd11UuBIy/ORsXFUGB
|
|
14
|
+
d/JurXmMC9kajoeg4wIDAQABo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1Ud
|
|
15
|
+
DwEB/wQEAwIBBjAdBgNVHQ4EFgQUr/zADf47qEsS1yVwDgx6CvjN/5kwDQYJKoZI
|
|
16
|
+
hvcNAQELBQADggGBAHnJExIh4oxx8xkLsNVKb6hQyM6814wP6gUmLLUAZXfu6LV1
|
|
17
|
+
PoiMoeUWLd02JocyGLJnVmBgUFJHab16GUEwldYNBvnaeycrpQXP/Bu9Kl6Tr2Ku
|
|
18
|
+
Efv8veCY+tj6GWgv/HkRGrvdn6LPhVE84zvrkLthwn262FxPqeb1dt/RkgrDBPKL
|
|
19
|
+
YEN2QG8uyo10YSQm7jgyl22f/QrLB3jEO3lhVqzu0lKz6TZrv1ikvl1gtAV3rEfW
|
|
20
|
+
vszRlgkJqcFV/2cZmEN6iflq3LZNlnUWxgtLaMpCGlPRnQ9sLbPTFfqMHOyeWaCS
|
|
21
|
+
8odcemTNIJSmiyv/3HTjcmqDt9k1UGU8PwOMeKhHe/0hbgtqAxLajiz0VFDAo9MS
|
|
22
|
+
IH5wUcg3vY6LEV0TzkrQxobo2sAFArkJpoDafYRoZu0vnAfkQndQP66l9KzL33EP
|
|
23
|
+
PtV09Z9vNbjGC0NpJpop2J0J69VdDk9wHHJOQHYDPSxbL8eQKcQwtkOBCq0N1u6c
|
|
24
|
+
RQc4USIGi/decqtfJA==
|
|
25
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "mycomesh.v11.network.v1",
|
|
3
|
+
"network_id": "mycomesh-v11-sepolia",
|
|
4
|
+
"chain_id": 11155111,
|
|
5
|
+
"settlement": "0xd46b7efb1e650e83cdeb4a36fdfef33499e54842",
|
|
6
|
+
"stablecoin": "0xbce27efad4191277167fe76819581a97959048cf",
|
|
7
|
+
"registry": "0x0ff3210e8e8abf5f5bfa73a39e60db7416b6c8c7",
|
|
8
|
+
"relay_directory": "0xa0aaad9393f51cb5786c69f152133a174588bdab",
|
|
9
|
+
"probe_ledger": "0x30055eae6645cce8754f834a17689d20a3795ca3",
|
|
10
|
+
"tiers": {
|
|
11
|
+
"1": {
|
|
12
|
+
"name": "OpenAI frontier",
|
|
13
|
+
"models": [
|
|
14
|
+
"gpt-5.5"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"2": {
|
|
18
|
+
"name": "Anthropic Claude",
|
|
19
|
+
"models": [
|
|
20
|
+
"claude-sonnet-4-6",
|
|
21
|
+
"claude-opus-4-8"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"deployment_block": 11809185,
|
|
26
|
+
"rpc_urls": [
|
|
27
|
+
"https://ethereum-sepolia-rpc.publicnode.com",
|
|
28
|
+
"https://rpc.sepolia.ethpandaops.io",
|
|
29
|
+
"https://sepolia.gateway.tenderly.co"
|
|
30
|
+
],
|
|
31
|
+
"tls_ca_file": "mycomesh-testnet-ca.crt",
|
|
32
|
+
"faucet_url": "https://136.0.3.126:10443",
|
|
33
|
+
"relays": [
|
|
34
|
+
{
|
|
35
|
+
"url": "https://136.0.3.126:10443",
|
|
36
|
+
"signer": "0x58fb2f217f3065d67cdde9d5231aa9388cf72297",
|
|
37
|
+
"link": "136.0.3.126:10991"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"url": "https://166.88.96.60:10443",
|
|
41
|
+
"signer": "0x2211e0288ef46a7b4ea03a5022f4a96f146d8c9d",
|
|
42
|
+
"link": "166.88.96.60:10991"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mycomesh-relay",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "MycoMesh Relay
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Run an independent MycoMesh V11 Relay in Docker: no domain, no certificate authority, discoverable on-chain.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Charleslzp/mycomesh"
|
|
8
|
+
},
|
|
5
9
|
"publishConfig": {
|
|
6
10
|
"access": "public"
|
|
7
11
|
},
|
|
8
12
|
"type": "module",
|
|
9
13
|
"bin": {
|
|
10
|
-
"mycomesh-relay": "bin/mycomesh-relay"
|
|
14
|
+
"mycomesh-relay": "bin/mycomesh-relay.mjs"
|
|
11
15
|
},
|
|
12
16
|
"files": [
|
|
13
|
-
"bin",
|
|
14
|
-
"src",
|
|
17
|
+
"bin/mycomesh-relay.mjs",
|
|
18
|
+
"src/*.mjs",
|
|
19
|
+
"networks/mycomesh-v11-sepolia.json",
|
|
20
|
+
"networks/mycomesh-testnet-ca.crt",
|
|
15
21
|
"README.md"
|
|
16
22
|
],
|
|
17
23
|
"engines": {
|
|
18
24
|
"node": ">=20"
|
|
19
25
|
},
|
|
20
26
|
"scripts": {
|
|
21
|
-
"test": "node --test"
|
|
27
|
+
"test": "node --test test/*.test.mjs"
|
|
22
28
|
},
|
|
23
29
|
"license": "UNLICENSED"
|
|
24
30
|
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// mycomesh-relay (V11): run an independent MycoMesh Relay (and optionally a keeper) in Docker.
|
|
2
|
+
// No domain name and no certificate authority: the Relay's self-signed certificate is pinned on-chain.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { request } from "node:https";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { parseArgs } from "node:util";
|
|
10
|
+
|
|
11
|
+
export const IMAGE = "ghcr.io/charleslzp/mycomesh:latest";
|
|
12
|
+
export const RELAY = "mycomesh-relay";
|
|
13
|
+
export const KEEPER = "mycomesh-keeper";
|
|
14
|
+
const NETWORKS = join(dirname(fileURLToPath(import.meta.url)), "../networks");
|
|
15
|
+
const NETWORK_FILE = "mycomesh-v11-sepolia.json";
|
|
16
|
+
|
|
17
|
+
const USAGE = `Usage: mycomesh-relay <command> [options]
|
|
18
|
+
|
|
19
|
+
init [--public-ip IP] create the owner and signer keys and a self-signed TLS certificate
|
|
20
|
+
register [--deposit UNITS] bind the signer on-chain, fund probes, and announce the Relay (with its
|
|
21
|
+
certificate pin) in the on-chain Relay directory; testnet gas comes from the faucet
|
|
22
|
+
start [--with-keeper] run the Relay on ports 10443 (HTTPS) and 10991 (Provider links)
|
|
23
|
+
status | logs | stop inspect or stop it
|
|
24
|
+
earnings | claim the Relay's share of fees and paying it out
|
|
25
|
+
address print the owner address (fund it with a little ETH on mainnets)
|
|
26
|
+
|
|
27
|
+
Options: --home DIR (default ~/.mycomesh/relay), --image REF, --network FILE,
|
|
28
|
+
--http-port 10443, --link-port 10991`;
|
|
29
|
+
|
|
30
|
+
function docker(args, { capture = true } = {}) {
|
|
31
|
+
const result = spawnSync("docker", args, { encoding: "utf8", stdio: capture ? "pipe" : "inherit" });
|
|
32
|
+
if (result.error) throw new Error(`docker is required: ${result.error.message}`);
|
|
33
|
+
if (result.status !== 0) throw new Error((result.stderr || result.stdout || `docker ${args[0]} failed`).trim().slice(-600));
|
|
34
|
+
return capture ? result.stdout.trim() : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const user = () => (typeof process.getuid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`, "-e", "HOME=/tmp"] : []);
|
|
38
|
+
|
|
39
|
+
function layout(values) {
|
|
40
|
+
const home = resolve(values.home || process.env.MYCOMESH_RELAY_HOME || join(homedir(), ".mycomesh", "relay"));
|
|
41
|
+
const paths = { home, keys: join(home, "keys"), data: join(home, "data"), config: join(home, "relay.json") };
|
|
42
|
+
for (const dir of [home, paths.keys, paths.data]) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
43
|
+
const config = existsSync(paths.config) ? JSON.parse(readFileSync(paths.config, "utf8")) : {};
|
|
44
|
+
return { paths, config };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const save = (paths, config) => writeFileSync(paths.config, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
48
|
+
|
|
49
|
+
function network(values) {
|
|
50
|
+
const file = resolve(values.network || join(NETWORKS, NETWORK_FILE));
|
|
51
|
+
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
52
|
+
if (manifest.schema !== "mycomesh.v11.network.v1") throw new Error(`${file} is not a MycoMesh V11 network manifest`);
|
|
53
|
+
return { file, manifest, mount: ["-v", `${dirname(file)}:/config:ro`], inside: `/config/${file.split("/").pop()}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mycomesh(values, paths, args, extra = []) {
|
|
57
|
+
const net = network(values);
|
|
58
|
+
return docker(["run", "--rm", ...user(), ...net.mount, "-v", `${paths.keys}:/keys`, "-v", `${paths.data}:/data`, ...extra,
|
|
59
|
+
values.image || IMAGE, ...args.map((arg) => (arg === "@network" ? net.inside : arg))]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function publicIp() {
|
|
63
|
+
return new Promise((resolvePromise, reject) => {
|
|
64
|
+
request("https://api.ipify.org", (res) => {
|
|
65
|
+
let body = "";
|
|
66
|
+
res.on("data", (chunk) => { body += chunk; });
|
|
67
|
+
res.on("end", () => (/^[0-9a-f.:]+$/i.test(body.trim()) ? resolvePromise(body.trim()) : reject(new Error("cannot detect the public IP; pass --public-ip"))));
|
|
68
|
+
}).on("error", () => reject(new Error("cannot detect the public IP; pass --public-ip"))).end();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function faucet(net, address) {
|
|
73
|
+
const { manifest, file } = net;
|
|
74
|
+
if (!manifest.faucet_url) return Promise.resolve(null);
|
|
75
|
+
const target = new URL(`${manifest.faucet_url}/v11/faucet`);
|
|
76
|
+
const ca = manifest.tls_ca_file ? readFileSync(join(dirname(file), manifest.tls_ca_file), "utf8") : undefined;
|
|
77
|
+
const body = JSON.stringify({ address });
|
|
78
|
+
return new Promise((resolvePromise) => {
|
|
79
|
+
const req = request(target, { method: "POST", ca, headers: { "content-type": "application/json", "content-length": body.length } }, (res) => {
|
|
80
|
+
res.resume();
|
|
81
|
+
res.on("end", () => resolvePromise(res.statusCode));
|
|
82
|
+
});
|
|
83
|
+
req.on("error", () => resolvePromise(null));
|
|
84
|
+
req.end(body);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function main(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
|
|
89
|
+
const { values, positionals } = parseArgs({
|
|
90
|
+
args: argv, allowPositionals: true,
|
|
91
|
+
options: {
|
|
92
|
+
home: { type: "string" }, image: { type: "string" }, network: { type: "string" }, "public-ip": { type: "string" },
|
|
93
|
+
deposit: { type: "string", default: "20000000" }, "http-port": { type: "string", default: "10443" },
|
|
94
|
+
"link-port": { type: "string", default: "10991" }, "with-keeper": { type: "boolean" }, help: { type: "boolean" },
|
|
95
|
+
version: { type: "boolean" },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const [command = "help"] = positionals;
|
|
99
|
+
if (values.version) {
|
|
100
|
+
stdout.write(`${JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version}\n`);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
if (values.help || command === "help") { stdout.write(`${USAGE}\n`); return 0; }
|
|
104
|
+
const { paths, config } = layout(values);
|
|
105
|
+
const has = (name) => existsSync(join(paths.keys, name));
|
|
106
|
+
|
|
107
|
+
if (command === "init") {
|
|
108
|
+
const ip = values["public-ip"] || config.public_ip || await publicIp();
|
|
109
|
+
for (const name of ["owner.key", "signer.key"]) if (!has(name)) mycomesh(values, paths, ["key", "new", `/keys/${name}`]);
|
|
110
|
+
if (!has("relay.crt")) {
|
|
111
|
+
mycomesh(values, paths, ["relay", "cert", "--network", "@network", "--public-host", ip,
|
|
112
|
+
"--tls-cert", "/keys/relay.crt", "--tls-key", "/keys/relay.key"]);
|
|
113
|
+
}
|
|
114
|
+
const owner = mycomesh(values, paths, ["key", "address", "/keys/owner.key"]);
|
|
115
|
+
save(paths, { ...config, public_ip: ip, owner });
|
|
116
|
+
stdout.write(`Relay owner ${owner}\npublic endpoint https://${ip}:${values["http-port"]} (self-signed, pinned on-chain at register)\n`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (!has("owner.key")) throw new Error("run `mycomesh-relay init` first");
|
|
120
|
+
if (command === "address") { stdout.write(`${config.owner}\n`); return 0; }
|
|
121
|
+
if (command === "register") {
|
|
122
|
+
const net = network(values);
|
|
123
|
+
const status = await faucet(net, config.owner);
|
|
124
|
+
if (status === 200) stdout.write("funded the owner from the testnet faucet\n");
|
|
125
|
+
const output = mycomesh(values, paths, ["relay", "register", "--network", "@network", "--owner-key", "/keys/owner.key",
|
|
126
|
+
"--signer-key", "/keys/signer.key", "--deposit", values.deposit, "--tls-cert", "/keys/relay.crt",
|
|
127
|
+
"--public-host", config.public_ip, "--public-http-port", values["http-port"], "--public-link-port", values["link-port"]]);
|
|
128
|
+
save(paths, { ...config, registered: true, http_port: values["http-port"], link_port: values["link-port"] });
|
|
129
|
+
stdout.write(`${output.split("\n").filter((line) => !line.startsWith(" 0x")).join("\n")}\nnext: mycomesh-relay start\n`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
if (command === "start") {
|
|
133
|
+
if (!config.registered) throw new Error("run `mycomesh-relay register` first");
|
|
134
|
+
const net = network(values);
|
|
135
|
+
const http = config.http_port || values["http-port"];
|
|
136
|
+
const link = config.link_port || values["link-port"];
|
|
137
|
+
spawnSync("docker", ["rm", "-f", RELAY], { stdio: "ignore" });
|
|
138
|
+
docker(["run", "-d", "--name", RELAY, "--restart", "unless-stopped", ...user(), ...net.mount,
|
|
139
|
+
"-v", `${paths.keys}:/keys:ro`, "-v", `${paths.data}:/data`, "-p", `${http}:${http}`, "-p", `${link}:${link}`,
|
|
140
|
+
"--log-opt", "max-size=20m", "--log-opt", "max-file=3", values.image || IMAGE,
|
|
141
|
+
"relay", "serve", "--network", net.inside, "--owner-key", "/keys/owner.key", "--signer-key", "/keys/signer.key",
|
|
142
|
+
"--data-dir", "/data/relay", "--http", `0.0.0.0:${http}`, "--link", `0.0.0.0:${link}`,
|
|
143
|
+
"--tls-cert", "/keys/relay.crt", "--tls-key", "/keys/relay.key"]);
|
|
144
|
+
if (values["with-keeper"]) {
|
|
145
|
+
spawnSync("docker", ["rm", "-f", KEEPER], { stdio: "ignore" });
|
|
146
|
+
docker(["run", "-d", "--name", KEEPER, "--restart", "unless-stopped", ...user(), ...net.mount,
|
|
147
|
+
"-v", `${paths.keys}:/keys:ro`, "-v", `${paths.data}:/data`, values.image || IMAGE,
|
|
148
|
+
"keeper", "serve", "--network", net.inside, "--key", "/keys/owner.key", "--data-dir", "/data/keeper"]);
|
|
149
|
+
}
|
|
150
|
+
stdout.write(`started ${RELAY}${values["with-keeper"] ? ` and ${KEEPER}` : ""} on https://${config.public_ip}:${http}\n`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
if (command === "status") {
|
|
154
|
+
for (const name of [RELAY, KEEPER]) {
|
|
155
|
+
const state = spawnSync("docker", ["ps", "-a", "--filter", `name=^${name}$`, "--format", "{{.Status}}"], { encoding: "utf8" }).stdout.trim();
|
|
156
|
+
if (state) stdout.write(`${name}: ${state}\n`);
|
|
157
|
+
}
|
|
158
|
+
stdout.write(`${spawnSync("docker", ["logs", "--tail", "5", RELAY], { encoding: "utf8" }).stdout || ""}`);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
if (command === "logs") { docker(["logs", "-f", "--tail", "100", RELAY], { capture: false }); return 0; }
|
|
162
|
+
if (command === "stop") {
|
|
163
|
+
for (const name of [RELAY, KEEPER]) spawnSync("docker", ["rm", "-f", name], { stdio: "ignore" });
|
|
164
|
+
stdout.write("stopped\n");
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
if (command === "earnings") {
|
|
168
|
+
stdout.write(`${mycomesh(values, paths, ["relay", "earnings", "--network", "@network", "--owner", config.owner])}\n`);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
if (command === "claim") {
|
|
172
|
+
stdout.write(`${mycomesh(values, paths, ["relay", "claim", "--network", "@network", "--owner-key", "/keys/owner.key"]).split("\n").pop()}\n`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`unknown command ${command}\n${USAGE}`);
|
|
176
|
+
}
|
package/bin/mycomesh-relay
DELETED
package/src/relay.mjs
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { spawn as defaultSpawn } from "node:child_process";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const DEFAULT_REPOSITORY_URL = "https://github.com/Charleslzp/mycomesh";
|
|
7
|
-
const DEFAULT_REF = "399cecef701d33beaaf56a18c811cf73d761723d";
|
|
8
|
-
const DEFAULT_BOOTSTRAP_REPOSITORY = "https://raw.githubusercontent.com/Charleslzp/mycomesh";
|
|
9
|
-
const MAX_BOOTSTRAP_BYTES = 256 * 1024;
|
|
10
|
-
|
|
11
|
-
const HELP = `Usage: mycomesh-relay [options]
|
|
12
|
-
|
|
13
|
-
Start the MycoMesh Relay through the Docker-backed bootstrap installer.
|
|
14
|
-
The first run opens a loopback browser wizard for public Relay settings.
|
|
15
|
-
|
|
16
|
-
Options:
|
|
17
|
-
--ref REF Git branch, tag, or commit (default: V8 release commit)
|
|
18
|
-
--repo-url URL HTTPS repository URL for the Relay checkout
|
|
19
|
-
--source-dir PATH Persistent checkout directory (default: ./mycomesh)
|
|
20
|
-
--wizard-port PORT Loopback onboarding port (default: 8766)
|
|
21
|
-
--no-browser Print the wizard URL without opening a browser
|
|
22
|
-
--no-start Prepare the checkout without starting Relay
|
|
23
|
-
--dry-run Print the planned operations only
|
|
24
|
-
-h, --help Show this help
|
|
25
|
-
|
|
26
|
-
The wizard accepts only a public payout address, concurrency and usage limit.
|
|
27
|
-
Settlement transaction credentials stay in the Relay operator environment;
|
|
28
|
-
this launcher never accepts or stores a private key. Docker Compose V2 and
|
|
29
|
-
GNU Make are required on the Relay machine.
|
|
30
|
-
|
|
31
|
-
Example:
|
|
32
|
-
npm install --global mycomesh-relay
|
|
33
|
-
mycomesh-relay --ref <reviewed-commit> --no-browser`;
|
|
34
|
-
|
|
35
|
-
class RelayCliError extends Error {
|
|
36
|
-
constructor(message, exitCode = 1) {
|
|
37
|
-
super(message);
|
|
38
|
-
this.name = "RelayCliError";
|
|
39
|
-
this.exitCode = exitCode;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function main(argv, dependencies = {}) {
|
|
44
|
-
const env = dependencies.env ?? process.env;
|
|
45
|
-
const stdout = dependencies.stdout ?? process.stdout;
|
|
46
|
-
const stderr = dependencies.stderr ?? process.stderr;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const parsed = parseArguments(argv, env);
|
|
50
|
-
if (parsed.help) {
|
|
51
|
-
stdout.write(`${HELP}\n`);
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const fetchImpl = dependencies.fetch ?? globalThis.fetch;
|
|
56
|
-
if (typeof fetchImpl !== "function") {
|
|
57
|
-
throw new RelayCliError("Node.js 20 or newer is required");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const bootstrap = await downloadBootstrap(parsed, fetchImpl);
|
|
61
|
-
try {
|
|
62
|
-
return await runBootstrap(
|
|
63
|
-
bootstrap.path,
|
|
64
|
-
toBootstrapArgs(parsed),
|
|
65
|
-
{
|
|
66
|
-
env,
|
|
67
|
-
spawn: dependencies.spawn ?? defaultSpawn,
|
|
68
|
-
},
|
|
69
|
-
);
|
|
70
|
-
} finally {
|
|
71
|
-
await rm(bootstrap.directory, { recursive: true, force: true });
|
|
72
|
-
}
|
|
73
|
-
} catch (error) {
|
|
74
|
-
const exitCode = error instanceof RelayCliError ? error.exitCode : 1;
|
|
75
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
-
stderr.write(`mycomesh relay: ${message}\n`);
|
|
77
|
-
return exitCode;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function parseArguments(argv, env = process.env) {
|
|
82
|
-
const parsed = {
|
|
83
|
-
ref: env.MYCOMESH_REF || DEFAULT_REF,
|
|
84
|
-
repositoryUrl: env.MYCOMESH_REPOSITORY_URL || DEFAULT_REPOSITORY_URL,
|
|
85
|
-
sourceDir: env.MYCOMESH_SOURCE_DIR || undefined,
|
|
86
|
-
wizardPort: env.MYCOMESH_RELAY_WIZARD_PORT || "8766",
|
|
87
|
-
noBrowser: false,
|
|
88
|
-
noStart: false,
|
|
89
|
-
dryRun: false,
|
|
90
|
-
help: false,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
94
|
-
const token = argv[index];
|
|
95
|
-
if (token === "-h" || token === "--help") {
|
|
96
|
-
parsed.help = true;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (token === "--no-browser") {
|
|
100
|
-
parsed.noBrowser = true;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (token === "--no-start") {
|
|
104
|
-
parsed.noStart = true;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (token === "--dry-run") {
|
|
108
|
-
parsed.dryRun = true;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (!token.startsWith("--")) {
|
|
112
|
-
throw new RelayCliError(`unexpected argument: ${token}`, 2);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const separator = token.indexOf("=");
|
|
116
|
-
const name = separator === -1 ? token : token.slice(0, separator);
|
|
117
|
-
let value = separator === -1 ? undefined : token.slice(separator + 1);
|
|
118
|
-
if (value === undefined) {
|
|
119
|
-
index += 1;
|
|
120
|
-
value = argv[index];
|
|
121
|
-
}
|
|
122
|
-
if (value === undefined || value === "") {
|
|
123
|
-
throw new RelayCliError(`${name} requires a value`, 2);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
switch (name) {
|
|
127
|
-
case "--ref":
|
|
128
|
-
parsed.ref = value;
|
|
129
|
-
break;
|
|
130
|
-
case "--repo-url":
|
|
131
|
-
parsed.repositoryUrl = value;
|
|
132
|
-
break;
|
|
133
|
-
case "--source-dir":
|
|
134
|
-
parsed.sourceDir = value;
|
|
135
|
-
break;
|
|
136
|
-
case "--wizard-port":
|
|
137
|
-
parsed.wizardPort = value;
|
|
138
|
-
break;
|
|
139
|
-
default:
|
|
140
|
-
throw new RelayCliError(`unknown option: ${name}`, 2);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
validateRef(parsed.ref);
|
|
145
|
-
validateRepositoryUrl(parsed.repositoryUrl);
|
|
146
|
-
validatePort(parsed.wizardPort);
|
|
147
|
-
return parsed;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function validateRef(ref) {
|
|
151
|
-
if (
|
|
152
|
-
typeof ref !== "string" ||
|
|
153
|
-
!/^[A-Za-z0-9._/-]{1,160}$/.test(ref) ||
|
|
154
|
-
ref.includes("..") ||
|
|
155
|
-
ref.startsWith("/")
|
|
156
|
-
) {
|
|
157
|
-
throw new RelayCliError("invalid repository ref", 2);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function validateRepositoryUrl(repositoryUrl) {
|
|
162
|
-
let parsed;
|
|
163
|
-
try {
|
|
164
|
-
parsed = new URL(repositoryUrl);
|
|
165
|
-
} catch {
|
|
166
|
-
throw new RelayCliError("--repo-url must be an HTTPS URL", 2);
|
|
167
|
-
}
|
|
168
|
-
if (parsed.protocol !== "https:") {
|
|
169
|
-
throw new RelayCliError("--repo-url must be an HTTPS URL", 2);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function validatePort(port) {
|
|
174
|
-
if (!/^\d+$/.test(String(port))) {
|
|
175
|
-
throw new RelayCliError("--wizard-port must be an integer", 2);
|
|
176
|
-
}
|
|
177
|
-
const value = Number(port);
|
|
178
|
-
if (!Number.isSafeInteger(value) || value < 1 || value > 65535) {
|
|
179
|
-
throw new RelayCliError("--wizard-port is out of range", 2);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function bootstrapUrl(ref) {
|
|
184
|
-
return `${DEFAULT_BOOTSTRAP_REPOSITORY}/${ref}/scripts/bootstrap-relay.sh`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function downloadBootstrap(parsed, fetchImpl) {
|
|
188
|
-
const response = await fetchImpl(bootstrapUrl(parsed.ref), {
|
|
189
|
-
headers: { accept: "text/plain" },
|
|
190
|
-
});
|
|
191
|
-
if (!response?.ok) {
|
|
192
|
-
const status = response ? `${response.status} ${response.statusText || ""}`.trim() : "unknown response";
|
|
193
|
-
throw new RelayCliError(`could not download Relay bootstrap (${status})`);
|
|
194
|
-
}
|
|
195
|
-
const script = await response.text();
|
|
196
|
-
if (Buffer.byteLength(script, "utf8") > MAX_BOOTSTRAP_BYTES) {
|
|
197
|
-
throw new RelayCliError("Relay bootstrap script is unexpectedly large");
|
|
198
|
-
}
|
|
199
|
-
if (!script.startsWith("#!/usr/bin/env bash")) {
|
|
200
|
-
throw new RelayCliError("downloaded Relay bootstrap is not a Bash script");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const directory = await mkdtemp(join(tmpdir(), "mycomesh-relay-"));
|
|
204
|
-
const path = join(directory, "bootstrap-relay.sh");
|
|
205
|
-
await writeFile(path, script, { encoding: "utf8", mode: 0o700 });
|
|
206
|
-
return { directory, path };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function toBootstrapArgs(parsed) {
|
|
210
|
-
const args = [
|
|
211
|
-
"--ref",
|
|
212
|
-
parsed.ref,
|
|
213
|
-
"--repo-url",
|
|
214
|
-
parsed.repositoryUrl,
|
|
215
|
-
"--wizard-port",
|
|
216
|
-
String(parsed.wizardPort),
|
|
217
|
-
];
|
|
218
|
-
if (parsed.sourceDir) args.push("--source-dir", parsed.sourceDir);
|
|
219
|
-
if (parsed.noBrowser) args.push("--no-browser");
|
|
220
|
-
if (parsed.noStart) args.push("--no-start");
|
|
221
|
-
if (parsed.dryRun) args.push("--dry-run");
|
|
222
|
-
return args;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function runBootstrap(scriptPath, args, { env, spawn }) {
|
|
226
|
-
return new Promise((resolve, reject) => {
|
|
227
|
-
let child;
|
|
228
|
-
try {
|
|
229
|
-
child = spawn("bash", [scriptPath, ...args], {
|
|
230
|
-
env: { ...env },
|
|
231
|
-
stdio: "inherit",
|
|
232
|
-
});
|
|
233
|
-
} catch (error) {
|
|
234
|
-
reject(new RelayCliError(`could not start bash: ${error.message}`));
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
child.once("error", (error) => {
|
|
238
|
-
reject(new RelayCliError(`could not start bash: ${error.message}`));
|
|
239
|
-
});
|
|
240
|
-
child.once("exit", (code, signal) => {
|
|
241
|
-
if (typeof code === "number") {
|
|
242
|
-
resolve(code);
|
|
243
|
-
} else {
|
|
244
|
-
resolve(128 + signalNumber(signal));
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function signalNumber(signal) {
|
|
251
|
-
const signals = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1 };
|
|
252
|
-
return signals[signal] || 1;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export { toBootstrapArgs };
|