@w3devx/zr-agent 0.0.0-stage → 0.1.0-rc.2
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 +32 -2
- package/dist/cli.js +538 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @w3devx/zr-agent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installer for **zr-agent**, the zyber runner node agent. The package version is the agent version: it downloads exactly that release
|
|
4
|
+
from [W3DevTech/zr-agent-releases](https://github.com/W3DevTech/zr-agent-releases), verifies it, installs it and registers the node.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
sudo npx @w3devx/zr-agent@<version> install --join zrj_... --url https://control-plane.example [--name NAME] [--no-start]
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`<version>` is the fleet version shown in the dashboard's Add node dialog. Run it as root on a Linux x64 or arm64 host with Docker and systemd.
|
|
11
|
+
|
|
12
|
+
What `install` does:
|
|
13
|
+
|
|
14
|
+
1. Downloads `zr-agent-linux-<arch>`, `SHA256SUMS` and `SHA256SUMS.minisig` for `v<version>`.
|
|
15
|
+
2. Verifies `SHA256SUMS` against the minisign public key pinned in this package (key id `A7504FCF5B75D562`; Ed25519 over the BLAKE2b-512
|
|
16
|
+
prehash, key id and trusted-comment signature all checked, and the trusted comment must be `zr-agent v<version>`), then the binary's SHA-256.
|
|
17
|
+
Nothing is written until both pass.
|
|
18
|
+
3. Installs to `/opt/zr-agent/versions/<version>/zr-agent`, points `/opt/zr-agent/current` at it (the previous version is kept for rollback,
|
|
19
|
+
older ones are removed), stamps it `.verified` and links `/usr/local/bin/zr-agent`. It also copies the binary to
|
|
20
|
+
`/opt/zr-agent/zr-agent-launcher`, outside `versions/`.
|
|
21
|
+
4. Writes `/etc/systemd/system/zr-agent.service`, which runs `zr-agent-launcher launcher`: the launcher starts `current/zr-agent`, restarts it
|
|
22
|
+
after a self-upgrade and flips `current` back if a new release does not say hello within 60 s.
|
|
23
|
+
5. With `--join` and `--url`: runs `zr-agent register` (the token goes through the environment, never argv or logs), then
|
|
24
|
+
`systemctl enable` and `restart zr-agent`. Without `--join` on an unconfigured host it stops after step 4 and tells you what to run.
|
|
25
|
+
|
|
26
|
+
Flags: `--join`, `--url`, `--name`, `--force` (register again), `--no-start`, `--version` (override). The token and URL are also read from
|
|
27
|
+
`ZR_AGENT_JOIN_TOKEN` and `ZR_AGENT_URL`.
|
|
28
|
+
|
|
29
|
+
No Node? Use the `install.sh` attached to the release instead; it does the same with `curl`, `sha256sum` and `minisign`
|
|
30
|
+
(it fetches a pinned minisign build if none is installed).
|
|
31
|
+
|
|
32
|
+
Source: `npm/zr-agent` in ashutoshpw/zyber-sh-manager. Releases are built, signed and published by `.github/workflows/zr-agent-release.yml`
|
|
33
|
+
(npm trusted publishing; no provenance attestation while the source repo is private).
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/main.ts
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
// package.json
|
|
8
|
+
var package_default = {
|
|
9
|
+
name: "@w3devx/zr-agent",
|
|
10
|
+
version: "0.1.0-rc.2",
|
|
11
|
+
type: "module",
|
|
12
|
+
description: "Installer for zr-agent, the zyber runner node agent: downloads the exact release, verifies its minisign signature and checksum, installs it under /opt/zr-agent and writes the systemd unit.",
|
|
13
|
+
bin: {
|
|
14
|
+
"zr-agent": "dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
files: [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
engines: {
|
|
21
|
+
node: ">=20"
|
|
22
|
+
},
|
|
23
|
+
os: [
|
|
24
|
+
"linux"
|
|
25
|
+
],
|
|
26
|
+
cpu: [
|
|
27
|
+
"x64",
|
|
28
|
+
"arm64"
|
|
29
|
+
],
|
|
30
|
+
license: "UNLICENSED",
|
|
31
|
+
repository: {
|
|
32
|
+
type: "git",
|
|
33
|
+
url: "git+https://github.com/ashutoshpw/zyber-sh-manager.git",
|
|
34
|
+
directory: "npm/zr-agent"
|
|
35
|
+
},
|
|
36
|
+
publishConfig: {
|
|
37
|
+
access: "public"
|
|
38
|
+
},
|
|
39
|
+
scripts: {
|
|
40
|
+
build: "bun build src/main.ts --target node --outfile dist/cli.js && chmod +x dist/cli.js",
|
|
41
|
+
test: "bun test"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/install.ts
|
|
46
|
+
import { chmod, mkdir, readdir, readlink, rename, rm, symlink, writeFile, lstat } from "node:fs/promises";
|
|
47
|
+
import { dirname, join } from "node:path";
|
|
48
|
+
|
|
49
|
+
// ../../dashboard/packages/agent-protocol/src/release.ts
|
|
50
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
51
|
+
|
|
52
|
+
// ../../dashboard/packages/agent-protocol/src/minisign.ts
|
|
53
|
+
import { createHash, createPublicKey, verify } from "node:crypto";
|
|
54
|
+
var ZR_AGENT_MINISIGN_PUBLIC_KEY = "RWRi1XVbz09Qp5TTZ0iefap1wtuBahGfzIp5j4z5KDXfM0bn05ImR3zc";
|
|
55
|
+
var KEY_ID_BYTES = 8;
|
|
56
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
57
|
+
var SIGNATURE_BYTES = 64;
|
|
58
|
+
var TRUSTED_COMMENT_PREFIX = "trusted comment:";
|
|
59
|
+
var UNTRUSTED_COMMENT_PREFIX = "untrusted comment:";
|
|
60
|
+
var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
61
|
+
function keyIdToHex(keyId) {
|
|
62
|
+
return Buffer.from(keyId).reverse().toString("hex").toUpperCase();
|
|
63
|
+
}
|
|
64
|
+
function b64(s) {
|
|
65
|
+
const t = s.trim();
|
|
66
|
+
if (t.length === 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(t))
|
|
67
|
+
return null;
|
|
68
|
+
return Buffer.from(t, "base64");
|
|
69
|
+
}
|
|
70
|
+
function lines(text) {
|
|
71
|
+
return text.split(/\r?\n/);
|
|
72
|
+
}
|
|
73
|
+
function parseMinisignPublicKey(text) {
|
|
74
|
+
const rows = lines(text).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(UNTRUSTED_COMMENT_PREFIX));
|
|
75
|
+
if (rows.length !== 1)
|
|
76
|
+
return null;
|
|
77
|
+
const raw = b64(rows[0]);
|
|
78
|
+
if (!raw || raw.length !== 2 + KEY_ID_BYTES + PUBLIC_KEY_BYTES)
|
|
79
|
+
return null;
|
|
80
|
+
if (raw[0] !== 69 || raw[1] !== 100)
|
|
81
|
+
return null;
|
|
82
|
+
const keyId = new Uint8Array(raw.subarray(2, 2 + KEY_ID_BYTES));
|
|
83
|
+
return { keyId, keyIdHex: keyIdToHex(keyId), publicKey: new Uint8Array(raw.subarray(2 + KEY_ID_BYTES)) };
|
|
84
|
+
}
|
|
85
|
+
function parseMinisignSignature(text) {
|
|
86
|
+
const rows = lines(text);
|
|
87
|
+
while (rows.length > 0 && rows[rows.length - 1].trim() === "")
|
|
88
|
+
rows.pop();
|
|
89
|
+
if (rows.length !== 4)
|
|
90
|
+
return null;
|
|
91
|
+
if (!rows[0].startsWith(UNTRUSTED_COMMENT_PREFIX))
|
|
92
|
+
return null;
|
|
93
|
+
const sigLine = b64(rows[1]);
|
|
94
|
+
if (!sigLine || sigLine.length !== 2 + KEY_ID_BYTES + SIGNATURE_BYTES)
|
|
95
|
+
return null;
|
|
96
|
+
const alg = sigLine.subarray(0, 2).toString("latin1");
|
|
97
|
+
if (alg !== "ED" && alg !== "Ed")
|
|
98
|
+
return null;
|
|
99
|
+
if (!rows[2].startsWith(TRUSTED_COMMENT_PREFIX))
|
|
100
|
+
return null;
|
|
101
|
+
const trustedComment = rows[2].slice(TRUSTED_COMMENT_PREFIX.length).replace(/^ /, "");
|
|
102
|
+
const global = b64(rows[3]);
|
|
103
|
+
if (!global || global.length !== SIGNATURE_BYTES)
|
|
104
|
+
return null;
|
|
105
|
+
return {
|
|
106
|
+
algorithm: alg,
|
|
107
|
+
keyId: new Uint8Array(sigLine.subarray(2, 2 + KEY_ID_BYTES)),
|
|
108
|
+
signature: new Uint8Array(sigLine.subarray(2 + KEY_ID_BYTES)),
|
|
109
|
+
trustedComment,
|
|
110
|
+
globalSignature: new Uint8Array(global)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function ed25519Verify(publicKey, message, signature) {
|
|
114
|
+
try {
|
|
115
|
+
const key = createPublicKey({ key: Buffer.concat([ED25519_SPKI_PREFIX, publicKey]), format: "der", type: "spki" });
|
|
116
|
+
return verify(null, message, key, signature);
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function sameBytes(a, b) {
|
|
122
|
+
return a.length === b.length && Buffer.from(a).equals(Buffer.from(b));
|
|
123
|
+
}
|
|
124
|
+
function verifyMinisign(input) {
|
|
125
|
+
const key = parseMinisignPublicKey(input.publicKey);
|
|
126
|
+
if (!key)
|
|
127
|
+
return { ok: false, reason: "malformed public key" };
|
|
128
|
+
const sig = parseMinisignSignature(input.signature);
|
|
129
|
+
if (!sig)
|
|
130
|
+
return { ok: false, reason: "malformed signature file" };
|
|
131
|
+
if (!sameBytes(sig.keyId, key.keyId)) {
|
|
132
|
+
return { ok: false, reason: `signature key id ${keyIdToHex(sig.keyId)} does not match the pinned key ${key.keyIdHex}` };
|
|
133
|
+
}
|
|
134
|
+
const signed = sig.algorithm === "ED" ? new Uint8Array(createHash("blake2b512").update(input.message).digest()) : input.message;
|
|
135
|
+
if (!ed25519Verify(key.publicKey, signed, sig.signature))
|
|
136
|
+
return { ok: false, reason: "signature does not match the message" };
|
|
137
|
+
const globalMessage = Buffer.concat([sig.signature, Buffer.from(sig.trustedComment, "utf8")]);
|
|
138
|
+
if (!ed25519Verify(key.publicKey, globalMessage, sig.globalSignature)) {
|
|
139
|
+
return { ok: false, reason: "trusted comment signature does not match" };
|
|
140
|
+
}
|
|
141
|
+
return { ok: true, trustedComment: sig.trustedComment, keyIdHex: key.keyIdHex, algorithm: sig.algorithm };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ../../dashboard/packages/agent-protocol/src/release.ts
|
|
145
|
+
var RELEASES_REPO = "W3DevTech/zr-agent-releases";
|
|
146
|
+
var RELEASES_DOWNLOAD_BASE = `https://github.com/${RELEASES_REPO}/releases/download`;
|
|
147
|
+
var SUMS_ASSET = "SHA256SUMS";
|
|
148
|
+
var SUMS_SIGNATURE_ASSET = "SHA256SUMS.minisig";
|
|
149
|
+
var VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
150
|
+
function isValidReleaseVersion(version) {
|
|
151
|
+
return typeof version === "string" && version.length <= 64 && VERSION_RE.test(version);
|
|
152
|
+
}
|
|
153
|
+
function releaseArch(nodeArch) {
|
|
154
|
+
if (nodeArch === "x64" || nodeArch === "x86_64" || nodeArch === "amd64")
|
|
155
|
+
return "x64";
|
|
156
|
+
if (nodeArch === "arm64" || nodeArch === "aarch64")
|
|
157
|
+
return "arm64";
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function binaryAssetName(arch) {
|
|
161
|
+
return `zr-agent-linux-${arch}`;
|
|
162
|
+
}
|
|
163
|
+
function releaseTagFor(version) {
|
|
164
|
+
return `v${version}`;
|
|
165
|
+
}
|
|
166
|
+
function releaseAssetUrl(version, assetName, base = RELEASES_DOWNLOAD_BASE) {
|
|
167
|
+
if (!isValidReleaseVersion(version))
|
|
168
|
+
throw new Error(`releaseAssetUrl: invalid version "${version}"`);
|
|
169
|
+
return `${base.replace(/\/+$/, "")}/${releaseTagFor(version)}/${assetName}`;
|
|
170
|
+
}
|
|
171
|
+
function releaseTrustedComment(version) {
|
|
172
|
+
return `zr-agent v${version}`;
|
|
173
|
+
}
|
|
174
|
+
function sha256Hex(bytes) {
|
|
175
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
176
|
+
}
|
|
177
|
+
function parseSha256Sums(text) {
|
|
178
|
+
const out = new Map;
|
|
179
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
180
|
+
if (raw.trim() === "")
|
|
181
|
+
continue;
|
|
182
|
+
const m = /^([0-9a-fA-F]{64}) [ *]([^\s/\\][^/\\]*)$/.exec(raw);
|
|
183
|
+
if (!m)
|
|
184
|
+
return null;
|
|
185
|
+
const name = m[2];
|
|
186
|
+
if (out.has(name))
|
|
187
|
+
return null;
|
|
188
|
+
out.set(name, m[1].toLowerCase());
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
function verifyReleaseSums(input) {
|
|
193
|
+
if (!isValidReleaseVersion(input.version))
|
|
194
|
+
return { ok: false, reason: `invalid version "${input.version}"` };
|
|
195
|
+
const sig = verifyMinisign({ message: input.sums, signature: input.signature, publicKey: input.publicKey ?? ZR_AGENT_MINISIGN_PUBLIC_KEY });
|
|
196
|
+
if (sig.ok === false)
|
|
197
|
+
return { ok: false, reason: `SHA256SUMS signature rejected: ${sig.reason}` };
|
|
198
|
+
const want = releaseTrustedComment(input.version);
|
|
199
|
+
if (sig.trustedComment !== want) {
|
|
200
|
+
return { ok: false, reason: `SHA256SUMS is signed for "${sig.trustedComment}", expected "${want}"` };
|
|
201
|
+
}
|
|
202
|
+
const table = parseSha256Sums(Buffer.from(input.sums).toString("utf8"));
|
|
203
|
+
if (!table)
|
|
204
|
+
return { ok: false, reason: "SHA256SUMS is malformed" };
|
|
205
|
+
return { ok: true, sums: table };
|
|
206
|
+
}
|
|
207
|
+
function verifyReleaseBinary(input) {
|
|
208
|
+
const sums = verifyReleaseSums(input);
|
|
209
|
+
if (sums.ok === false)
|
|
210
|
+
return sums;
|
|
211
|
+
const expected = sums.sums.get(input.assetName);
|
|
212
|
+
if (!expected)
|
|
213
|
+
return { ok: false, reason: `SHA256SUMS has no entry for ${input.assetName}` };
|
|
214
|
+
const actual = sha256Hex(input.bytes);
|
|
215
|
+
if (actual !== expected)
|
|
216
|
+
return { ok: false, reason: `checksum mismatch for ${input.assetName}: expected ${expected}, got ${actual}` };
|
|
217
|
+
return { ok: true, sha256: actual };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/systemd.ts
|
|
221
|
+
var INSTALL_ROOT = "/opt/zr-agent";
|
|
222
|
+
var UNIT_NAME = "zr-agent.service";
|
|
223
|
+
var UNIT_PATH = `/etc/systemd/system/${UNIT_NAME}`;
|
|
224
|
+
var CONFIG_PATH = "/etc/zr-agent/config.json";
|
|
225
|
+
var BIN_LINK = "/usr/local/bin/zr-agent";
|
|
226
|
+
var LAUNCHER_BIN = `${INSTALL_ROOT}/zr-agent-launcher`;
|
|
227
|
+
var SYSTEMD_UNIT = `[Unit]
|
|
228
|
+
Description=zr-agent (zyber runner node agent)
|
|
229
|
+
Documentation=https://github.com/W3DevTech/zr-agent-releases
|
|
230
|
+
After=network-online.target docker.service
|
|
231
|
+
Wants=network-online.target docker.service
|
|
232
|
+
|
|
233
|
+
[Service]
|
|
234
|
+
Type=simple
|
|
235
|
+
ExecStart=${LAUNCHER_BIN} launcher
|
|
236
|
+
Restart=always
|
|
237
|
+
RestartSec=3
|
|
238
|
+
RuntimeDirectory=zr-agent
|
|
239
|
+
RuntimeDirectoryPreserve=yes
|
|
240
|
+
StateDirectory=zr-agent
|
|
241
|
+
LimitNOFILE=1048576
|
|
242
|
+
|
|
243
|
+
[Install]
|
|
244
|
+
WantedBy=multi-user.target
|
|
245
|
+
`;
|
|
246
|
+
|
|
247
|
+
// src/install.ts
|
|
248
|
+
class InstallError extends Error {
|
|
249
|
+
}
|
|
250
|
+
var BINARY_NAME = "zr-agent";
|
|
251
|
+
async function exists(path) {
|
|
252
|
+
try {
|
|
253
|
+
await lstat(path);
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
var defaultInstallDeps = {
|
|
260
|
+
fetchBytes: async (url) => {
|
|
261
|
+
const res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(5 * 60000) });
|
|
262
|
+
if (!res.ok)
|
|
263
|
+
throw new InstallError(`download failed: ${url} -> HTTP ${res.status}`);
|
|
264
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
265
|
+
},
|
|
266
|
+
exec: async (cmd, args, env) => {
|
|
267
|
+
const { spawn } = await import("node:child_process");
|
|
268
|
+
return await new Promise((resolve, reject) => {
|
|
269
|
+
const child = spawn(cmd, args, { stdio: "inherit", env: { ...process.env, ...env } });
|
|
270
|
+
child.on("error", reject);
|
|
271
|
+
child.on("close", (code) => resolve({ code: code ?? 1 }));
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
fileExists: exists,
|
|
275
|
+
log: (line) => console.log(line)
|
|
276
|
+
};
|
|
277
|
+
async function writeAtomic(path, data, mode) {
|
|
278
|
+
await mkdir(dirname(path), { recursive: true });
|
|
279
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
280
|
+
await writeFile(tmp, data, { mode });
|
|
281
|
+
await chmod(tmp, mode);
|
|
282
|
+
await rename(tmp, path);
|
|
283
|
+
}
|
|
284
|
+
async function flipSymlink(link, target) {
|
|
285
|
+
const tmp = `${link}.tmp-${process.pid}`;
|
|
286
|
+
await rm(tmp, { force: true });
|
|
287
|
+
await symlink(target, tmp);
|
|
288
|
+
await rename(tmp, link);
|
|
289
|
+
}
|
|
290
|
+
async function currentVersion(link) {
|
|
291
|
+
try {
|
|
292
|
+
const target = await readlink(link);
|
|
293
|
+
const v = target.split("/").filter(Boolean).pop() ?? "";
|
|
294
|
+
return isValidReleaseVersion(v) ? v : null;
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function pruneVersions(versionsDir, keep, log) {
|
|
300
|
+
let entries = [];
|
|
301
|
+
try {
|
|
302
|
+
entries = await readdir(versionsDir);
|
|
303
|
+
} catch {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
for (const e of entries) {
|
|
307
|
+
if (!isValidReleaseVersion(e) || keep.has(e))
|
|
308
|
+
continue;
|
|
309
|
+
await rm(join(versionsDir, e), { recursive: true, force: true });
|
|
310
|
+
log(`removed old version ${e}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function installAgent(opts, depsIn = {}) {
|
|
314
|
+
const deps = { ...defaultInstallDeps, ...Object.fromEntries(Object.entries(depsIn).filter(([, v]) => v !== undefined)) };
|
|
315
|
+
const root = opts.root ?? "";
|
|
316
|
+
const start = opts.start ?? true;
|
|
317
|
+
if (!isValidReleaseVersion(opts.version))
|
|
318
|
+
throw new InstallError(`invalid version "${opts.version}"`);
|
|
319
|
+
const asset = binaryAssetName(opts.arch);
|
|
320
|
+
const dl = (name) => releaseAssetUrl(opts.version, name, opts.releasesBase);
|
|
321
|
+
deps.log(`downloading zr-agent ${opts.version} (${asset})`);
|
|
322
|
+
const [sums, sig, bin] = await Promise.all([
|
|
323
|
+
deps.fetchBytes(dl(SUMS_ASSET)),
|
|
324
|
+
deps.fetchBytes(dl(SUMS_SIGNATURE_ASSET)),
|
|
325
|
+
deps.fetchBytes(dl(asset))
|
|
326
|
+
]);
|
|
327
|
+
const verdict = verifyReleaseBinary({
|
|
328
|
+
version: opts.version,
|
|
329
|
+
assetName: asset,
|
|
330
|
+
bytes: bin,
|
|
331
|
+
sums,
|
|
332
|
+
signature: Buffer.from(sig).toString("utf8"),
|
|
333
|
+
publicKey: opts.publicKey
|
|
334
|
+
});
|
|
335
|
+
if (!verdict.ok)
|
|
336
|
+
throw new InstallError(`refusing to install: ${verdict.reason}`);
|
|
337
|
+
deps.log(`verified signature and checksum (sha256 ${verdict.sha256.slice(0, 12)}...)`);
|
|
338
|
+
const base = join(root, INSTALL_ROOT);
|
|
339
|
+
const versionsDir = join(base, "versions");
|
|
340
|
+
const current = join(base, "current");
|
|
341
|
+
const previous = await currentVersion(current);
|
|
342
|
+
await writeAtomic(join(versionsDir, opts.version, BINARY_NAME), bin, 493);
|
|
343
|
+
await writeAtomic(join(versionsDir, opts.version, ".verified"), `${JSON.stringify({ version: opts.version, asset, sha256: verdict.sha256, bytes: bin.length, atMs: Date.now() })}
|
|
344
|
+
`, 420);
|
|
345
|
+
await writeAtomic(join(root, LAUNCHER_BIN), bin, 493);
|
|
346
|
+
await rm(join(base, "upgrade-pending.json"), { force: true });
|
|
347
|
+
await flipSymlink(current, join("versions", opts.version));
|
|
348
|
+
deps.log(`installed ${join(INSTALL_ROOT, "versions", opts.version, BINARY_NAME)}`);
|
|
349
|
+
await mkdir(dirname(join(root, BIN_LINK)), { recursive: true });
|
|
350
|
+
await flipSymlink(join(root, BIN_LINK), join(INSTALL_ROOT, "current", BINARY_NAME));
|
|
351
|
+
await pruneVersions(versionsDir, new Set([opts.version, ...previous ? [previous] : []]), deps.log);
|
|
352
|
+
await writeAtomic(join(root, UNIT_PATH), SYSTEMD_UNIT, 420);
|
|
353
|
+
deps.log(`wrote ${UNIT_PATH}`);
|
|
354
|
+
const configPath = join(root, CONFIG_PATH);
|
|
355
|
+
const configured = await deps.fileExists(configPath);
|
|
356
|
+
const agent = join(current, BINARY_NAME);
|
|
357
|
+
if (opts.join) {
|
|
358
|
+
if (!opts.url)
|
|
359
|
+
throw new InstallError("--url <control plane URL> is required with --join");
|
|
360
|
+
const args = ["register"];
|
|
361
|
+
if (opts.name)
|
|
362
|
+
args.push("--name", opts.name);
|
|
363
|
+
if (opts.forceRegister)
|
|
364
|
+
args.push("--force");
|
|
365
|
+
if (root)
|
|
366
|
+
args.push("--config", configPath);
|
|
367
|
+
const r = await deps.exec(agent, args, { ZR_AGENT_JOIN_TOKEN: opts.join, ZR_AGENT_URL: opts.url });
|
|
368
|
+
if (r.code !== 0)
|
|
369
|
+
throw new InstallError(`zr-agent register failed (exit ${r.code}); the binary is installed, fix the problem and run: zr-agent register --join <token> --url <url>`);
|
|
370
|
+
} else if (!configured) {
|
|
371
|
+
deps.log(`not registered yet: run "zr-agent register --join <zrj_ token> --url <control plane URL>" then "systemctl enable --now ${UNIT_NAME}"`);
|
|
372
|
+
if (start)
|
|
373
|
+
deps.log("skipping service start (no config)");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!start) {
|
|
377
|
+
deps.log(`start it with: systemctl enable --now ${UNIT_NAME}`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
for (const args of [["daemon-reload"], ["enable", UNIT_NAME], ["restart", UNIT_NAME]]) {
|
|
381
|
+
const r = await deps.exec("systemctl", args, {});
|
|
382
|
+
if (r.code !== 0)
|
|
383
|
+
throw new InstallError(`systemctl ${args.join(" ")} failed (exit ${r.code})`);
|
|
384
|
+
}
|
|
385
|
+
deps.log(`zr-agent ${opts.version} is running (journalctl -u ${UNIT_NAME} -f)`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ../../dashboard/packages/agent-protocol/src/constants.ts
|
|
389
|
+
var MAX_FRAME_BYTES = 4 * 1024 * 1024;
|
|
390
|
+
var JOIN_TOKEN_TTL_MS = 15 * 60000;
|
|
391
|
+
var JOIN_TOKEN_PREFIX = "zrj_";
|
|
392
|
+
var JOIN_TOKEN_BYTES = 32;
|
|
393
|
+
|
|
394
|
+
// ../../dashboard/packages/agent-protocol/src/crypto.ts
|
|
395
|
+
var TOKEN_BODY_LEN = Math.ceil(JOIN_TOKEN_BYTES * 4 / 3);
|
|
396
|
+
var TOKEN_RE = new RegExp(`^${JOIN_TOKEN_PREFIX}[A-Za-z0-9_-]{${TOKEN_BODY_LEN}}$`);
|
|
397
|
+
function isJoinTokenFormat(token) {
|
|
398
|
+
return typeof token === "string" && TOKEN_RE.test(token);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/cli.ts
|
|
402
|
+
var USAGE = `@w3devx/zr-agent <version>
|
|
403
|
+
|
|
404
|
+
Usage:
|
|
405
|
+
npx @w3devx/zr-agent@<version> install --join <zrj_token> --url <control plane URL> [--name <name>] [--no-start]
|
|
406
|
+
|
|
407
|
+
Downloads zr-agent <version> (this package's own version), verifies the minisign signature and SHA-256 checksum,
|
|
408
|
+
installs it to /opt/zr-agent/versions/<version>/, points /opt/zr-agent/current at it, writes the zr-agent systemd unit,
|
|
409
|
+
registers the node and starts the service. Run it as root (sudo).
|
|
410
|
+
|
|
411
|
+
Flags (the token and URL are also read from ZR_AGENT_JOIN_TOKEN and ZR_AGENT_URL):
|
|
412
|
+
--join <token> join token from the dashboard's Add node dialog
|
|
413
|
+
--url <url> control plane URL
|
|
414
|
+
--name <name> node name (default: hostname)
|
|
415
|
+
--force register again even if /etc/zr-agent/config.json exists
|
|
416
|
+
--no-start install the files and unit only
|
|
417
|
+
--version <v> install a different release than this package's version
|
|
418
|
+
`;
|
|
419
|
+
var VALUE_FLAGS = new Set(["join", "url", "name", "version", "root", "releases-base"]);
|
|
420
|
+
var BOOL_FLAGS = new Set(["force", "no-start", "help"]);
|
|
421
|
+
function parseCliArgs(argv) {
|
|
422
|
+
const flags = {};
|
|
423
|
+
let command = null;
|
|
424
|
+
for (let i = 0;i < argv.length; i++) {
|
|
425
|
+
const a = argv[i];
|
|
426
|
+
if (a === "-h") {
|
|
427
|
+
flags.help = true;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (!a.startsWith("--")) {
|
|
431
|
+
if (command !== null)
|
|
432
|
+
return { command, flags, error: `unexpected argument "${a}"` };
|
|
433
|
+
command = a;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const eq = a.indexOf("=");
|
|
437
|
+
const name = eq === -1 ? a.slice(2) : a.slice(2, eq);
|
|
438
|
+
if (BOOL_FLAGS.has(name)) {
|
|
439
|
+
if (eq !== -1)
|
|
440
|
+
return { command, flags, error: `--${name} takes no value` };
|
|
441
|
+
flags[name] = true;
|
|
442
|
+
} else if (VALUE_FLAGS.has(name)) {
|
|
443
|
+
const value = eq !== -1 ? a.slice(eq + 1) : argv[++i];
|
|
444
|
+
if (value === undefined || value === "" || eq === -1 && value.startsWith("--"))
|
|
445
|
+
return { command, flags, error: `--${name} needs a value` };
|
|
446
|
+
flags[name] = value;
|
|
447
|
+
} else {
|
|
448
|
+
return { command, flags, error: `unknown flag --${name}` };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return { command, flags, error: null };
|
|
452
|
+
}
|
|
453
|
+
var str = (v) => typeof v === "string" ? v : undefined;
|
|
454
|
+
async function runCli(argv, deps) {
|
|
455
|
+
const p = parseCliArgs(argv);
|
|
456
|
+
if (p.error) {
|
|
457
|
+
deps.err(`zr-agent: ${p.error}`);
|
|
458
|
+
deps.err(USAGE);
|
|
459
|
+
return 2;
|
|
460
|
+
}
|
|
461
|
+
if (p.flags.help || p.command === null || p.command === "help") {
|
|
462
|
+
deps.out(USAGE);
|
|
463
|
+
return p.command === null && !p.flags.help ? 2 : 0;
|
|
464
|
+
}
|
|
465
|
+
if (p.command === "version") {
|
|
466
|
+
deps.out(deps.packageVersion);
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
if (p.command !== "install") {
|
|
470
|
+
deps.err(`zr-agent: unknown command "${p.command}" (this package only installs; after install use the zr-agent binary: zr-agent status)`);
|
|
471
|
+
return 2;
|
|
472
|
+
}
|
|
473
|
+
if (deps.platform !== "linux") {
|
|
474
|
+
deps.err(`zr-agent install: linux only (this is ${deps.platform})`);
|
|
475
|
+
return 1;
|
|
476
|
+
}
|
|
477
|
+
const arch = releaseArch(deps.arch);
|
|
478
|
+
if (!arch) {
|
|
479
|
+
deps.err(`zr-agent install: no build for CPU architecture "${deps.arch}" (x64 and arm64 only)`);
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
|
482
|
+
const version = str(p.flags.version) ?? deps.packageVersion;
|
|
483
|
+
if (!isValidReleaseVersion(version) || version === "0.0.0") {
|
|
484
|
+
deps.err(`zr-agent install: "${version}" is not a released version; pass --version <x.y.z> or use npx @w3devx/zr-agent@<x.y.z>`);
|
|
485
|
+
return 2;
|
|
486
|
+
}
|
|
487
|
+
const root = str(p.flags.root) ?? "";
|
|
488
|
+
if (root === "" && deps.uid !== 0) {
|
|
489
|
+
deps.err("zr-agent install: must run as root (try: sudo npx @w3devx/zr-agent@" + version + " install ...)");
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
const start = p.flags["no-start"] !== true;
|
|
493
|
+
if (start && root === "" && !deps.hasSystemd()) {
|
|
494
|
+
deps.err("zr-agent install: systemd not found; pass --no-start to install the files only");
|
|
495
|
+
return 1;
|
|
496
|
+
}
|
|
497
|
+
const join2 = str(p.flags.join) ?? deps.env.ZR_AGENT_JOIN_TOKEN?.trim();
|
|
498
|
+
const url = str(p.flags.url) ?? deps.env.ZR_AGENT_URL?.trim();
|
|
499
|
+
if (join2 !== undefined && join2 !== "" && !isJoinTokenFormat(join2)) {
|
|
500
|
+
deps.err("zr-agent install: the join token is malformed (expected zrj_ followed by 43 base64url characters)");
|
|
501
|
+
return 2;
|
|
502
|
+
}
|
|
503
|
+
if (join2 && !url) {
|
|
504
|
+
deps.err("zr-agent install: --url <control plane URL> is required with --join");
|
|
505
|
+
return 2;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
await deps.install({
|
|
509
|
+
version,
|
|
510
|
+
arch,
|
|
511
|
+
root,
|
|
512
|
+
join: join2 || undefined,
|
|
513
|
+
url,
|
|
514
|
+
name: str(p.flags.name),
|
|
515
|
+
forceRegister: p.flags.force === true,
|
|
516
|
+
start,
|
|
517
|
+
releasesBase: str(p.flags["releases-base"])
|
|
518
|
+
}, { fetchBytes: deps.fetchBytes, exec: deps.exec, fileExists: deps.fileExists, log: deps.log ?? deps.out });
|
|
519
|
+
} catch (e) {
|
|
520
|
+
deps.err(`zr-agent install: ${e instanceof InstallError ? e.message : e instanceof Error ? e.message : String(e)}`);
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
523
|
+
return 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/main.ts
|
|
527
|
+
var code = await runCli(process.argv.slice(2), {
|
|
528
|
+
packageVersion: package_default.version,
|
|
529
|
+
platform: process.platform,
|
|
530
|
+
arch: process.arch,
|
|
531
|
+
uid: typeof process.geteuid === "function" ? process.geteuid() : null,
|
|
532
|
+
env: process.env,
|
|
533
|
+
out: (line) => console.log(line),
|
|
534
|
+
err: (line) => console.error(line),
|
|
535
|
+
hasSystemd: () => existsSync("/run/systemd/system"),
|
|
536
|
+
install: installAgent
|
|
537
|
+
});
|
|
538
|
+
process.exitCode = code;
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@w3devx/zr-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-rc.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Installer for zr-agent, the zyber runner node agent: downloads the exact release, verifies its minisign signature and checksum, installs it under /opt/zr-agent and writes the systemd unit.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zr-agent": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"os": [
|
|
17
|
+
"linux"
|
|
18
|
+
],
|
|
19
|
+
"cpu": [
|
|
20
|
+
"x64",
|
|
21
|
+
"arm64"
|
|
22
|
+
],
|
|
23
|
+
"license": "UNLICENSED",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/ashutoshpw/zyber-sh-manager.git",
|
|
27
|
+
"directory": "npm/zr-agent"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "bun build src/main.ts --target node --outfile dist/cli.js && chmod +x dist/cli.js",
|
|
34
|
+
"test": "bun test"
|
|
35
|
+
}
|
|
36
|
+
}
|