@nordbyte/nordrelay 0.6.0 → 0.7.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/.env.example +17 -0
- package/README.md +67 -6
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +77 -6
- package/dist/channel-adapter.js +11 -5
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +214 -1
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +15 -0
- package/dist/config.js +31 -6
- package/dist/context-key.js +10 -0
- package/dist/discord-bot.js +85 -26
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +20 -0
- package/dist/metrics.js +46 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +72 -274
- package/dist/remote-prompt.js +98 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-general-commands.js +14 -0
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/web-api-contract.js +8 -0
- package/dist/web-dashboard-pages.js +12 -0
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +12 -0
- package/dist/webui-assets/dashboard.js +427 -14
- package/package.json +3 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +373 -7
|
@@ -35,6 +35,10 @@ function readRuntimePackageVersion() {
|
|
|
35
35
|
function parseArgs(argv) {
|
|
36
36
|
const copy = [...argv];
|
|
37
37
|
let command = "foreground";
|
|
38
|
+
if (copy[0] === "--help" || copy[0] === "-h") {
|
|
39
|
+
command = "help";
|
|
40
|
+
copy.shift();
|
|
41
|
+
}
|
|
38
42
|
if (copy[0] === "--version" || copy[0] === "-v") {
|
|
39
43
|
command = "version";
|
|
40
44
|
copy.shift();
|
|
@@ -53,6 +57,7 @@ function parseArgs(argv) {
|
|
|
53
57
|
port: undefined,
|
|
54
58
|
restartAfterUpdate: true,
|
|
55
59
|
updateMethod: undefined,
|
|
60
|
+
buildBeforeStart: false,
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
for (let i = 0; i < copy.length; i += 1) {
|
|
@@ -63,6 +68,7 @@ function parseArgs(argv) {
|
|
|
63
68
|
else if (arg === "--host") options.host = requireValue(copy, ++i, arg);
|
|
64
69
|
else if (arg === "--port") options.port = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
65
70
|
else if (arg === "--method") options.updateMethod = requireValue(copy, ++i, arg);
|
|
71
|
+
else if (arg === "--build") options.buildBeforeStart = true;
|
|
66
72
|
else if (arg === "--no-restart") options.restartAfterUpdate = false;
|
|
67
73
|
else if (arg === "--restart") options.restartAfterUpdate = true;
|
|
68
74
|
else if (arg === "--token") options.telegramBotToken = requireValue(copy, ++i, arg);
|
|
@@ -226,6 +232,7 @@ function formatDashboardUrl(endpoint) {
|
|
|
226
232
|
async function commandStart(options, settings = {}) {
|
|
227
233
|
await mkdirp(options.home);
|
|
228
234
|
loadEnvFiles(options.home);
|
|
235
|
+
await prepareRuntimeForLaunch(options);
|
|
229
236
|
const dashboard = resolveDashboardEndpoint(options);
|
|
230
237
|
|
|
231
238
|
const currentPid = await readPid(options.pidFile);
|
|
@@ -243,7 +250,7 @@ async function commandStart(options, settings = {}) {
|
|
|
243
250
|
});
|
|
244
251
|
|
|
245
252
|
const logFd = fs.openSync(options.logFile, "a");
|
|
246
|
-
const child = spawn(process.execPath, [SCRIPT_PATH, "foreground", ...options.rawFlags], {
|
|
253
|
+
const child = spawn(process.execPath, [SCRIPT_PATH, "foreground", ...runtimeForwardFlags(options.rawFlags)], {
|
|
247
254
|
cwd: RUNTIME_ROOT,
|
|
248
255
|
detached: true,
|
|
249
256
|
env: process.env,
|
|
@@ -410,6 +417,8 @@ async function commandStatus(options) {
|
|
|
410
417
|
console.log(`OpenClaw CLI: ${state.openClawCli || "-"}`);
|
|
411
418
|
console.log(`Claude Code CLI: ${state.claudeCodeCli || "-"}`);
|
|
412
419
|
console.log(`OpenClaw Gateway: ${state.openClawGateway || process.env.OPENCLAW_GATEWAY_URL || "-"}`);
|
|
420
|
+
console.log(`Peers: ${state.peerEnabled ? state.peerUrl || "enabled" : "disabled"}`);
|
|
421
|
+
if (state.peerTlsFingerprint) console.log(`Peer TLS fingerprint: ${state.peerTlsFingerprint}`);
|
|
413
422
|
console.log(`WebUI: ${formatDashboardUrl(dashboard)} (${webStatus})`);
|
|
414
423
|
console.log(`Log: ${options.logFile}`);
|
|
415
424
|
console.log(`WebUI log: ${options.webLogFile}`);
|
|
@@ -727,6 +736,11 @@ async function commandInit(options) {
|
|
|
727
736
|
"OPENCLAW_DEFAULT_PROFILE=default",
|
|
728
737
|
"CLAUDE_CODE_DEFAULT_PROFILE=default",
|
|
729
738
|
"CLAUDE_CODE_MAX_TURNS=100",
|
|
739
|
+
"NORDRELAY_PEER_ENABLED=false",
|
|
740
|
+
"NORDRELAY_PEER_HOST=127.0.0.1",
|
|
741
|
+
"NORDRELAY_PEER_PORT=31979",
|
|
742
|
+
"NORDRELAY_PEER_TLS_ENABLED=true",
|
|
743
|
+
"NORDRELAY_PEER_REQUIRE_TLS=true",
|
|
730
744
|
`NORDRELAY_STATE_BACKEND=${stateBackend === "sqlite" ? "sqlite" : "json"}`,
|
|
731
745
|
"TELEGRAM_TRANSPORT=polling",
|
|
732
746
|
"TELEGRAM_AUTO_SEND_ARTIFACTS=false",
|
|
@@ -755,6 +769,142 @@ async function createUserStore(home) {
|
|
|
755
769
|
return new mod.UserStore(home);
|
|
756
770
|
}
|
|
757
771
|
|
|
772
|
+
async function peerModules() {
|
|
773
|
+
const required = [
|
|
774
|
+
"peer-store.js",
|
|
775
|
+
"peer-identity.js",
|
|
776
|
+
"peer-client.js",
|
|
777
|
+
];
|
|
778
|
+
for (const file of required) {
|
|
779
|
+
const modulePath = path.join(RUNTIME_ROOT, "dist", file);
|
|
780
|
+
if (!fs.existsSync(modulePath)) {
|
|
781
|
+
throw new Error(`Missing peer runtime. Run \`npm run build\` in ${RUNTIME_ROOT}.`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const [store, identity, client] = await Promise.all(required.map((file) => import(pathToFileURL(path.join(RUNTIME_ROOT, "dist", file)).href)));
|
|
785
|
+
return { store, identity, client };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function parsePeerFlags(argv) {
|
|
789
|
+
const copy = [...argv];
|
|
790
|
+
const subcommand = copy[0] && !copy[0].startsWith("-") ? copy.shift() : "list";
|
|
791
|
+
const flags = { subcommand, url: undefined };
|
|
792
|
+
if (["add", "test", "revoke"].includes(subcommand) && copy[0] && !copy[0].startsWith("-")) {
|
|
793
|
+
flags.url = copy.shift();
|
|
794
|
+
flags.id = flags.url;
|
|
795
|
+
}
|
|
796
|
+
for (let i = 0; i < copy.length; i += 1) {
|
|
797
|
+
const arg = copy[i];
|
|
798
|
+
if (arg === "--name") flags.name = requireValue(copy, ++i, arg);
|
|
799
|
+
else if (arg === "--code") flags.code = requireValue(copy, ++i, arg);
|
|
800
|
+
else if (arg === "--public-url") flags.publicUrl = requireValue(copy, ++i, arg);
|
|
801
|
+
else if (arg === "--expires" || arg === "--expires-minutes") flags.expiresMinutes = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
802
|
+
else if (arg === "--scopes") flags.scopes = requireValue(copy, ++i, arg);
|
|
803
|
+
else if (arg === "--agents") flags.agents = requireValue(copy, ++i, arg);
|
|
804
|
+
else if (arg === "--workspaces") flags.workspaces = requireValue(copy, ++i, arg);
|
|
805
|
+
else if (arg === "--workspace-aliases" || arg === "--aliases") flags.workspaceAliases = requireValue(copy, ++i, arg);
|
|
806
|
+
}
|
|
807
|
+
return flags;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function csv(value) {
|
|
811
|
+
return value ? value.split(",").map((item) => item.trim()).filter(Boolean) : undefined;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function aliasMap(value) {
|
|
815
|
+
if (!value) return undefined;
|
|
816
|
+
return Object.fromEntries(value.split(",").map((item) => item.split("=", 2).map((part) => part.trim())).filter(([alias, workspace]) => alias && workspace));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function commandPeer(options) {
|
|
820
|
+
await mkdirp(options.home);
|
|
821
|
+
loadEnvFiles(options.home);
|
|
822
|
+
const flags = parsePeerFlags(options.rawFlags);
|
|
823
|
+
const { store: storeMod, identity: identityMod, client: clientMod } = await peerModules();
|
|
824
|
+
const store = new storeMod.PeerStore(options.home);
|
|
825
|
+
const identity = identityMod.loadOrCreatePeerIdentity(options.home, process.env.NORDRELAY_PEER_NAME);
|
|
826
|
+
|
|
827
|
+
if (flags.subcommand === "identity") {
|
|
828
|
+
console.log(`Node ID: ${identity.public.nodeId}`);
|
|
829
|
+
console.log(`Name: ${identity.public.name}`);
|
|
830
|
+
console.log(`Fingerprint: ${identity.public.fingerprint}`);
|
|
831
|
+
console.log(`Created: ${identity.public.createdAt}`);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (flags.subcommand === "list") {
|
|
836
|
+
const peers = store.listPublic();
|
|
837
|
+
if (peers.length === 0) {
|
|
838
|
+
console.log("No peers configured.");
|
|
839
|
+
console.log("Create an invite with `nordrelay peer invite` or add a peer with `nordrelay peer add <url> --code <code>`.");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
for (const peer of peers) {
|
|
843
|
+
console.log(`${peer.id} ${peer.enabled ? "enabled" : "disabled"} ${peer.name}`);
|
|
844
|
+
console.log(` URL: ${peer.url || "-"}`);
|
|
845
|
+
console.log(` Node: ${peer.nodeId} ${peer.fingerprint}`);
|
|
846
|
+
console.log(` Direction: ${peer.direction}`);
|
|
847
|
+
console.log(` Scopes: ${peer.scopes.join(",") || "-"}`);
|
|
848
|
+
console.log(` Agents: ${peer.allowedAgents.join(",") || "all"}`);
|
|
849
|
+
const aliases = Object.entries(peer.workspaceAliases || {}).map(([alias, workspace]) => `${alias}=${workspace}`).join(",");
|
|
850
|
+
if (aliases) console.log(` Workspace aliases: ${aliases}`);
|
|
851
|
+
if (peer.lastSeenAt) console.log(` Last seen: ${peer.lastSeenAt}`);
|
|
852
|
+
if (peer.lastLatencyMs !== undefined) console.log(` Latency: ${peer.lastLatencyMs}ms`);
|
|
853
|
+
if (peer.remoteVersion) console.log(` Remote version: ${peer.remoteVersion}`);
|
|
854
|
+
if (peer.lastError) console.log(` Last error: ${peer.lastError}`);
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (flags.subcommand === "invite") {
|
|
860
|
+
const url = process.env.NORDRELAY_PEER_PUBLIC_URL || `${process.env.NORDRELAY_PEER_TLS_ENABLED === "false" ? "http" : "https"}://${process.env.NORDRELAY_PEER_HOST || "127.0.0.1"}:${process.env.NORDRELAY_PEER_PORT || "31979"}`;
|
|
861
|
+
const created = store.createInvitation({
|
|
862
|
+
name: flags.name,
|
|
863
|
+
expiresInMs: Number.isFinite(flags.expiresMinutes) ? flags.expiresMinutes * 60 * 1000 : undefined,
|
|
864
|
+
scopes: csv(flags.scopes),
|
|
865
|
+
allowedAgents: csv(flags.agents),
|
|
866
|
+
allowedWorkspaceRoots: csv(flags.workspaces),
|
|
867
|
+
workspaceAliases: aliasMap(flags.workspaceAliases),
|
|
868
|
+
});
|
|
869
|
+
console.log(`Pairing code: ${created.code}`);
|
|
870
|
+
console.log(`Expires: ${created.invitation.expiresAt}`);
|
|
871
|
+
console.log(`Fingerprint: ${identity.public.fingerprint}`);
|
|
872
|
+
console.log(`Command: nordrelay peer add ${url} --code ${created.code}`);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (flags.subcommand === "add") {
|
|
877
|
+
const url = flags.url || await ask(null, "Peer URL", "");
|
|
878
|
+
const code = flags.code || await ask(null, "Pairing code", "");
|
|
879
|
+
const result = await clientMod.pairPeer({
|
|
880
|
+
url,
|
|
881
|
+
code,
|
|
882
|
+
name: flags.name,
|
|
883
|
+
publicUrl: flags.publicUrl,
|
|
884
|
+
}, identity, store);
|
|
885
|
+
console.log(`Added peer ${result.peer.name} (${result.peer.id}).`);
|
|
886
|
+
console.log(`Node: ${result.peer.nodeId}`);
|
|
887
|
+
console.log(`Fingerprint: ${result.peer.fingerprint}`);
|
|
888
|
+
if (result.tlsFingerprint) console.log(`TLS fingerprint: ${result.tlsFingerprint}`);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (flags.subcommand === "test") {
|
|
893
|
+
const id = flags.id || await ask(null, "Peer id", "");
|
|
894
|
+
const response = await new clientMod.RemoteRelayClient(store).rpc(id, "peer.ping");
|
|
895
|
+
console.log(`Peer ${id} ok: ${JSON.stringify(response)}`);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (flags.subcommand === "revoke") {
|
|
900
|
+
const id = flags.id || await ask(null, "Peer id", "");
|
|
901
|
+
console.log(store.revokePeer(id) ? `Revoked peer ${id}.` : `Peer not found: ${id}`);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
throw new Error("Usage: nordrelay peer [identity|list|invite|add|test|revoke]");
|
|
906
|
+
}
|
|
907
|
+
|
|
758
908
|
function parseUserFlags(argv) {
|
|
759
909
|
const copy = [...argv];
|
|
760
910
|
const subcommand = copy[0] && !copy[0].startsWith("-") ? copy.shift() : "list";
|
|
@@ -864,16 +1014,39 @@ async function commandDoctor(options) {
|
|
|
864
1014
|
const userSnapshot = userStore?.snapshot();
|
|
865
1015
|
const checks = [];
|
|
866
1016
|
checks.push(check("Node.js >= 22", Number.parseInt(process.versions.node.split(".")[0], 10) >= 22, process.version));
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
checks.push(check(
|
|
1017
|
+
const telegramRequested = process.env.TELEGRAM_ENABLED !== "false";
|
|
1018
|
+
const discordRequested = process.env.DISCORD_ENABLED === "true";
|
|
1019
|
+
const telegramUsable = telegramRequested && Boolean(process.env.TELEGRAM_BOT_TOKEN);
|
|
1020
|
+
const discordUsable = discordRequested && Boolean(process.env.DISCORD_BOT_TOKEN);
|
|
1021
|
+
checks.push(check(
|
|
1022
|
+
"Telegram bot token",
|
|
1023
|
+
!telegramRequested || telegramUsable,
|
|
1024
|
+
telegramRequested ? (telegramUsable ? "configured" : "missing; Telegram adapter will be disabled") : "disabled",
|
|
1025
|
+
telegramRequested && !discordUsable ? "fail" : "warn",
|
|
1026
|
+
));
|
|
1027
|
+
checks.push(check(
|
|
1028
|
+
"Discord bot token",
|
|
1029
|
+
!discordRequested || discordUsable,
|
|
1030
|
+
discordRequested ? (discordUsable ? "configured" : "missing; Discord adapter will be disabled") : "disabled",
|
|
1031
|
+
discordRequested && !telegramUsable ? "fail" : "warn",
|
|
1032
|
+
));
|
|
1033
|
+
checks.push(check(
|
|
1034
|
+
"Usable chat adapter",
|
|
1035
|
+
telegramUsable || discordUsable,
|
|
1036
|
+
telegramUsable && discordUsable ? "Telegram and Discord" : telegramUsable ? "Telegram" : discordUsable ? "Discord" : "none",
|
|
1037
|
+
"fail",
|
|
1038
|
+
));
|
|
1039
|
+
checks.push(check("Discord client ID", !discordUsable || Boolean(process.env.DISCORD_CLIENT_ID), discordUsable ? (process.env.DISCORD_CLIENT_ID ? "configured" : "missing; slash command auto-registration disabled") : "disabled", "warn"));
|
|
872
1040
|
checks.push(check("User store", Boolean(userStore), userStore ? userStore.filePath : "missing runtime", userStore ? "pass" : "fail"));
|
|
873
1041
|
checks.push(check("Admin user", Boolean(userSnapshot?.adminConfigured), userSnapshot?.adminConfigured ? "configured" : "missing"));
|
|
874
1042
|
checks.push(check("WebUI login", true, "required for every dashboard request"));
|
|
875
1043
|
checks.push(check("Telegram access", true, "requires linked active users and enabled group chats"));
|
|
876
1044
|
checks.push(check("Discord access", true, "requires linked active users and enabled channels"));
|
|
1045
|
+
const peerEnabled = process.env.NORDRELAY_PEER_ENABLED === "true";
|
|
1046
|
+
const peerTlsEnabled = process.env.NORDRELAY_PEER_TLS_ENABLED !== "false";
|
|
1047
|
+
const peerHost = process.env.NORDRELAY_PEER_HOST || "127.0.0.1";
|
|
1048
|
+
checks.push(check("Peer server", peerEnabled, peerEnabled ? `${peerHost}:${process.env.NORDRELAY_PEER_PORT || "31979"}` : "disabled", "warn"));
|
|
1049
|
+
checks.push(check("Peer TLS", !peerEnabled || peerTlsEnabled || isLoopbackName(peerHost), peerTlsEnabled ? "enabled" : "plaintext loopback only", peerEnabled ? "fail" : "warn"));
|
|
877
1050
|
checks.push(check("Codex enabled flag", process.env.NORDRELAY_CODEX_ENABLED !== "false", `NORDRELAY_CODEX_ENABLED=${process.env.NORDRELAY_CODEX_ENABLED ?? "true"}`));
|
|
878
1051
|
checks.push(check("Pi enabled flag", process.env.NORDRELAY_PI_ENABLED === "true" || process.env.NORDRELAY_PI_ENABLED === undefined, `NORDRELAY_PI_ENABLED=${process.env.NORDRELAY_PI_ENABLED ?? "false"}`, process.env.NORDRELAY_PI_ENABLED === "true" ? "pass" : "warn"));
|
|
879
1052
|
checks.push(check("Hermes enabled flag", process.env.NORDRELAY_HERMES_ENABLED === "true", `NORDRELAY_HERMES_ENABLED=${process.env.NORDRELAY_HERMES_ENABLED ?? "false"}`, process.env.NORDRELAY_HERMES_ENABLED === "true" ? "pass" : "warn"));
|
|
@@ -978,6 +1151,7 @@ async function checkOpenClawGateway() {
|
|
|
978
1151
|
async function commandWeb(options) {
|
|
979
1152
|
await mkdirp(options.home);
|
|
980
1153
|
loadEnvFiles(options.home);
|
|
1154
|
+
await prepareRuntimeForLaunch(options);
|
|
981
1155
|
await ensureConnectorStartedForWeb(options);
|
|
982
1156
|
await startWebDashboard(options, { detached: false });
|
|
983
1157
|
}
|
|
@@ -1077,6 +1251,7 @@ async function startWebDashboard(options, settings = {}) {
|
|
|
1077
1251
|
async function commandForeground(options) {
|
|
1078
1252
|
await mkdirp(options.home);
|
|
1079
1253
|
loadEnvFiles(options.home);
|
|
1254
|
+
await prepareRuntimeForLaunch(options);
|
|
1080
1255
|
process.chdir(RUNTIME_ROOT);
|
|
1081
1256
|
|
|
1082
1257
|
await writeJsonAtomic(options.stateFile, {
|
|
@@ -1162,6 +1337,157 @@ async function resolveRuntimeEntry() {
|
|
|
1162
1337
|
return null;
|
|
1163
1338
|
}
|
|
1164
1339
|
|
|
1340
|
+
async function prepareRuntimeForLaunch(options) {
|
|
1341
|
+
if (options.buildBeforeStart) {
|
|
1342
|
+
await buildRuntime();
|
|
1343
|
+
options.buildBeforeStart = false;
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
warnIfRuntimeBuildIsStale();
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function runtimeForwardFlags(flags) {
|
|
1350
|
+
return flags.filter((flag) => flag !== "--build");
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
async function buildRuntime() {
|
|
1354
|
+
if (!isSourceRuntime()) {
|
|
1355
|
+
throw new Error(`Runtime build is only available from a source checkout. Current runtime: ${RUNTIME_ROOT}`);
|
|
1356
|
+
}
|
|
1357
|
+
const npm = resolveNpmSpawnCommand();
|
|
1358
|
+
if (!npm) {
|
|
1359
|
+
throw new Error("npm was not found. Install Node.js/npm or add npm to PATH.");
|
|
1360
|
+
}
|
|
1361
|
+
console.log("Building NordRelay runtime...");
|
|
1362
|
+
await runInteractiveStep("Build runtime", npm.command, [...npm.argsPrefix, "run", "build"], {
|
|
1363
|
+
cwd: RUNTIME_ROOT,
|
|
1364
|
+
shell: npm.shell,
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function warnIfRuntimeBuildIsStale() {
|
|
1369
|
+
const status = runtimeBuildStatus();
|
|
1370
|
+
if (!status || !status.stale) {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const source = status.sourcePath ? path.relative(RUNTIME_ROOT, status.sourcePath) : "source files";
|
|
1374
|
+
const target = status.targetPath ? path.relative(RUNTIME_ROOT, status.targetPath) : "dist";
|
|
1375
|
+
const reason = status.missing
|
|
1376
|
+
? `missing ${target}`
|
|
1377
|
+
: `${source} is newer than ${target}`;
|
|
1378
|
+
console.warn(`Warning: NordRelay runtime build may be stale (${reason}). Run \`nordrelay restart --build\` or \`npm run build\`.`);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function runtimeBuildStatus() {
|
|
1382
|
+
if (!isSourceRuntime()) {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
const source = newestMtime([
|
|
1386
|
+
path.join(RUNTIME_ROOT, "src"),
|
|
1387
|
+
path.join(RUNTIME_ROOT, "plugins", "nordrelay", "scripts"),
|
|
1388
|
+
path.join(RUNTIME_ROOT, "scripts"),
|
|
1389
|
+
path.join(RUNTIME_ROOT, "package.json"),
|
|
1390
|
+
path.join(RUNTIME_ROOT, "tsconfig.json"),
|
|
1391
|
+
path.join(RUNTIME_ROOT, "tsconfig.webui.json"),
|
|
1392
|
+
]);
|
|
1393
|
+
const distTargets = [
|
|
1394
|
+
path.join(RUNTIME_ROOT, "dist", "index.js"),
|
|
1395
|
+
path.join(RUNTIME_ROOT, "dist", "web-dashboard.js"),
|
|
1396
|
+
path.join(RUNTIME_ROOT, "dist", "webui-assets", "dashboard.js"),
|
|
1397
|
+
path.join(RUNTIME_ROOT, "dist", "webui-assets", "dashboard.css"),
|
|
1398
|
+
];
|
|
1399
|
+
const missingTarget = distTargets.find((target) => !fs.existsSync(target));
|
|
1400
|
+
if (missingTarget) {
|
|
1401
|
+
return {
|
|
1402
|
+
stale: true,
|
|
1403
|
+
missing: true,
|
|
1404
|
+
sourcePath: source.path,
|
|
1405
|
+
sourceMtimeMs: source.mtimeMs,
|
|
1406
|
+
targetPath: missingTarget,
|
|
1407
|
+
targetMtimeMs: 0,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
const target = oldestMtime(distTargets);
|
|
1411
|
+
return {
|
|
1412
|
+
stale: source.mtimeMs > target.mtimeMs,
|
|
1413
|
+
missing: false,
|
|
1414
|
+
sourcePath: source.path,
|
|
1415
|
+
sourceMtimeMs: source.mtimeMs,
|
|
1416
|
+
targetPath: target.path,
|
|
1417
|
+
targetMtimeMs: target.mtimeMs,
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function isSourceRuntime() {
|
|
1422
|
+
return fs.existsSync(path.join(RUNTIME_ROOT, "src", "index.ts")) &&
|
|
1423
|
+
fs.existsSync(path.join(RUNTIME_ROOT, "scripts", "build-web-assets.mjs"));
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function newestMtime(paths) {
|
|
1427
|
+
let newest = { path: null, mtimeMs: 0 };
|
|
1428
|
+
for (const itemPath of paths) {
|
|
1429
|
+
const candidate = newestMtimeForPath(itemPath);
|
|
1430
|
+
if (candidate.mtimeMs > newest.mtimeMs) {
|
|
1431
|
+
newest = candidate;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return newest;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function newestMtimeForPath(itemPath) {
|
|
1438
|
+
if (!fs.existsSync(itemPath)) {
|
|
1439
|
+
return { path: itemPath, mtimeMs: 0 };
|
|
1440
|
+
}
|
|
1441
|
+
const stat = fs.statSync(itemPath);
|
|
1442
|
+
if (!stat.isDirectory()) {
|
|
1443
|
+
return { path: itemPath, mtimeMs: stat.mtimeMs };
|
|
1444
|
+
}
|
|
1445
|
+
let newest = { path: itemPath, mtimeMs: stat.mtimeMs };
|
|
1446
|
+
for (const entry of fs.readdirSync(itemPath, { withFileTypes: true })) {
|
|
1447
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
const candidate = newestMtimeForPath(path.join(itemPath, entry.name));
|
|
1451
|
+
if (candidate.mtimeMs > newest.mtimeMs) {
|
|
1452
|
+
newest = candidate;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return newest;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function oldestMtime(paths) {
|
|
1459
|
+
let oldest = { path: null, mtimeMs: Number.POSITIVE_INFINITY };
|
|
1460
|
+
for (const itemPath of paths) {
|
|
1461
|
+
const mtimeMs = fs.statSync(itemPath).mtimeMs;
|
|
1462
|
+
if (mtimeMs < oldest.mtimeMs) {
|
|
1463
|
+
oldest = { path: itemPath, mtimeMs };
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return oldest;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
async function runInteractiveStep(label, command, args, settings = {}) {
|
|
1470
|
+
console.log(`${label}: ${formatCommand(command, args)}`);
|
|
1471
|
+
const useShell = Boolean(settings.shell);
|
|
1472
|
+
const child = spawn(useShell ? formatShellCommand(command, args) : command, useShell ? [] : args, {
|
|
1473
|
+
cwd: settings.cwd || RUNTIME_ROOT,
|
|
1474
|
+
env: process.env,
|
|
1475
|
+
shell: useShell,
|
|
1476
|
+
stdio: "inherit",
|
|
1477
|
+
windowsHide: false,
|
|
1478
|
+
});
|
|
1479
|
+
const exit = await new Promise((resolve, reject) => {
|
|
1480
|
+
child.once("error", reject);
|
|
1481
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
1482
|
+
});
|
|
1483
|
+
if (exit.signal) {
|
|
1484
|
+
throw new Error(`${label} stopped with signal ${exit.signal}`);
|
|
1485
|
+
}
|
|
1486
|
+
if (exit.code !== 0) {
|
|
1487
|
+
throw new Error(`${label} failed with exit code ${exit.code ?? "unknown"}`);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1165
1491
|
async function resolveWebRuntimeEntry() {
|
|
1166
1492
|
const distEntry = path.join(RUNTIME_ROOT, "dist", "web-dashboard.js");
|
|
1167
1493
|
if (fs.existsSync(distEntry)) {
|
|
@@ -1307,6 +1633,10 @@ function isWindowsShellScript(filePath) {
|
|
|
1307
1633
|
return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(filePath);
|
|
1308
1634
|
}
|
|
1309
1635
|
|
|
1636
|
+
function isLoopbackName(host) {
|
|
1637
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1310
1640
|
function validateStateBackend() {
|
|
1311
1641
|
const backend = process.env.NORDRELAY_STATE_BACKEND || "json";
|
|
1312
1642
|
if (backend === "json") return { ok: true, detail: "NORDRELAY_STATE_BACKEND=json" };
|
|
@@ -1350,19 +1680,54 @@ function sleep(ms) {
|
|
|
1350
1680
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1351
1681
|
}
|
|
1352
1682
|
|
|
1683
|
+
function printHelp() {
|
|
1684
|
+
console.log(`${APP_NAME} ${VERSION}`);
|
|
1685
|
+
console.log("");
|
|
1686
|
+
console.log("Usage: nordrelay <command> [options]");
|
|
1687
|
+
console.log("");
|
|
1688
|
+
console.log("Commands:");
|
|
1689
|
+
console.log(" init Create local config and first admin user");
|
|
1690
|
+
console.log(" user Manage users, groups, and channel links");
|
|
1691
|
+
console.log(" peer Manage secure NordRelay peer federation");
|
|
1692
|
+
console.log(" doctor Validate the local setup");
|
|
1693
|
+
console.log(" web, dashboard Start the WebUI and connector");
|
|
1694
|
+
console.log(" start Start the connector");
|
|
1695
|
+
console.log(" stop Stop the connector and WebUI");
|
|
1696
|
+
console.log(" restart Restart the connector");
|
|
1697
|
+
console.log(" status Show connector and WebUI status");
|
|
1698
|
+
console.log(" update Update NordRelay");
|
|
1699
|
+
console.log(" foreground Run the connector in the foreground");
|
|
1700
|
+
console.log(" version Print the installed version");
|
|
1701
|
+
console.log("");
|
|
1702
|
+
console.log("Options:");
|
|
1703
|
+
console.log(" --home <path> Runtime home directory");
|
|
1704
|
+
console.log(" --host <host> WebUI bind host");
|
|
1705
|
+
console.log(" --port <port> WebUI port");
|
|
1706
|
+
console.log(" --build Build source runtime before start/web/restart");
|
|
1707
|
+
console.log(" --force Overwrite existing config during init");
|
|
1708
|
+
console.log(" --help, -h Show this help");
|
|
1709
|
+
console.log(" --version, -v Show the installed version");
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1353
1712
|
async function main() {
|
|
1354
1713
|
const options = parseArgs(process.argv.slice(2));
|
|
1714
|
+
if (options.command === "help") {
|
|
1715
|
+
printHelp();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1355
1718
|
if (options.command === "start") return commandStart(options);
|
|
1356
1719
|
if (options.command === "stop") return commandStop(options);
|
|
1357
1720
|
if (options.command === "status") return commandStatus(options);
|
|
1358
1721
|
if (options.command === "init") return commandInit(options);
|
|
1359
1722
|
if (options.command === "user") return commandUser(options);
|
|
1723
|
+
if (options.command === "peer") return commandPeer(options);
|
|
1360
1724
|
if (options.command === "doctor") return commandDoctor(options);
|
|
1361
1725
|
if (options.command === "update") return commandUpdate(options);
|
|
1362
1726
|
if (options.command === "web" || options.command === "dashboard") return commandWeb(options);
|
|
1363
1727
|
if (options.command === "restart") {
|
|
1364
1728
|
await mkdirp(options.home);
|
|
1365
1729
|
loadEnvFiles(options.home);
|
|
1730
|
+
await prepareRuntimeForLaunch(options);
|
|
1366
1731
|
const webWasRunning = await isWebDashboardRunning(options);
|
|
1367
1732
|
await commandStop(options);
|
|
1368
1733
|
await commandStart(options);
|
|
@@ -1378,7 +1743,8 @@ async function main() {
|
|
|
1378
1743
|
}
|
|
1379
1744
|
|
|
1380
1745
|
console.error(`Unknown command: ${options.command}`);
|
|
1381
|
-
console.error("Usage: nordrelay [init|user|doctor|web|start|stop|restart|status|update|foreground|version]");
|
|
1746
|
+
console.error("Usage: nordrelay [init|user|peer|doctor|web|start|stop|restart|status|update|foreground|version]");
|
|
1747
|
+
console.error("Run `nordrelay --help` for details.");
|
|
1382
1748
|
process.exitCode = 2;
|
|
1383
1749
|
}
|
|
1384
1750
|
|