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 CHANGED
@@ -1,33 +1,19 @@
1
1
  # mycomesh-relay
2
2
 
3
- The MycoMesh Relay installer is a small Node.js launcher around the repository's
4
- Docker Compose Relay runtime. It downloads a pinned checkout, opens the
5
- loopback browser onboarding wizard, and then runs `make relay-start`.
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
- The wizard asks for a public payout address, maximum concurrent sessions and an
15
- optional usage limit. It never accepts an EVM private key, seed phrase, OAuth
16
- credential or API key. If on-screen browser opening is unavailable, use
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,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/cli.mjs";
3
+
4
+ main().then((code) => { if (typeof code === "number") process.exitCode = code; }).catch((error) => {
5
+ process.stderr.write(`mycomesh-relay: ${error.message}\n`);
6
+ process.exitCode = 1;
7
+ });
@@ -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.7",
4
- "description": "MycoMesh Relay onboarding and Docker launcher.",
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
+ }
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { main } from "../src/relay.mjs";
4
-
5
- process.exitCode = await main(process.argv.slice(2));
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 };