@nordbyte/nordrelay 0.5.2 → 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 +80 -11
- package/README.md +154 -22
- package/dist/access-control.js +7 -1
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +535 -11
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +40 -7
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +369 -0
- 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/channel-turn-service.js +237 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +93 -13
- package/dist/config.js +103 -8
- package/dist/context-key.js +87 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2073 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +57 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +36 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +87 -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-external-activity-monitor.js +47 -6
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +897 -394
- package/dist/remote-prompt.js +98 -0
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/support-bundle.js +1 -0
- package/dist/telegram-access-commands.js +15 -2
- package/dist/telegram-access-middleware.js +16 -3
- package/dist/telegram-agent-commands.js +25 -0
- package/dist/telegram-artifact-commands.js +46 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +16 -6
- package/dist/telegram-operational-commands.js +14 -6
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/telegram-queue-commands.js +74 -4
- package/dist/telegram-support-command.js +7 -0
- package/dist/telegram-update-commands.js +27 -0
- package/dist/user-management.js +208 -0
- package/dist/web-api-contract.js +17 -0
- package/dist/web-dashboard-access-routes.js +74 -1
- package/dist/web-dashboard-artifact-routes.js +3 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-pages.js +109 -13
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +2 -0
- package/dist/web-dashboard.js +160 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +779 -55
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
|
@@ -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,9 +68,14 @@ 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);
|
|
75
|
+
else if (arg === "--disable-telegram") options.disableTelegram = true;
|
|
76
|
+
else if (arg === "--enable-discord") options.enableDiscord = true;
|
|
77
|
+
else if (arg === "--discord-token") options.discordBotToken = requireValue(copy, ++i, arg);
|
|
78
|
+
else if (arg === "--discord-client-id") options.discordClientId = requireValue(copy, ++i, arg);
|
|
69
79
|
else if (arg === "--admin-email") options.adminEmail = requireValue(copy, ++i, arg);
|
|
70
80
|
else if (arg === "--admin-name") options.adminName = requireValue(copy, ++i, arg);
|
|
71
81
|
else if (arg === "--admin-password") options.adminPassword = requireValue(copy, ++i, arg);
|
|
@@ -81,6 +91,9 @@ function parseArgs(argv) {
|
|
|
81
91
|
options.pidFile = path.join(options.home, "nordrelay.pid");
|
|
82
92
|
options.stateFile = path.join(options.home, "state.json");
|
|
83
93
|
options.logFile = path.join(options.home, "nordrelay.log");
|
|
94
|
+
options.webPidFile = path.join(options.home, "nordrelay-web.pid");
|
|
95
|
+
options.webStateFile = path.join(options.home, "web-state.json");
|
|
96
|
+
options.webLogFile = path.join(options.home, "nordrelay-web.log");
|
|
84
97
|
return options;
|
|
85
98
|
}
|
|
86
99
|
|
|
@@ -175,6 +188,26 @@ async function readPid(pidFile) {
|
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
190
|
|
|
191
|
+
async function readWebState(options) {
|
|
192
|
+
return await readJson(options.webStateFile, {});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function readWebPid(options) {
|
|
196
|
+
return await readPid(options.webPidFile);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function isWebDashboardRunning(options) {
|
|
200
|
+
return isProcessRunning(await readWebPid(options));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function writeWebState(options, patch) {
|
|
204
|
+
await writeJsonAtomic(options.webStateFile, {
|
|
205
|
+
updatedAt: nowIso(),
|
|
206
|
+
logFile: options.webLogFile,
|
|
207
|
+
...patch,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
178
211
|
function resolveDashboardEndpoint(options, settings = {}) {
|
|
179
212
|
const host = options.host || process.env.NORDRELAY_DASHBOARD_HOST || "127.0.0.1";
|
|
180
213
|
const rawPort = options.port ?? Number.parseInt(process.env.NORDRELAY_DASHBOARD_PORT || "31878", 10);
|
|
@@ -199,6 +232,7 @@ function formatDashboardUrl(endpoint) {
|
|
|
199
232
|
async function commandStart(options, settings = {}) {
|
|
200
233
|
await mkdirp(options.home);
|
|
201
234
|
loadEnvFiles(options.home);
|
|
235
|
+
await prepareRuntimeForLaunch(options);
|
|
202
236
|
const dashboard = resolveDashboardEndpoint(options);
|
|
203
237
|
|
|
204
238
|
const currentPid = await readPid(options.pidFile);
|
|
@@ -216,7 +250,7 @@ async function commandStart(options, settings = {}) {
|
|
|
216
250
|
});
|
|
217
251
|
|
|
218
252
|
const logFd = fs.openSync(options.logFile, "a");
|
|
219
|
-
const child = spawn(process.execPath, [SCRIPT_PATH, "foreground", ...options.rawFlags], {
|
|
253
|
+
const child = spawn(process.execPath, [SCRIPT_PATH, "foreground", ...runtimeForwardFlags(options.rawFlags)], {
|
|
220
254
|
cwd: RUNTIME_ROOT,
|
|
221
255
|
detached: true,
|
|
222
256
|
env: process.env,
|
|
@@ -233,7 +267,9 @@ async function commandStart(options, settings = {}) {
|
|
|
233
267
|
console.log(`Workspace: ${state.workspace || "-"}`);
|
|
234
268
|
console.log(`Mode: ${state.sessionMode || "per Telegram context"}`);
|
|
235
269
|
if (!settings.skipWebHint) {
|
|
236
|
-
|
|
270
|
+
const webPid = await readWebPid(options);
|
|
271
|
+
const webHint = isProcessRunning(webPid) ? `(running with PID ${webPid})` : "(run `nordrelay web` to start it)";
|
|
272
|
+
console.log(`WebUI: ${formatDashboardUrl(dashboard)} ${webHint}`);
|
|
237
273
|
}
|
|
238
274
|
console.log(`Log: ${options.logFile}`);
|
|
239
275
|
return;
|
|
@@ -251,7 +287,9 @@ async function commandStart(options, settings = {}) {
|
|
|
251
287
|
|
|
252
288
|
console.log(`Started ${APP_NAME} ${VERSION} with PID ${child.pid}`);
|
|
253
289
|
if (!settings.skipWebHint) {
|
|
254
|
-
|
|
290
|
+
const webPid = await readWebPid(options);
|
|
291
|
+
const webHint = isProcessRunning(webPid) ? `(running with PID ${webPid})` : "(run `nordrelay web` to start it)";
|
|
292
|
+
console.log(`WebUI: ${formatDashboardUrl(dashboard)} ${webHint}`);
|
|
255
293
|
}
|
|
256
294
|
console.log(`Startup is still in progress. Log: ${options.logFile}`);
|
|
257
295
|
}
|
|
@@ -286,7 +324,51 @@ async function waitForState(stateFile, pid, timeoutMs) {
|
|
|
286
324
|
return await readJson(stateFile);
|
|
287
325
|
}
|
|
288
326
|
|
|
289
|
-
async function
|
|
327
|
+
async function stopWebDashboard(options, settings = {}) {
|
|
328
|
+
const pid = await readWebPid(options);
|
|
329
|
+
if (!isProcessRunning(pid)) {
|
|
330
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
331
|
+
const state = await readWebState(options);
|
|
332
|
+
if (state.status === "running" || state.status === "starting") {
|
|
333
|
+
await writeWebState(options, { status: "stopped", pid: null });
|
|
334
|
+
}
|
|
335
|
+
if (!settings.quiet) {
|
|
336
|
+
console.log("WebUI is not running.");
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
process.kill(pid, "SIGTERM");
|
|
342
|
+
for (let i = 0; i < 40; i += 1) {
|
|
343
|
+
if (!isProcessRunning(pid)) break;
|
|
344
|
+
await sleep(250);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (isProcessRunning(pid)) {
|
|
348
|
+
process.kill(pid, "SIGKILL");
|
|
349
|
+
for (let i = 0; i < 20; i += 1) {
|
|
350
|
+
if (!isProcessRunning(pid)) break;
|
|
351
|
+
await sleep(250);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (isProcessRunning(pid)) {
|
|
356
|
+
console.log(`WebUI PID ${pid} did not exit after SIGTERM/SIGKILL.`);
|
|
357
|
+
process.exitCode = 1;
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
361
|
+
await writeWebState(options, { status: "stopped", pid: null });
|
|
362
|
+
if (!settings.quiet) {
|
|
363
|
+
console.log(`Stopped WebUI PID ${pid}.`);
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function commandStop(options, settings = {}) {
|
|
369
|
+
if (!settings.keepWeb) {
|
|
370
|
+
await stopWebDashboard(options);
|
|
371
|
+
}
|
|
290
372
|
const pid = await readPid(options.pidFile);
|
|
291
373
|
if (!isProcessRunning(pid)) {
|
|
292
374
|
console.log("Connector is not running.");
|
|
@@ -313,10 +395,19 @@ async function commandStatus(options) {
|
|
|
313
395
|
loadEnvFiles(options.home);
|
|
314
396
|
const dashboard = resolveDashboardEndpoint(options);
|
|
315
397
|
const pid = await readPid(options.pidFile);
|
|
398
|
+
const webPid = await readWebPid(options);
|
|
316
399
|
const state = await readJson(options.stateFile, {});
|
|
400
|
+
const webState = await readWebState(options);
|
|
317
401
|
const running = isProcessRunning(pid);
|
|
402
|
+
const webRunning = isProcessRunning(webPid);
|
|
403
|
+
const webStatus = webRunning ? "running" : webState.status === "running" || webState.status === "starting" ? "stale" : webState.status || "stopped";
|
|
404
|
+
if (!webRunning && (webState.status === "running" || webState.status === "starting")) {
|
|
405
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
406
|
+
await writeWebState(options, { status: "stopped", pid: null });
|
|
407
|
+
}
|
|
318
408
|
console.log(`Status: ${state.status || (running ? "running" : "stopped")}`);
|
|
319
409
|
console.log(`PID: ${pid || "-"} (${running ? "running" : "not running"})`);
|
|
410
|
+
console.log(`WebUI PID: ${webPid || "-"} (${webRunning ? "running" : "not running"})`);
|
|
320
411
|
console.log(`Workspace: ${state.workspace || "-"}`);
|
|
321
412
|
console.log(`Mode: ${state.sessionMode || "per Telegram context"}`);
|
|
322
413
|
console.log(`Auth: ${state.authenticated === undefined ? "-" : state.authenticated ? "yes" : "no"}`);
|
|
@@ -326,8 +417,11 @@ async function commandStatus(options) {
|
|
|
326
417
|
console.log(`OpenClaw CLI: ${state.openClawCli || "-"}`);
|
|
327
418
|
console.log(`Claude Code CLI: ${state.claudeCodeCli || "-"}`);
|
|
328
419
|
console.log(`OpenClaw Gateway: ${state.openClawGateway || process.env.OPENCLAW_GATEWAY_URL || "-"}`);
|
|
329
|
-
console.log(`
|
|
420
|
+
console.log(`Peers: ${state.peerEnabled ? state.peerUrl || "enabled" : "disabled"}`);
|
|
421
|
+
if (state.peerTlsFingerprint) console.log(`Peer TLS fingerprint: ${state.peerTlsFingerprint}`);
|
|
422
|
+
console.log(`WebUI: ${formatDashboardUrl(dashboard)} (${webStatus})`);
|
|
330
423
|
console.log(`Log: ${options.logFile}`);
|
|
424
|
+
console.log(`WebUI log: ${options.webLogFile}`);
|
|
331
425
|
if (state.error) console.log(`Error: ${state.error}`);
|
|
332
426
|
}
|
|
333
427
|
|
|
@@ -579,9 +673,17 @@ async function commandInit(options) {
|
|
|
579
673
|
return;
|
|
580
674
|
}
|
|
581
675
|
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
await ask(null, "Telegram bot token", "")
|
|
676
|
+
const enableTelegram = options.disableTelegram ? "false" : await askChoice(null, "Enable Telegram", "true");
|
|
677
|
+
const telegramBotToken = enableTelegram === "true"
|
|
678
|
+
? options.telegramBotToken || process.env.TELEGRAM_BOT_TOKEN || await ask(null, "Telegram bot token", "")
|
|
679
|
+
: "";
|
|
680
|
+
const enableDiscord = options.enableDiscord ? "true" : await askChoice(null, "Enable Discord", "false");
|
|
681
|
+
const discordBotToken = enableDiscord === "true"
|
|
682
|
+
? options.discordBotToken || process.env.DISCORD_BOT_TOKEN || await ask(null, "Discord bot token", "")
|
|
683
|
+
: "";
|
|
684
|
+
const discordClientId = enableDiscord === "true"
|
|
685
|
+
? options.discordClientId || process.env.DISCORD_CLIENT_ID || await ask(null, "Discord client ID", "")
|
|
686
|
+
: "";
|
|
585
687
|
const adminEmail = options.adminEmail || await ask(null, "Admin email", "");
|
|
586
688
|
const adminName = options.adminName || await ask(null, "Admin name", "Admin");
|
|
587
689
|
const adminPassword = options.adminPassword || await askSecret(null, "Admin password", "");
|
|
@@ -593,7 +695,9 @@ async function commandInit(options) {
|
|
|
593
695
|
const enableClaudeCode = options.enableClaudeCode ? "true" : await askChoice(null, "Enable Claude Code", "false");
|
|
594
696
|
const stateBackend = options.stateBackend || await askChoice(null, "State backend (json/sqlite)", "json");
|
|
595
697
|
|
|
596
|
-
if (!telegramBotToken) throw new Error("Telegram bot token is required.");
|
|
698
|
+
if (enableTelegram === "true" && !telegramBotToken) throw new Error("Telegram bot token is required when Telegram is enabled.");
|
|
699
|
+
if (enableDiscord === "true" && !discordBotToken) throw new Error("Discord bot token is required when Discord is enabled.");
|
|
700
|
+
if (enableTelegram !== "true" && enableDiscord !== "true") throw new Error("At least one chat adapter must be enabled.");
|
|
597
701
|
if (!adminEmail) throw new Error("Admin email is required.");
|
|
598
702
|
if (!adminPassword) throw new Error("Admin password is required.");
|
|
599
703
|
if (enableCodex !== "true" && enablePi !== "true" && enableHermes !== "true" && enableOpenClaw !== "true" && enableClaudeCode !== "true") throw new Error("At least one agent must be enabled.");
|
|
@@ -610,7 +714,14 @@ async function commandInit(options) {
|
|
|
610
714
|
const lines = [
|
|
611
715
|
"# NordRelay local runtime config.",
|
|
612
716
|
"# Keep this file private; it contains bot credentials.",
|
|
717
|
+
`TELEGRAM_ENABLED=${enableTelegram}`,
|
|
613
718
|
`TELEGRAM_BOT_TOKEN=${telegramBotToken}`,
|
|
719
|
+
`DISCORD_ENABLED=${enableDiscord}`,
|
|
720
|
+
`DISCORD_BOT_TOKEN=${discordBotToken}`,
|
|
721
|
+
`DISCORD_CLIENT_ID=${discordClientId}`,
|
|
722
|
+
"DISCORD_COMMAND_MODE=both",
|
|
723
|
+
"DISCORD_MESSAGE_CONTENT_ENABLED=true",
|
|
724
|
+
"DISCORD_AUTO_REGISTER_COMMANDS=true",
|
|
614
725
|
`NORDRELAY_CODEX_ENABLED=${enableCodex}`,
|
|
615
726
|
`NORDRELAY_PI_ENABLED=${enablePi}`,
|
|
616
727
|
`NORDRELAY_HERMES_ENABLED=${enableHermes}`,
|
|
@@ -625,6 +736,11 @@ async function commandInit(options) {
|
|
|
625
736
|
"OPENCLAW_DEFAULT_PROFILE=default",
|
|
626
737
|
"CLAUDE_CODE_DEFAULT_PROFILE=default",
|
|
627
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",
|
|
628
744
|
`NORDRELAY_STATE_BACKEND=${stateBackend === "sqlite" ? "sqlite" : "json"}`,
|
|
629
745
|
"TELEGRAM_TRANSPORT=polling",
|
|
630
746
|
"TELEGRAM_AUTO_SEND_ARTIFACTS=false",
|
|
@@ -653,6 +769,142 @@ async function createUserStore(home) {
|
|
|
653
769
|
return new mod.UserStore(home);
|
|
654
770
|
}
|
|
655
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
|
+
|
|
656
908
|
function parseUserFlags(argv) {
|
|
657
909
|
const copy = [...argv];
|
|
658
910
|
const subcommand = copy[0] && !copy[0].startsWith("-") ? copy.shift() : "list";
|
|
@@ -664,6 +916,7 @@ function parseUserFlags(argv) {
|
|
|
664
916
|
else if (arg === "--password") flags.password = requireValue(copy, ++i, arg);
|
|
665
917
|
else if (arg === "--group" || arg === "--groups") flags.groups = requireValue(copy, ++i, arg);
|
|
666
918
|
else if (arg === "--telegram-user-id") flags.telegramUserId = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
919
|
+
else if (arg === "--discord-user-id") flags.discordUserId = requireValue(copy, ++i, arg);
|
|
667
920
|
else if (arg === "--user-id") flags.userId = requireValue(copy, ++i, arg);
|
|
668
921
|
}
|
|
669
922
|
return flags;
|
|
@@ -695,8 +948,8 @@ async function commandUser(options) {
|
|
|
695
948
|
? ["admin"]
|
|
696
949
|
: (flags.groups ? flags.groups.split(",").map((item) => item.trim()).filter(Boolean) : ["user"]);
|
|
697
950
|
const created = flags.subcommand === "create-admin"
|
|
698
|
-
? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId })
|
|
699
|
-
: store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId });
|
|
951
|
+
? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId })
|
|
952
|
+
: store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId });
|
|
700
953
|
console.log(`Created user ${created.user.email} (${created.groups.map((group) => group.name).join(", ")}).`);
|
|
701
954
|
return;
|
|
702
955
|
}
|
|
@@ -721,7 +974,17 @@ async function commandUser(options) {
|
|
|
721
974
|
return;
|
|
722
975
|
}
|
|
723
976
|
|
|
724
|
-
if (flags.subcommand === "link-
|
|
977
|
+
if (flags.subcommand === "link-discord") {
|
|
978
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
979
|
+
const discordUserId = flags.discordUserId || await ask(null, "Discord user id", "");
|
|
980
|
+
const user = store.getUserByEmail(email);
|
|
981
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
982
|
+
store.linkDiscordUser(user.user.id, { discordUserId });
|
|
983
|
+
console.log(`Linked Discord user ${discordUserId} to ${user.user.email}.`);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (flags.subcommand === "link-code" || flags.subcommand === "telegram-link-code") {
|
|
725
988
|
const email = flags.email || await ask(null, "Email", "");
|
|
726
989
|
const user = store.getUserByEmail(email);
|
|
727
990
|
if (!user) throw new Error(`User not found: ${email}`);
|
|
@@ -731,7 +994,17 @@ async function commandUser(options) {
|
|
|
731
994
|
return;
|
|
732
995
|
}
|
|
733
996
|
|
|
734
|
-
|
|
997
|
+
if (flags.subcommand === "discord-link-code") {
|
|
998
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
999
|
+
const user = store.getUserByEmail(email);
|
|
1000
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
1001
|
+
const code = store.createDiscordLinkCode(user.user.id);
|
|
1002
|
+
console.log(`Discord link code for ${user.user.email}: ${code.code}`);
|
|
1003
|
+
console.log(`Expires: ${code.expiresAt}`);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
throw new Error("Usage: nordrelay user [list|create-admin|create|reset-password|link-telegram|link-discord|link-code|telegram-link-code|discord-link-code]");
|
|
735
1008
|
}
|
|
736
1009
|
|
|
737
1010
|
async function commandDoctor(options) {
|
|
@@ -741,11 +1014,39 @@ async function commandDoctor(options) {
|
|
|
741
1014
|
const userSnapshot = userStore?.snapshot();
|
|
742
1015
|
const checks = [];
|
|
743
1016
|
checks.push(check("Node.js >= 22", Number.parseInt(process.versions.node.split(".")[0], 10) >= 22, process.version));
|
|
744
|
-
|
|
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"));
|
|
745
1040
|
checks.push(check("User store", Boolean(userStore), userStore ? userStore.filePath : "missing runtime", userStore ? "pass" : "fail"));
|
|
746
1041
|
checks.push(check("Admin user", Boolean(userSnapshot?.adminConfigured), userSnapshot?.adminConfigured ? "configured" : "missing"));
|
|
747
1042
|
checks.push(check("WebUI login", true, "required for every dashboard request"));
|
|
748
1043
|
checks.push(check("Telegram access", true, "requires linked active users and enabled group chats"));
|
|
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"));
|
|
749
1050
|
checks.push(check("Codex enabled flag", process.env.NORDRELAY_CODEX_ENABLED !== "false", `NORDRELAY_CODEX_ENABLED=${process.env.NORDRELAY_CODEX_ENABLED ?? "true"}`));
|
|
750
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"));
|
|
751
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"));
|
|
@@ -850,8 +1151,22 @@ async function checkOpenClawGateway() {
|
|
|
850
1151
|
async function commandWeb(options) {
|
|
851
1152
|
await mkdirp(options.home);
|
|
852
1153
|
loadEnvFiles(options.home);
|
|
853
|
-
|
|
1154
|
+
await prepareRuntimeForLaunch(options);
|
|
854
1155
|
await ensureConnectorStartedForWeb(options);
|
|
1156
|
+
await startWebDashboard(options, { detached: false });
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async function startWebDashboard(options, settings = {}) {
|
|
1160
|
+
await mkdirp(options.home);
|
|
1161
|
+
loadEnvFiles(options.home);
|
|
1162
|
+
const { host, port } = resolveDashboardEndpoint(options, { strict: true });
|
|
1163
|
+
const currentPid = await readWebPid(options);
|
|
1164
|
+
if (isProcessRunning(currentPid)) {
|
|
1165
|
+
console.log(`NordRelay dashboard already running with PID ${currentPid}.`);
|
|
1166
|
+
console.log(`NordRelay dashboard: ${formatDashboardUrl({ host, port })}`);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
855
1170
|
const entry = await resolveWebRuntimeEntry();
|
|
856
1171
|
if (!entry) {
|
|
857
1172
|
throw new Error(`Missing dashboard runtime. Run \`npm install\` and \`npm run build\` in ${RUNTIME_ROOT}.`);
|
|
@@ -864,12 +1179,43 @@ async function commandWeb(options) {
|
|
|
864
1179
|
NORDRELAY_DASHBOARD_HOST: host,
|
|
865
1180
|
NORDRELAY_DASHBOARD_PORT: String(port),
|
|
866
1181
|
};
|
|
1182
|
+
await writeWebState(options, {
|
|
1183
|
+
status: "starting",
|
|
1184
|
+
pid: null,
|
|
1185
|
+
host,
|
|
1186
|
+
port,
|
|
1187
|
+
url: formatDashboardUrl({ host, port }),
|
|
1188
|
+
});
|
|
1189
|
+
const stdio = settings.detached
|
|
1190
|
+
? ["ignore", fs.openSync(options.webLogFile, "a"), fs.openSync(options.webLogFile, "a")]
|
|
1191
|
+
: "inherit";
|
|
867
1192
|
const child = spawn(entry.command, [...entry.args, "--host", host, "--port", String(port), "--home", options.home], {
|
|
868
1193
|
cwd: RUNTIME_ROOT,
|
|
869
1194
|
env,
|
|
870
|
-
|
|
1195
|
+
detached: Boolean(settings.detached),
|
|
1196
|
+
stdio,
|
|
1197
|
+
});
|
|
1198
|
+
await fsp.writeFile(options.webPidFile, `${child.pid}\n`);
|
|
1199
|
+
await writeWebState(options, {
|
|
1200
|
+
status: "running",
|
|
1201
|
+
pid: child.pid,
|
|
1202
|
+
host,
|
|
1203
|
+
port,
|
|
1204
|
+
url: formatDashboardUrl({ host, port }),
|
|
871
1205
|
});
|
|
872
1206
|
|
|
1207
|
+
if (settings.detached) {
|
|
1208
|
+
child.unref();
|
|
1209
|
+
if (Array.isArray(stdio)) {
|
|
1210
|
+
fs.closeSync(stdio[1]);
|
|
1211
|
+
fs.closeSync(stdio[2]);
|
|
1212
|
+
}
|
|
1213
|
+
console.log(`NordRelay dashboard started with PID ${child.pid}.`);
|
|
1214
|
+
console.log(`NordRelay dashboard: ${formatDashboardUrl({ host, port })}`);
|
|
1215
|
+
console.log(`WebUI log: ${options.webLogFile}`);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
873
1219
|
const forwardSignal = (signal) => {
|
|
874
1220
|
if (isProcessRunning(child.pid)) {
|
|
875
1221
|
child.kill(signal);
|
|
@@ -878,9 +1224,23 @@ async function commandWeb(options) {
|
|
|
878
1224
|
process.once("SIGINT", () => forwardSignal("SIGINT"));
|
|
879
1225
|
process.once("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
880
1226
|
|
|
881
|
-
const exit = await new Promise((resolve) => {
|
|
1227
|
+
const exit = await new Promise((resolve, reject) => {
|
|
1228
|
+
child.once("error", reject);
|
|
882
1229
|
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
883
1230
|
});
|
|
1231
|
+
const pid = await readWebPid(options);
|
|
1232
|
+
if (pid === child.pid) {
|
|
1233
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
1234
|
+
}
|
|
1235
|
+
await writeWebState(options, {
|
|
1236
|
+
status: exit.code === 0 ? "stopped" : "error",
|
|
1237
|
+
pid: null,
|
|
1238
|
+
host,
|
|
1239
|
+
port,
|
|
1240
|
+
url: formatDashboardUrl({ host, port }),
|
|
1241
|
+
exitCode: exit.code,
|
|
1242
|
+
signal: exit.signal,
|
|
1243
|
+
});
|
|
884
1244
|
if (exit.signal) {
|
|
885
1245
|
process.kill(process.pid, exit.signal);
|
|
886
1246
|
return;
|
|
@@ -891,6 +1251,7 @@ async function commandWeb(options) {
|
|
|
891
1251
|
async function commandForeground(options) {
|
|
892
1252
|
await mkdirp(options.home);
|
|
893
1253
|
loadEnvFiles(options.home);
|
|
1254
|
+
await prepareRuntimeForLaunch(options);
|
|
894
1255
|
process.chdir(RUNTIME_ROOT);
|
|
895
1256
|
|
|
896
1257
|
await writeJsonAtomic(options.stateFile, {
|
|
@@ -976,6 +1337,157 @@ async function resolveRuntimeEntry() {
|
|
|
976
1337
|
return null;
|
|
977
1338
|
}
|
|
978
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
|
+
|
|
979
1491
|
async function resolveWebRuntimeEntry() {
|
|
980
1492
|
const distEntry = path.join(RUNTIME_ROOT, "dist", "web-dashboard.js");
|
|
981
1493
|
if (fs.existsSync(distEntry)) {
|
|
@@ -1121,6 +1633,10 @@ function isWindowsShellScript(filePath) {
|
|
|
1121
1633
|
return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(filePath);
|
|
1122
1634
|
}
|
|
1123
1635
|
|
|
1636
|
+
function isLoopbackName(host) {
|
|
1637
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1124
1640
|
function validateStateBackend() {
|
|
1125
1641
|
const backend = process.env.NORDRELAY_STATE_BACKEND || "json";
|
|
1126
1642
|
if (backend === "json") return { ok: true, detail: "NORDRELAY_STATE_BACKEND=json" };
|
|
@@ -1164,19 +1680,61 @@ function sleep(ms) {
|
|
|
1164
1680
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1165
1681
|
}
|
|
1166
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
|
+
|
|
1167
1712
|
async function main() {
|
|
1168
1713
|
const options = parseArgs(process.argv.slice(2));
|
|
1714
|
+
if (options.command === "help") {
|
|
1715
|
+
printHelp();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1169
1718
|
if (options.command === "start") return commandStart(options);
|
|
1170
1719
|
if (options.command === "stop") return commandStop(options);
|
|
1171
1720
|
if (options.command === "status") return commandStatus(options);
|
|
1172
1721
|
if (options.command === "init") return commandInit(options);
|
|
1173
1722
|
if (options.command === "user") return commandUser(options);
|
|
1723
|
+
if (options.command === "peer") return commandPeer(options);
|
|
1174
1724
|
if (options.command === "doctor") return commandDoctor(options);
|
|
1175
1725
|
if (options.command === "update") return commandUpdate(options);
|
|
1176
1726
|
if (options.command === "web" || options.command === "dashboard") return commandWeb(options);
|
|
1177
1727
|
if (options.command === "restart") {
|
|
1728
|
+
await mkdirp(options.home);
|
|
1729
|
+
loadEnvFiles(options.home);
|
|
1730
|
+
await prepareRuntimeForLaunch(options);
|
|
1731
|
+
const webWasRunning = await isWebDashboardRunning(options);
|
|
1178
1732
|
await commandStop(options);
|
|
1179
|
-
|
|
1733
|
+
await commandStart(options);
|
|
1734
|
+
if (webWasRunning && process.exitCode !== 1) {
|
|
1735
|
+
await startWebDashboard(options, { detached: true });
|
|
1736
|
+
}
|
|
1737
|
+
return;
|
|
1180
1738
|
}
|
|
1181
1739
|
if (options.command === "foreground") return commandForeground(options);
|
|
1182
1740
|
if (options.command === "--version" || options.command === "version") {
|
|
@@ -1185,7 +1743,8 @@ async function main() {
|
|
|
1185
1743
|
}
|
|
1186
1744
|
|
|
1187
1745
|
console.error(`Unknown command: ${options.command}`);
|
|
1188
|
-
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.");
|
|
1189
1748
|
process.exitCode = 2;
|
|
1190
1749
|
}
|
|
1191
1750
|
|