mycomesh-relay 0.1.0

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 ADDED
@@ -0,0 +1,33 @@
1
+ # mycomesh-relay
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
8
+
9
+ ```sh
10
+ npm install --global mycomesh-relay
11
+ mycomesh-relay
12
+ ```
13
+
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`.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/relay.mjs";
4
+
5
+ process.exitCode = await main(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "mycomesh-relay",
3
+ "version": "0.1.0",
4
+ "description": "MycoMesh Relay onboarding and Docker launcher.",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "bin": {
10
+ "mycomesh-relay": "bin/mycomesh-relay"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "test": "node --test"
22
+ },
23
+ "license": "UNLICENSED"
24
+ }
package/src/relay.mjs ADDED
@@ -0,0 +1,255 @@
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 = "main";
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: main)
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 };