@nordbyte/nordrelay 0.8.1 → 0.8.3
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 +9 -0
- package/README.md +84 -1205
- package/dist/{access-control.js → access/access-control.js} +1 -1
- package/dist/{audit-log.js → access/audit-log.js} +32 -15
- package/dist/{session-locks.js → access/session-locks.js} +1 -1
- package/dist/{user-management.js → access/user-management.js} +1 -1
- package/dist/{claude-code-cli.js → agents/claude-code/claude-code-cli.js} +2 -2
- package/dist/{claude-code-session.js → agents/claude-code/claude-code-session.js} +1 -1
- package/dist/{codex-cli.js → agents/codex/codex-cli.js} +14 -5
- package/dist/{codex-session.js → agents/codex/codex-session.js} +2 -4
- package/dist/{hermes-cli.js → agents/hermes/hermes-cli.js} +2 -2
- package/dist/{hermes-launch.js → agents/hermes/hermes-launch.js} +1 -1
- package/dist/{hermes-session.js → agents/hermes/hermes-session.js} +1 -1
- package/dist/{openclaw-cli.js → agents/openclaw/openclaw-cli.js} +2 -2
- package/dist/{openclaw-launch.js → agents/openclaw/openclaw-launch.js} +1 -1
- package/dist/{openclaw-session.js → agents/openclaw/openclaw-session.js} +1 -1
- package/dist/{pi-cli.js → agents/pi/pi-cli.js} +2 -2
- package/dist/{pi-launch.js → agents/pi/pi-launch.js} +1 -1
- package/dist/{pi-session.js → agents/pi/pi-session.js} +1 -1
- package/dist/{adapter-conformance.js → agents/shared/adapter-conformance.js} +2 -2
- package/dist/{agent-activity.js → agents/shared/agent-activity.js} +5 -5
- package/dist/agents/shared/agent-auth-commands.js +30 -0
- package/dist/{agent-factory.js → agents/shared/agent-factory.js} +5 -5
- package/dist/{agent-feature-matrix.js → agents/shared/agent-feature-matrix.js} +2 -2
- package/dist/{agent-updates.js → agents/shared/agent-updates.js} +7 -7
- package/dist/{discord-artifacts.js → channels/discord/discord-artifacts.js} +4 -4
- package/dist/{discord-bot.js → channels/discord/discord-bot.js} +176 -451
- package/dist/{discord-channel-runtime.js → channels/discord/discord-channel-runtime.js} +2 -2
- package/dist/{discord-command-surface.js → channels/discord/discord-command-surface.js} +3 -3
- package/dist/{bot-rendering.js → channels/shared/bot-rendering.js} +6 -6
- package/dist/{channel-actions.js → channels/shared/channel-actions.js} +4 -4
- package/dist/channels/shared/channel-bridge-controller.js +69 -0
- package/dist/channels/shared/channel-cli-artifacts.js +51 -0
- package/dist/{channel-command-service.js → channels/shared/channel-command-service.js} +51 -28
- package/dist/channels/shared/channel-external-mirror-controller.js +193 -0
- package/dist/channels/shared/channel-external-monitor.js +52 -0
- package/dist/{channel-mirror-registry.js → channels/shared/channel-mirror-registry.js} +14 -6
- package/dist/{channel-peer-prompt.js → channels/shared/channel-peer-prompt.js} +3 -3
- package/dist/channels/shared/channel-prompt-queue.js +37 -0
- package/dist/{channel-turn-service.js → channels/shared/channel-turn-service.js} +25 -11
- package/dist/{context-key.js → channels/shared/context-key.js} +1 -1
- package/dist/{session-format.js → channels/shared/session-format.js} +2 -2
- package/dist/{slack-artifacts.js → channels/slack/slack-artifacts.js} +4 -4
- package/dist/{slack-bot.js → channels/slack/slack-bot.js} +171 -309
- package/dist/{slack-channel-runtime.js → channels/slack/slack-channel-runtime.js} +2 -2
- package/dist/{slack-command-surface.js → channels/slack/slack-command-surface.js} +2 -2
- package/dist/{slack-diagnostics.js → channels/slack/slack-diagnostics.js} +2 -2
- package/dist/{bot-ui.js → channels/telegram/bot-ui.js} +1 -1
- package/dist/{bot.js → channels/telegram/bot.js} +195 -430
- package/dist/{telegram-access-commands.js → channels/telegram/telegram-access-commands.js} +3 -3
- package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js} +4 -4
- package/dist/{telegram-agent-commands.js → channels/telegram/telegram-agent-commands.js} +9 -9
- package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js} +4 -4
- package/dist/{telegram-channel-runtime.js → channels/telegram/telegram-channel-runtime.js} +2 -2
- package/dist/{telegram-command-menu.js → channels/telegram/telegram-command-menu.js} +1 -1
- package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js} +7 -7
- package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js} +4 -4
- package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js} +5 -5
- package/dist/{telegram-output.js → channels/telegram/telegram-output.js} +2 -2
- package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js} +3 -3
- package/dist/{telegram-queue-commands.js → channels/telegram/telegram-queue-commands.js} +6 -6
- package/dist/{telegram-support-command.js → channels/telegram/telegram-support-command.js} +4 -4
- package/dist/{telegram-update-commands.js → channels/telegram/telegram-update-commands.js} +5 -5
- package/dist/{config-metadata.js → core/config-metadata.js} +8 -0
- package/dist/{config.js → core/config.js} +11 -3
- package/dist/core/pagination.js +22 -0
- package/dist/index.js +27 -23
- package/dist/peers/peer-discovery-jobs.js +206 -0
- package/dist/peers/peer-discovery.js +223 -0
- package/dist/peers/peer-health-monitor.js +49 -0
- package/dist/{peer-identity.js → peers/peer-identity.js} +50 -1
- package/dist/{peer-runtime-service.js → peers/peer-runtime-service.js} +29 -7
- package/dist/{peer-server.js → peers/peer-server.js} +3 -2
- package/dist/{peer-store.js → peers/peer-store.js} +96 -9
- package/dist/{peer-types.js → peers/peer-types.js} +28 -0
- package/dist/peers/peer-web-proxy-contract.js +129 -0
- package/dist/{metrics.js → runtime/metrics.js} +5 -3
- package/dist/{relay-artifact-service.js → runtime/relay-artifact-service.js} +1 -1
- package/dist/runtime/relay-auth-service.js +63 -0
- package/dist/runtime/relay-dashboard-service.js +139 -0
- package/dist/{relay-external-activity-monitor.js → runtime/relay-external-activity-monitor.js} +155 -53
- package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +1 -0
- package/dist/runtime/relay-runtime-active-sessions.js +387 -0
- package/dist/runtime/relay-runtime-dashboard.js +204 -0
- package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +3 -0
- package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +311 -0
- package/dist/runtime/relay-runtime-sessions.js +631 -0
- package/dist/runtime/relay-runtime-trace.js +92 -0
- package/dist/runtime/relay-runtime-types.js +1 -0
- package/dist/runtime/relay-runtime-updates-jobs.js +366 -0
- package/dist/runtime/relay-runtime.js +461 -0
- package/dist/runtime/runtime-cache.js +117 -0
- package/dist/{prompt-store.js → state/prompt-store.js} +13 -1
- package/dist/{session-registry.js → state/session-registry.js} +3 -3
- package/dist/{operations.js → support/operations.js} +7 -7
- package/dist/{support-bundle.js → support/support-bundle.js} +1 -1
- package/dist/{web-api-contract.js → web/web-api-contract.js} +19 -3
- package/dist/web/web-api-types.js +1 -0
- package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +17 -14
- package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +6 -2
- package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +25 -2
- package/dist/{web-dashboard-http.js → web/web-dashboard-http.js} +41 -5
- package/dist/{web-dashboard-pages.js → web/web-dashboard-pages.js} +95 -30
- package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +121 -7
- package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +8 -1
- package/dist/web/web-dashboard-security.js +14 -0
- package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +29 -13
- package/dist/web/web-dashboard-ui.js +56 -0
- package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
- package/dist/web/web-performance.js +62 -0
- package/dist/web/web-rate-limit.js +19 -0
- package/dist/{web-state.js → web/web-state.js} +107 -9
- package/dist/webui-assets/dashboard.css +398 -49
- package/dist/webui-assets/dashboard.js +1239 -103
- package/dist/webui-assets/favicon.ico +0 -0
- package/dist/webui-assets/favicon.png +0 -0
- package/dist/webui-assets/logo.png +0 -0
- package/package.json +6 -3
- package/plugins/nordrelay/scripts/nordrelay.mjs +346 -12
- package/plugins/nordrelay/scripts/service-installer.mjs +183 -0
- package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
- package/scripts/postinstall.mjs +122 -0
- package/dist/relay-runtime.js +0 -1916
- package/dist/runtime-cache.js +0 -57
- package/dist/web-dashboard-ui.js +0 -20
- /package/dist/{user-management-crypto.js → access/user-management-crypto.js} +0 -0
- /package/dist/{user-management-normalize.js → access/user-management-normalize.js} +0 -0
- /package/dist/{user-management-types.js → access/user-management-types.js} +0 -0
- /package/dist/{claude-code-auth.js → agents/claude-code/claude-code-auth.js} +0 -0
- /package/dist/{claude-code-launch.js → agents/claude-code/claude-code-launch.js} +0 -0
- /package/dist/{claude-code-state.js → agents/claude-code/claude-code-state.js} +0 -0
- /package/dist/{codex-auth.js → agents/codex/codex-auth.js} +0 -0
- /package/dist/{codex-config.js → agents/codex/codex-config.js} +0 -0
- /package/dist/{codex-launch.js → agents/codex/codex-launch.js} +0 -0
- /package/dist/{codex-state.js → agents/codex/codex-state.js} +0 -0
- /package/dist/{hermes-api.js → agents/hermes/hermes-api.js} +0 -0
- /package/dist/{hermes-auth.js → agents/hermes/hermes-auth.js} +0 -0
- /package/dist/{hermes-state.js → agents/hermes/hermes-state.js} +0 -0
- /package/dist/{openclaw-auth.js → agents/openclaw/openclaw-auth.js} +0 -0
- /package/dist/{openclaw-gateway.js → agents/openclaw/openclaw-gateway.js} +0 -0
- /package/dist/{openclaw-state.js → agents/openclaw/openclaw-state.js} +0 -0
- /package/dist/{pi-auth.js → agents/pi/pi-auth.js} +0 -0
- /package/dist/{pi-rpc.js → agents/pi/pi-rpc.js} +0 -0
- /package/dist/{pi-state.js → agents/pi/pi-state.js} +0 -0
- /package/dist/{agent-adapter.js → agents/shared/agent-adapter.js} +0 -0
- /package/dist/{agent.js → agents/shared/agent.js} +0 -0
- /package/dist/{artifacts.js → artifacts/artifacts.js} +0 -0
- /package/dist/{attachments.js → artifacts/attachments.js} +0 -0
- /package/dist/{voice.js → artifacts/voice.js} +0 -0
- /package/dist/{discord-rate-limit.js → channels/discord/discord-rate-limit.js} +0 -0
- /package/dist/{channel-adapter.js → channels/shared/channel-adapter.js} +0 -0
- /package/dist/{relay-runtime-types.js → channels/shared/channel-bridge-state.js} +0 -0
- /package/dist/{channel-command-catalog.js → channels/shared/channel-command-catalog.js} +0 -0
- /package/dist/{channel-command-core.js → channels/shared/channel-command-core.js} +0 -0
- /package/dist/{channel-prompt-engine.js → channels/shared/channel-prompt-engine.js} +0 -0
- /package/dist/{channel-runtime.js → channels/shared/channel-runtime.js} +0 -0
- /package/dist/{channel-turn-lifecycle.js → channels/shared/channel-turn-lifecycle.js} +0 -0
- /package/dist/{slack-rate-limit.js → channels/slack/slack-rate-limit.js} +0 -0
- /package/dist/{telegram-command-types.js → channels/telegram/telegram-command-types.js} +0 -0
- /package/dist/{telegram-rate-limit.js → channels/telegram/telegram-rate-limit.js} +0 -0
- /package/dist/{activity-events.js → core/activity-events.js} +0 -0
- /package/dist/{error-messages.js → core/error-messages.js} +0 -0
- /package/dist/{format.js → core/format.js} +0 -0
- /package/dist/{logger.js → core/logger.js} +0 -0
- /package/dist/{redaction.js → core/redaction.js} +0 -0
- /package/dist/{settings-service.js → core/settings-service.js} +0 -0
- /package/dist/{settings-wizard-test.js → core/settings-wizard-test.js} +0 -0
- /package/dist/{workspace-policy.js → core/workspace-policy.js} +0 -0
- /package/dist/{peer-auth.js → peers/peer-auth.js} +0 -0
- /package/dist/{peer-client.js → peers/peer-client.js} +0 -0
- /package/dist/{peer-context.js → peers/peer-context.js} +0 -0
- /package/dist/{peer-readiness.js → peers/peer-readiness.js} +0 -0
- /package/dist/{web-api-types.js → runtime/relay-runtime-delegate.js} +0 -0
- /package/dist/{remote-prompt.js → runtime/remote-prompt.js} +0 -0
- /package/dist/{bot-preferences.js → state/bot-preferences.js} +0 -0
- /package/dist/{job-store.js → state/job-store.js} +0 -0
- /package/dist/{persistence.js → state/persistence.js} +0 -0
- /package/dist/{state-backend.js → state/state-backend.js} +0 -0
- /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { consumeRateLimit, resetRateLimit } from "
|
|
2
|
-
import { friendlyErrorText } from "
|
|
3
|
-
import { escapeHTML } from "
|
|
1
|
+
import { consumeRateLimit, resetRateLimit } from "../shared/bot-rendering.js";
|
|
2
|
+
import { friendlyErrorText } from "../../core/error-messages.js";
|
|
3
|
+
import { escapeHTML } from "../../core/format.js";
|
|
4
4
|
import { safeReply } from "./telegram-output.js";
|
|
5
5
|
export function registerTelegramAccessCommands(deps) {
|
|
6
6
|
const { bot, userStore, contextUsers, linkAttempts, audit, getUserRole } = deps;
|
package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { permissionForCallbackData, permissionForCommand } from "
|
|
2
|
-
import { extractCommandName } from "
|
|
3
|
-
import { escapeHTML } from "
|
|
1
|
+
import { permissionForCallbackData, permissionForCommand } from "../../access/access-control.js";
|
|
2
|
+
import { extractCommandName } from "../shared/bot-rendering.js";
|
|
3
|
+
import { escapeHTML } from "../../core/format.js";
|
|
4
4
|
import { safeReply } from "./telegram-output.js";
|
|
5
|
-
import { UserStore } from "
|
|
5
|
+
import { UserStore } from "../../access/user-management.js";
|
|
6
6
|
export function createTelegramAccessMiddleware(options) {
|
|
7
7
|
const { userStore, contextUsers, audit } = options;
|
|
8
8
|
return async (ctx, next) => {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
|
-
import { agentLabel, } from "
|
|
3
|
-
import { enabledAgents } from "
|
|
4
|
-
import { capabilitiesOf, idOf, labelOf, } from "
|
|
5
|
-
import { checkAuthStatus } from "
|
|
6
|
-
import { contextKeyFromCtx } from "
|
|
7
|
-
import { friendlyErrorText } from "
|
|
8
|
-
import { escapeHTML } from "
|
|
9
|
-
import { redactText } from "
|
|
10
|
-
import { renderSessionInfoHTML, renderSessionInfoPlain, } from "
|
|
2
|
+
import { agentLabel, } from "../../agents/shared/agent.js";
|
|
3
|
+
import { enabledAgents } from "../../agents/shared/agent-factory.js";
|
|
4
|
+
import { capabilitiesOf, idOf, labelOf, } from "../shared/bot-rendering.js";
|
|
5
|
+
import { checkAuthStatus } from "../../agents/codex/codex-auth.js";
|
|
6
|
+
import { contextKeyFromCtx } from "../shared/context-key.js";
|
|
7
|
+
import { friendlyErrorText } from "../../core/error-messages.js";
|
|
8
|
+
import { escapeHTML } from "../../core/format.js";
|
|
9
|
+
import { redactText } from "../../core/redaction.js";
|
|
10
|
+
import { renderSessionInfoHTML, renderSessionInfoPlain, } from "../shared/session-format.js";
|
|
11
11
|
import { safeEditMessage, safeReply, } from "./telegram-output.js";
|
|
12
12
|
export function registerTelegramAgentCommands(options) {
|
|
13
13
|
options.bot.command("agent", async (ctx) => {
|
package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
|
-
import { getArtifactTurnReport, listRecentArtifactReports, removeArtifactTurn, } from "
|
|
3
|
-
import { buildArtifactActionsKeyboard, filterArtifactReports, } from "
|
|
4
|
-
import { renderArtifactReportsAction } from "
|
|
5
|
-
import { escapeHTML } from "
|
|
2
|
+
import { getArtifactTurnReport, listRecentArtifactReports, removeArtifactTurn, } from "../../artifacts/artifacts.js";
|
|
3
|
+
import { buildArtifactActionsKeyboard, filterArtifactReports, } from "../shared/bot-rendering.js";
|
|
4
|
+
import { renderArtifactReportsAction } from "../shared/channel-actions.js";
|
|
5
|
+
import { escapeHTML } from "../../core/format.js";
|
|
6
6
|
import { NOOP_PAGE_CALLBACK_DATA } from "./telegram-channel-runtime.js";
|
|
7
7
|
import { safeEditMessage, safeReply, } from "./telegram-output.js";
|
|
8
8
|
export function registerTelegramArtifactCommands(options) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Bot, InlineKeyboard, InputFile } from "grammy";
|
|
2
|
-
import { TelegramChannelAdapter, } from "
|
|
3
|
-
import { redactText } from "
|
|
2
|
+
import { TelegramChannelAdapter, } from "../shared/channel-adapter.js";
|
|
3
|
+
import { redactText } from "../../core/redaction.js";
|
|
4
4
|
import { telegramRateLimiter } from "./telegram-rate-limit.js";
|
|
5
5
|
import { chatBucket, safeEditMessage, sendChatActionSafe, sendTextMessage, } from "./telegram-output.js";
|
|
6
6
|
const KEYBOARD_PAGE_SIZE = 6;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { telegramCommandCatalog } from "
|
|
1
|
+
import { telegramCommandCatalog } from "../shared/channel-command-catalog.js";
|
|
2
2
|
export const TELEGRAM_COMMANDS = telegramCommandCatalog();
|
|
3
3
|
export async function registerCommands(bot) {
|
|
4
4
|
await bot.api.setMyCommands([...TELEGRAM_COMMANDS]);
|
package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { getAgentDiagnostics } from "
|
|
2
|
-
import { formatQuietHours } from "
|
|
3
|
-
import { cliPathOptions } from "
|
|
4
|
-
import { checkAuthStatus } from "
|
|
5
|
-
import { contextKeyFromCtx } from "
|
|
6
|
-
import { getConnectorHealth, } from "
|
|
7
|
-
import { renderAgentDiagnostics, renderDiagnosticsHTML, renderDiagnosticsPlain, renderHealthHTML, renderHealthPlain, } from "
|
|
1
|
+
import { getAgentDiagnostics } from "../../agents/shared/agent-activity.js";
|
|
2
|
+
import { formatQuietHours } from "../../state/bot-preferences.js";
|
|
3
|
+
import { cliPathOptions } from "../shared/channel-command-service.js";
|
|
4
|
+
import { checkAuthStatus } from "../../agents/codex/codex-auth.js";
|
|
5
|
+
import { contextKeyFromCtx } from "../shared/context-key.js";
|
|
6
|
+
import { getConnectorHealth, } from "../../support/operations.js";
|
|
7
|
+
import { renderAgentDiagnostics, renderDiagnosticsHTML, renderDiagnosticsPlain, renderHealthHTML, renderHealthPlain, } from "../shared/bot-rendering.js";
|
|
8
8
|
import { getTelegramRateLimitMetrics } from "./telegram-rate-limit.js";
|
|
9
9
|
import { safeReply } from "./telegram-output.js";
|
|
10
10
|
export function registerTelegramDiagnosticsCommands(options) {
|
package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { renderWelcomeFirstTime, renderWelcomeReturning, renderHelpMessage, } from "./bot-ui.js";
|
|
2
|
-
import { authHelpText, capabilitiesOf, labelOf, } from "
|
|
3
|
-
import { escapeHTML } from "
|
|
4
|
-
import { spawnConnectorRestart } from "
|
|
5
|
-
import { renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "
|
|
2
|
+
import { authHelpText, capabilitiesOf, labelOf, } from "../shared/bot-rendering.js";
|
|
3
|
+
import { escapeHTML } from "../../core/format.js";
|
|
4
|
+
import { spawnConnectorRestart } from "../../support/operations.js";
|
|
5
|
+
import { renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "../shared/session-format.js";
|
|
6
6
|
import { safeReply } from "./telegram-output.js";
|
|
7
7
|
export function registerTelegramGeneralCommands(options) {
|
|
8
8
|
options.bot.command("start", async (ctx) => {
|
package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js}
RENAMED
|
@@ -3,11 +3,11 @@ import { unlink, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { InputFile } from "grammy";
|
|
6
|
-
import { getAgentActivityLog } from "
|
|
7
|
-
import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, formatLockOwner, labelOf, parseActivityOptions, renderActivityTimeline, renderAuditEvents, renderProgressHTML, renderProgressPlain, renderSessionLocks, } from "
|
|
8
|
-
import { escapeHTML } from "
|
|
9
|
-
import { renderSessionInfoHTML, renderSessionInfoPlain } from "
|
|
10
|
-
import { canWriteWithLock } from "
|
|
6
|
+
import { getAgentActivityLog } from "../../agents/shared/agent-activity.js";
|
|
7
|
+
import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, formatLockOwner, labelOf, parseActivityOptions, renderActivityTimeline, renderAuditEvents, renderProgressHTML, renderProgressPlain, renderSessionLocks, } from "../shared/bot-rendering.js";
|
|
8
|
+
import { escapeHTML } from "../../core/format.js";
|
|
9
|
+
import { renderSessionInfoHTML, renderSessionInfoPlain } from "../shared/session-format.js";
|
|
10
|
+
import { canWriteWithLock } from "../../access/session-locks.js";
|
|
11
11
|
import { chatBucket, safeReply } from "./telegram-output.js";
|
|
12
12
|
import { telegramRateLimiter } from "./telegram-rate-limit.js";
|
|
13
13
|
export function registerTelegramOperationalCommands(options) {
|
|
@@ -3,8 +3,8 @@ import { writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { Bot, InlineKeyboard } from "grammy";
|
|
6
|
-
import { formatTelegramHTML } from "
|
|
7
|
-
import { redactText } from "
|
|
6
|
+
import { formatTelegramHTML } from "../../core/format.js";
|
|
7
|
+
import { redactText } from "../../core/redaction.js";
|
|
8
8
|
import { telegramRateLimiter } from "./telegram-rate-limit.js";
|
|
9
9
|
const TELEGRAM_MESSAGE_LIMIT = 4000;
|
|
10
10
|
const FORMATTED_CHUNK_TARGET = 3000;
|
package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { capabilitiesOf, labelOf, } from "
|
|
2
|
-
import { escapeHTML } from "
|
|
3
|
-
import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "
|
|
1
|
+
import { capabilitiesOf, labelOf, } from "../shared/bot-rendering.js";
|
|
2
|
+
import { escapeHTML } from "../../core/format.js";
|
|
3
|
+
import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "../../core/workspace-policy.js";
|
|
4
4
|
import { safeReply } from "./telegram-output.js";
|
|
5
5
|
export function registerTelegramPreferenceCommands(options) {
|
|
6
6
|
options.bot.command("mirror", async (ctx) => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
|
-
import { renderQueueListAction, renderQueuedPromptDetailAction, } from "
|
|
3
|
-
import { contextKeyFromCtx } from "
|
|
4
|
-
import { friendlyErrorText } from "
|
|
5
|
-
import { escapeHTML } from "
|
|
6
|
-
import { PromptStore, toPromptEnvelope } from "
|
|
7
|
-
import { formatLocalDateTime } from "
|
|
2
|
+
import { renderQueueListAction, renderQueuedPromptDetailAction, } from "../shared/channel-actions.js";
|
|
3
|
+
import { contextKeyFromCtx } from "../shared/context-key.js";
|
|
4
|
+
import { friendlyErrorText } from "../../core/error-messages.js";
|
|
5
|
+
import { escapeHTML } from "../../core/format.js";
|
|
6
|
+
import { PromptStore, toPromptEnvelope } from "../../state/prompt-store.js";
|
|
7
|
+
import { formatLocalDateTime } from "../shared/bot-rendering.js";
|
|
8
8
|
import { safeEditMessage, safeReply, } from "./telegram-output.js";
|
|
9
9
|
export function queueCancelCallbackData(action, contextKey, queueId) {
|
|
10
10
|
return `queue_${action}:${contextKey}:${queueId}`;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { InputFile } from "grammy";
|
|
2
|
-
import { contextKeyFromCtx } from "
|
|
3
|
-
import { getConnectorHealth, getVersionChecks } from "
|
|
4
|
-
import { formatLocalDateTime } from "
|
|
5
|
-
import { createSupportBundle } from "
|
|
2
|
+
import { contextKeyFromCtx } from "../shared/context-key.js";
|
|
3
|
+
import { getConnectorHealth, getVersionChecks } from "../../support/operations.js";
|
|
4
|
+
import { formatLocalDateTime } from "../shared/bot-rendering.js";
|
|
5
|
+
import { createSupportBundle } from "../../support/support-bundle.js";
|
|
6
6
|
import { chatBucket } from "./telegram-output.js";
|
|
7
7
|
import { telegramRateLimiter } from "./telegram-rate-limit.js";
|
|
8
8
|
export function registerTelegramSupportCommands(options) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { listAgentAdapterDescriptors } from "
|
|
2
|
-
import { agentLabel } from "
|
|
3
|
-
import { parseAgentUpdateId, renderAgentUpdateJobAction, renderAgentUpdateJobsAction, renderAgentUpdateLogAction, renderAgentUpdatePickerAction, renderSelfUpdateStartedAction, } from "
|
|
4
|
-
import { escapeHTML } from "
|
|
5
|
-
import { spawnSelfUpdate } from "
|
|
1
|
+
import { listAgentAdapterDescriptors } from "../../agents/shared/agent-adapter.js";
|
|
2
|
+
import { agentLabel } from "../../agents/shared/agent.js";
|
|
3
|
+
import { parseAgentUpdateId, renderAgentUpdateJobAction, renderAgentUpdateJobsAction, renderAgentUpdateLogAction, renderAgentUpdatePickerAction, renderSelfUpdateStartedAction, } from "../shared/channel-actions.js";
|
|
4
|
+
import { escapeHTML } from "../../core/format.js";
|
|
5
|
+
import { spawnSelfUpdate } from "../../support/operations.js";
|
|
6
6
|
import { safeReply } from "./telegram-output.js";
|
|
7
7
|
export function registerTelegramUpdateCommands(deps) {
|
|
8
8
|
const { bot, agentUpdates, replyChannelAction, startTelegramAgentUpdate } = deps;
|
|
@@ -140,6 +140,8 @@ export const SETTING_DEFINITIONS = [
|
|
|
140
140
|
setting("TELEGRAM_EDIT_MIN_INTERVAL_MS", "Telegram edit interval", "Operations", "number", "Minimum edit interval.", true),
|
|
141
141
|
setting("NORDRELAY_CLI_MIRROR_MODE", "Default CLI mirror mode", "Operations", "string", "Default mirror mode for chat adapters: off, status, final, or full.", false, ["off", "status", "final", "full"]),
|
|
142
142
|
setting("NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS", "Default mirror update interval", "Operations", "number", "Default minimum mirrored edit interval.", true),
|
|
143
|
+
setting("NORDRELAY_WEB_CLI_MIRROR_MODE", "WebUI mirror override", "Operations", "string", "Optional WebUI override for CLI mirror mode. Uses the NordRelay default when unset.", false, ["off", "status", "final", "full"]),
|
|
144
|
+
setting("NORDRELAY_WEB_CLI_MIRROR_MIN_UPDATE_MS", "WebUI mirror update override", "Operations", "number", "Optional WebUI override for mirrored status interval.", true),
|
|
143
145
|
setting("NORDRELAY_NOTIFY_MODE", "Default notify mode", "Operations", "string", "Default completion notifications: off, minimal, or all.", false, ["off", "minimal", "all"]),
|
|
144
146
|
setting("NORDRELAY_QUIET_HOURS", "Default quiet hours", "Operations", "string", "Default quiet hours. Use HH-HH, off, or leave blank.", false),
|
|
145
147
|
setting("NORDRELAY_AUTO_SEND_ARTIFACTS", "Default auto-send artifacts", "Operations", "boolean", "Default automatic artifact summaries/uploads for chat adapters.", false),
|
|
@@ -172,6 +174,8 @@ export const SETTING_DEFINITIONS = [
|
|
|
172
174
|
setting("NORDRELAY_PEER_PUBLIC_URL", "Peer public URL", "Peers", "string", "Optional public URL other instances should use for this node.", true),
|
|
173
175
|
setting("NORDRELAY_PEER_TLS_ENABLED", "Peer TLS enabled", "Peers", "boolean", "Serve the peer API over HTTPS with an automatically generated local certificate.", true),
|
|
174
176
|
setting("NORDRELAY_PEER_REQUIRE_TLS", "Require peer TLS", "Peers", "boolean", "Reject plaintext peer serving on non-loopback hosts.", true),
|
|
177
|
+
setting("NORDRELAY_PEER_HEALTH_CHECK_MS", "Peer health interval", "Peers", "number", "Background reachability check interval for configured peers. Use 0 to disable.", true),
|
|
178
|
+
setting("NORDRELAY_PEER_DISCOVERY_TIMEOUT_MS", "Peer discovery timeout", "Peers", "number", "Per-host LAN discovery timeout in milliseconds.", true),
|
|
175
179
|
setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
|
|
176
180
|
setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
|
|
177
181
|
setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
|
|
@@ -280,6 +284,8 @@ const EXAMPLE_VALUES = {
|
|
|
280
284
|
"TELEGRAM_WEBHOOK_SECRET": "",
|
|
281
285
|
"NORDRELAY_CLI_MIRROR_MODE": "status",
|
|
282
286
|
"NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS": "4000",
|
|
287
|
+
"NORDRELAY_WEB_CLI_MIRROR_MODE": "",
|
|
288
|
+
"NORDRELAY_WEB_CLI_MIRROR_MIN_UPDATE_MS": "",
|
|
283
289
|
"NORDRELAY_NOTIFY_MODE": "minimal",
|
|
284
290
|
"NORDRELAY_QUIET_HOURS": "",
|
|
285
291
|
"NORDRELAY_AUTO_SEND_ARTIFACTS": "false",
|
|
@@ -309,6 +315,8 @@ const EXAMPLE_VALUES = {
|
|
|
309
315
|
"NORDRELAY_PEER_PUBLIC_URL": "",
|
|
310
316
|
"NORDRELAY_PEER_TLS_ENABLED": "true",
|
|
311
317
|
"NORDRELAY_PEER_REQUIRE_TLS": "true",
|
|
318
|
+
"NORDRELAY_PEER_HEALTH_CHECK_MS": "60000",
|
|
319
|
+
"NORDRELAY_PEER_DISCOVERY_TIMEOUT_MS": "650",
|
|
312
320
|
"NORDRELAY_DASHBOARD_HOST": "127.0.0.1",
|
|
313
321
|
"NORDRELAY_DASHBOARD_PORT": "31878",
|
|
314
322
|
"NORDRELAY_ENV_FILE": "",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createBuiltinLaunchProfiles, createDefaultLaunchProfile, findLaunchProfile, isCodexApprovalPolicy, isCodexSandboxMode, parseLaunchProfilesJson, } from "
|
|
4
|
-
import { CLAUDE_CODE_EFFORT_LEVELS, HERMES_REASONING_EFFORTS, OPENCLAW_THINKING_LEVELS, isAgentId, PI_THINKING_LEVELS, } from "
|
|
5
|
-
import { parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "
|
|
3
|
+
import { createBuiltinLaunchProfiles, createDefaultLaunchProfile, findLaunchProfile, isCodexApprovalPolicy, isCodexSandboxMode, parseLaunchProfilesJson, } from "../agents/codex/codex-launch.js";
|
|
4
|
+
import { CLAUDE_CODE_EFFORT_LEVELS, HERMES_REASONING_EFFORTS, OPENCLAW_THINKING_LEVELS, isAgentId, PI_THINKING_LEVELS, } from "../agents/shared/agent.js";
|
|
5
|
+
import { parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "../state/bot-preferences.js";
|
|
6
6
|
export function loadConfig() {
|
|
7
7
|
loadEnvFile(path.resolve(process.cwd(), ".env"));
|
|
8
8
|
const adapterWarnings = [];
|
|
@@ -12,6 +12,8 @@ export function loadConfig() {
|
|
|
12
12
|
const telegramEditMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_EDIT_MIN_INTERVAL_MS), 1_200, "TELEGRAM_EDIT_MIN_INTERVAL_MS");
|
|
13
13
|
const mirrorMode = parseMirrorMode(optionalString(process.env.NORDRELAY_CLI_MIRROR_MODE), "status");
|
|
14
14
|
const mirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS), 4_000, "NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS");
|
|
15
|
+
const webMirrorMode = parseMirrorMode(optionalString(process.env.NORDRELAY_WEB_CLI_MIRROR_MODE), mirrorMode);
|
|
16
|
+
const webMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_WEB_CLI_MIRROR_MIN_UPDATE_MS), mirrorMinUpdateMs, "NORDRELAY_WEB_CLI_MIRROR_MIN_UPDATE_MS");
|
|
15
17
|
const notifyMode = parseNotifyMode(optionalString(process.env.NORDRELAY_NOTIFY_MODE), "minimal");
|
|
16
18
|
const quietHours = parseQuietHoursOverride(process.env.NORDRELAY_QUIET_HOURS, null);
|
|
17
19
|
const autoSendArtifacts = parseBooleanEnv(optionalString(process.env.NORDRELAY_AUTO_SEND_ARTIFACTS), false);
|
|
@@ -131,6 +133,8 @@ export function loadConfig() {
|
|
|
131
133
|
const peerPublicUrl = optionalString(process.env.NORDRELAY_PEER_PUBLIC_URL);
|
|
132
134
|
const peerTlsEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_TLS_ENABLED), true);
|
|
133
135
|
const peerRequireTls = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_REQUIRE_TLS), true);
|
|
136
|
+
const peerHealthCheckMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_PEER_HEALTH_CHECK_MS), 60_000, "NORDRELAY_PEER_HEALTH_CHECK_MS");
|
|
137
|
+
const peerDiscoveryTimeoutMs = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_PEER_DISCOVERY_TIMEOUT_MS), 650, "NORDRELAY_PEER_DISCOVERY_TIMEOUT_MS");
|
|
134
138
|
let telegramEnabled = requestedTelegramEnabled;
|
|
135
139
|
if (telegramEnabled && telegramTransport === "webhook" && !telegramWebhookUrl) {
|
|
136
140
|
telegramEnabled = false;
|
|
@@ -170,6 +174,8 @@ export function loadConfig() {
|
|
|
170
174
|
telegramEditMinIntervalMs,
|
|
171
175
|
mirrorMode,
|
|
172
176
|
mirrorMinUpdateMs,
|
|
177
|
+
webMirrorMode,
|
|
178
|
+
webMirrorMinUpdateMs,
|
|
173
179
|
notifyMode,
|
|
174
180
|
quietHours,
|
|
175
181
|
autoSendArtifacts,
|
|
@@ -288,6 +294,8 @@ export function loadConfig() {
|
|
|
288
294
|
peerPublicUrl,
|
|
289
295
|
peerTlsEnabled,
|
|
290
296
|
peerRequireTls,
|
|
297
|
+
peerHealthCheckMs,
|
|
298
|
+
peerDiscoveryTimeoutMs,
|
|
291
299
|
};
|
|
292
300
|
}
|
|
293
301
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function normalizeCursorLimit(value, fallback = 100, max = 500) {
|
|
2
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
3
|
+
const selected = Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
4
|
+
return Math.max(1, Math.min(max, selected));
|
|
5
|
+
}
|
|
6
|
+
export function cursorPage(items, cursor, limit, cursorOf) {
|
|
7
|
+
const normalizedLimit = normalizeCursorLimit(limit, limit);
|
|
8
|
+
const startIndex = cursor ? Math.max(0, items.findIndex((item) => cursorOf(item) === cursor) + 1) : 0;
|
|
9
|
+
const window = items.slice(startIndex, startIndex + normalizedLimit + 1);
|
|
10
|
+
const pageItems = window.slice(0, normalizedLimit);
|
|
11
|
+
const hasNext = window.length > normalizedLimit;
|
|
12
|
+
const last = pageItems.at(-1);
|
|
13
|
+
return {
|
|
14
|
+
items: pageItems,
|
|
15
|
+
pagination: {
|
|
16
|
+
limit: normalizedLimit,
|
|
17
|
+
nextCursor: hasNext && last ? cursorOf(last) ?? null : null,
|
|
18
|
+
hasNext,
|
|
19
|
+
total: items.length,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,35 +2,37 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { webhookCallback } from "grammy";
|
|
5
|
-
import { agentLabel } from "./agent.js";
|
|
6
|
-
import { createBot, registerCommands } from "./bot.js";
|
|
7
|
-
import { createDiscordBridge } from "./discord-bot.js";
|
|
8
|
-
import { createSlackBridge } from "./slack-bot.js";
|
|
9
|
-
import { checkAuthStatus } from "./codex-auth.js";
|
|
10
|
-
import { describeCodexCli, resolveCodexCli } from "./codex-cli.js";
|
|
11
|
-
import { checkClaudeCodeAuthStatus } from "./claude-code-auth.js";
|
|
12
|
-
import { describeClaudeCodeCli, resolveClaudeCodeCli } from "./claude-code-cli.js";
|
|
13
|
-
import { findLaunchProfile, formatLaunchProfileBehavior } from "./codex-launch.js";
|
|
14
|
-
import { enabledAgents } from "./agent-factory.js";
|
|
15
|
-
import { loadConfig } from "./config.js";
|
|
16
|
-
import { checkHermesAuthStatus } from "./hermes-auth.js";
|
|
17
|
-
import { describeHermesCli, resolveHermesCli } from "./hermes-cli.js";
|
|
18
|
-
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
19
|
-
import { describeOpenClawCli, resolveOpenClawCli } from "./openclaw-cli.js";
|
|
20
|
-
import { installConsoleLogger } from "./logger.js";
|
|
21
|
-
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
22
|
-
import { describePiCli, resolvePiCli } from "./pi-cli.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
5
|
+
import { agentLabel } from "./agents/shared/agent.js";
|
|
6
|
+
import { createBot, registerCommands } from "./channels/telegram/bot.js";
|
|
7
|
+
import { createDiscordBridge } from "./channels/discord/discord-bot.js";
|
|
8
|
+
import { createSlackBridge } from "./channels/slack/slack-bot.js";
|
|
9
|
+
import { checkAuthStatus } from "./agents/codex/codex-auth.js";
|
|
10
|
+
import { describeCodexCli, resolveCodexCli } from "./agents/codex/codex-cli.js";
|
|
11
|
+
import { checkClaudeCodeAuthStatus } from "./agents/claude-code/claude-code-auth.js";
|
|
12
|
+
import { describeClaudeCodeCli, resolveClaudeCodeCli } from "./agents/claude-code/claude-code-cli.js";
|
|
13
|
+
import { findLaunchProfile, formatLaunchProfileBehavior } from "./agents/codex/codex-launch.js";
|
|
14
|
+
import { enabledAgents } from "./agents/shared/agent-factory.js";
|
|
15
|
+
import { loadConfig } from "./core/config.js";
|
|
16
|
+
import { checkHermesAuthStatus } from "./agents/hermes/hermes-auth.js";
|
|
17
|
+
import { describeHermesCli, resolveHermesCli } from "./agents/hermes/hermes-cli.js";
|
|
18
|
+
import { checkOpenClawAuthStatus } from "./agents/openclaw/openclaw-auth.js";
|
|
19
|
+
import { describeOpenClawCli, resolveOpenClawCli } from "./agents/openclaw/openclaw-cli.js";
|
|
20
|
+
import { installConsoleLogger } from "./core/logger.js";
|
|
21
|
+
import { checkPiAuthStatus } from "./agents/pi/pi-auth.js";
|
|
22
|
+
import { describePiCli, resolvePiCli } from "./agents/pi/pi-cli.js";
|
|
23
|
+
import { startPeerHealthMonitor } from "./peers/peer-health-monitor.js";
|
|
24
|
+
import { startPeerServer } from "./peers/peer-server.js";
|
|
25
|
+
import { RelayRuntime } from "./runtime/relay-runtime.js";
|
|
26
|
+
import { configureRedaction } from "./core/redaction.js";
|
|
27
|
+
import { SessionRegistry } from "./state/session-registry.js";
|
|
28
|
+
import { UserStore } from "./access/user-management.js";
|
|
28
29
|
let registry;
|
|
29
30
|
let bot;
|
|
30
31
|
let discordBridge;
|
|
31
32
|
let slackBridge;
|
|
32
33
|
let webhookServer;
|
|
33
34
|
let peerServer;
|
|
35
|
+
let peerHealthMonitor;
|
|
34
36
|
let peerRuntime;
|
|
35
37
|
let runtimeConfig;
|
|
36
38
|
try {
|
|
@@ -51,6 +53,7 @@ try {
|
|
|
51
53
|
peerRuntime = new RelayRuntime(config);
|
|
52
54
|
peerServer = await startPeerServer({ config, runtime: peerRuntime });
|
|
53
55
|
}
|
|
56
|
+
peerHealthMonitor = startPeerHealthMonitor({ config });
|
|
54
57
|
console.log("NordRelay running");
|
|
55
58
|
const userStore = new UserStore();
|
|
56
59
|
if (userStore.hasAdminUser()) {
|
|
@@ -177,6 +180,7 @@ const shutdown = (signal) => {
|
|
|
177
180
|
void peerServer?.close().catch((error) => {
|
|
178
181
|
console.warn("Failed to stop peer server:", error instanceof Error ? error.message : String(error));
|
|
179
182
|
});
|
|
183
|
+
peerHealthMonitor?.close();
|
|
180
184
|
setTimeout(() => {
|
|
181
185
|
registry?.disposeAll();
|
|
182
186
|
peerRuntime?.dispose();
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readJsonFileWithBackup, writeJsonFileAtomic } from "../state/persistence.js";
|
|
5
|
+
import { countDiscoveryTargets, discoverLanPeers } from "./peer-discovery.js";
|
|
6
|
+
const MAX_JOBS = 25;
|
|
7
|
+
const MAX_LOG_LINES = 300;
|
|
8
|
+
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
9
|
+
export class PeerDiscoveryJobManager {
|
|
10
|
+
config;
|
|
11
|
+
jobs = new Map();
|
|
12
|
+
filePath;
|
|
13
|
+
constructor(config, home = process.env.NORDRELAY_HOME || DEFAULT_HOME) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.filePath = path.join(home, "peer-discovery-jobs.json");
|
|
16
|
+
this.load();
|
|
17
|
+
}
|
|
18
|
+
list() {
|
|
19
|
+
return [...this.jobs.values()].map((entry) => cloneJob(entry.snapshot))
|
|
20
|
+
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt));
|
|
21
|
+
}
|
|
22
|
+
get(id) {
|
|
23
|
+
const entry = this.jobs.get(id);
|
|
24
|
+
return entry ? cloneJob(entry.snapshot) : null;
|
|
25
|
+
}
|
|
26
|
+
log(id) {
|
|
27
|
+
return this.jobs.get(id)?.snapshot.log.join("\n") ?? "";
|
|
28
|
+
}
|
|
29
|
+
async start(input = {}) {
|
|
30
|
+
this.prune();
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const options = normalizeInput(this.config, input);
|
|
33
|
+
const id = randomUUID().replace(/-/g, "").slice(0, 12);
|
|
34
|
+
const snapshot = {
|
|
35
|
+
id,
|
|
36
|
+
status: "queued",
|
|
37
|
+
createdAt: new Date().toISOString(),
|
|
38
|
+
scanned: 0,
|
|
39
|
+
total: await countDiscoveryTargets(this.config, options).catch(() => 0),
|
|
40
|
+
candidates: [],
|
|
41
|
+
warnings: [],
|
|
42
|
+
log: [],
|
|
43
|
+
options,
|
|
44
|
+
};
|
|
45
|
+
const entry = { snapshot, controller };
|
|
46
|
+
this.jobs.set(id, entry);
|
|
47
|
+
this.append(entry, `Queued peer discovery job ${id}.`);
|
|
48
|
+
void this.run(entry).catch((error) => {
|
|
49
|
+
entry.snapshot.status = controller.signal.aborted ? "cancelled" : "failed";
|
|
50
|
+
entry.snapshot.error = error instanceof Error ? error.message : String(error);
|
|
51
|
+
entry.snapshot.completedAt = new Date().toISOString();
|
|
52
|
+
this.append(entry, entry.snapshot.error);
|
|
53
|
+
});
|
|
54
|
+
return cloneJob(snapshot);
|
|
55
|
+
}
|
|
56
|
+
cancel(id) {
|
|
57
|
+
const entry = this.jobs.get(id);
|
|
58
|
+
if (!entry)
|
|
59
|
+
return null;
|
|
60
|
+
if (entry.snapshot.status === "queued" || entry.snapshot.status === "running") {
|
|
61
|
+
entry.controller.abort();
|
|
62
|
+
entry.snapshot.status = "cancelled";
|
|
63
|
+
entry.snapshot.completedAt = new Date().toISOString();
|
|
64
|
+
this.append(entry, "Cancellation requested.");
|
|
65
|
+
}
|
|
66
|
+
return cloneJob(entry.snapshot);
|
|
67
|
+
}
|
|
68
|
+
async run(entry) {
|
|
69
|
+
entry.snapshot.status = "running";
|
|
70
|
+
entry.snapshot.startedAt = new Date().toISOString();
|
|
71
|
+
this.append(entry, `Scanning ${entry.snapshot.total} peer endpoint candidate(s).`);
|
|
72
|
+
const result = await discoverLanPeers(this.config, {
|
|
73
|
+
...entry.snapshot.options,
|
|
74
|
+
signal: entry.controller.signal,
|
|
75
|
+
onProgress: (progress) => {
|
|
76
|
+
entry.snapshot.scanned = progress.scanned;
|
|
77
|
+
if (progress.candidate) {
|
|
78
|
+
entry.snapshot.candidates = mergeCandidates(entry.snapshot.candidates, progress.candidate);
|
|
79
|
+
this.append(entry, `Found ${progress.candidate.name || progress.candidate.host} at ${progress.candidate.url}.`);
|
|
80
|
+
}
|
|
81
|
+
else if (progress.scanned % 25 === 0 || progress.scanned === entry.snapshot.total) {
|
|
82
|
+
this.append(entry, `Scanned ${progress.scanned}/${progress.total}.`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
entry.snapshot.scanned = result.scanned;
|
|
87
|
+
entry.snapshot.candidates = result.candidates;
|
|
88
|
+
entry.snapshot.warnings = result.warnings;
|
|
89
|
+
entry.snapshot.status = entry.controller.signal.aborted ? "cancelled" : "completed";
|
|
90
|
+
entry.snapshot.completedAt = new Date().toISOString();
|
|
91
|
+
this.append(entry, `${entry.snapshot.status === "completed" ? "Completed" : "Cancelled"} with ${result.candidates.length} candidate(s).`);
|
|
92
|
+
for (const warning of result.warnings) {
|
|
93
|
+
this.append(entry, `Warning: ${warning}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
append(entry, line) {
|
|
97
|
+
entry.snapshot.log.push(`[${new Date().toLocaleString()}] ${line}`);
|
|
98
|
+
if (entry.snapshot.log.length > MAX_LOG_LINES) {
|
|
99
|
+
entry.snapshot.log.splice(0, entry.snapshot.log.length - MAX_LOG_LINES);
|
|
100
|
+
}
|
|
101
|
+
this.save();
|
|
102
|
+
}
|
|
103
|
+
prune() {
|
|
104
|
+
const completed = this.list()
|
|
105
|
+
.filter((job) => job.status !== "running" && job.status !== "queued")
|
|
106
|
+
.slice(MAX_JOBS);
|
|
107
|
+
for (const job of completed) {
|
|
108
|
+
this.jobs.delete(job.id);
|
|
109
|
+
}
|
|
110
|
+
this.save();
|
|
111
|
+
}
|
|
112
|
+
load() {
|
|
113
|
+
const result = readJsonFileWithBackup(this.filePath);
|
|
114
|
+
const jobs = Array.isArray(result.value?.jobs) ? result.value.jobs : [];
|
|
115
|
+
let changed = false;
|
|
116
|
+
for (const job of jobs) {
|
|
117
|
+
const snapshot = normalizePersistedJob(job);
|
|
118
|
+
if (!snapshot)
|
|
119
|
+
continue;
|
|
120
|
+
if (snapshot.status === "queued" || snapshot.status === "running") {
|
|
121
|
+
snapshot.status = "failed";
|
|
122
|
+
snapshot.completedAt = new Date().toISOString();
|
|
123
|
+
snapshot.error = "Discovery job was interrupted by a NordRelay restart.";
|
|
124
|
+
snapshot.log = [
|
|
125
|
+
...snapshot.log,
|
|
126
|
+
`[${new Date().toLocaleString()}] Discovery job was interrupted by a NordRelay restart.`,
|
|
127
|
+
].slice(-MAX_LOG_LINES);
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
this.jobs.set(snapshot.id, { snapshot, controller: new AbortController() });
|
|
131
|
+
}
|
|
132
|
+
this.prune();
|
|
133
|
+
if (changed) {
|
|
134
|
+
this.save();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
save() {
|
|
138
|
+
const jobs = this.list().slice(0, MAX_JOBS);
|
|
139
|
+
writeJsonFileAtomic(this.filePath, { version: 1, jobs });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function normalizePersistedJob(value) {
|
|
143
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
144
|
+
return null;
|
|
145
|
+
const record = value;
|
|
146
|
+
if (typeof record.id !== "string" || typeof record.createdAt !== "string")
|
|
147
|
+
return null;
|
|
148
|
+
const status = typeof record.status === "string" && ["queued", "running", "completed", "failed", "cancelled"].includes(record.status)
|
|
149
|
+
? record.status
|
|
150
|
+
: "failed";
|
|
151
|
+
const optionsRecord = record.options && typeof record.options === "object" && !Array.isArray(record.options)
|
|
152
|
+
? record.options
|
|
153
|
+
: {};
|
|
154
|
+
return {
|
|
155
|
+
id: record.id,
|
|
156
|
+
status,
|
|
157
|
+
createdAt: record.createdAt,
|
|
158
|
+
startedAt: typeof record.startedAt === "string" ? record.startedAt : undefined,
|
|
159
|
+
completedAt: typeof record.completedAt === "string" ? record.completedAt : undefined,
|
|
160
|
+
scanned: integerField(record.scanned),
|
|
161
|
+
total: integerField(record.total),
|
|
162
|
+
candidates: Array.isArray(record.candidates) ? record.candidates : [],
|
|
163
|
+
warnings: Array.isArray(record.warnings) ? record.warnings.filter((item) => typeof item === "string") : [],
|
|
164
|
+
log: Array.isArray(record.log) ? record.log.filter((item) => typeof item === "string").slice(-MAX_LOG_LINES) : [],
|
|
165
|
+
error: typeof record.error === "string" ? record.error : undefined,
|
|
166
|
+
options: {
|
|
167
|
+
targets: Array.isArray(optionsRecord.targets) ? optionsRecord.targets.filter((item) => typeof item === "string") : [],
|
|
168
|
+
timeoutMs: integerField(optionsRecord.timeoutMs),
|
|
169
|
+
concurrency: integerField(optionsRecord.concurrency),
|
|
170
|
+
maxHosts: integerField(optionsRecord.maxHosts),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function integerField(value) {
|
|
175
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
176
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
177
|
+
}
|
|
178
|
+
function normalizeInput(config, input) {
|
|
179
|
+
return {
|
|
180
|
+
targets: (input.targets ?? []).map((target) => target.trim()).filter(Boolean),
|
|
181
|
+
timeoutMs: clampInteger(input.timeoutMs, config.peerDiscoveryTimeoutMs, 100, 30_000),
|
|
182
|
+
concurrency: clampInteger(input.concurrency, 32, 1, 128),
|
|
183
|
+
maxHosts: clampInteger(input.maxHosts, 512, 1, 65_536),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function clampInteger(value, fallback, min, max) {
|
|
187
|
+
const parsed = Number(value);
|
|
188
|
+
return Number.isInteger(parsed) ? Math.max(min, Math.min(max, parsed)) : fallback;
|
|
189
|
+
}
|
|
190
|
+
function mergeCandidates(existing, candidate) {
|
|
191
|
+
const byNode = new Map(existing.map((item) => [item.nodeId, item]));
|
|
192
|
+
byNode.set(candidate.nodeId, candidate);
|
|
193
|
+
return [...byNode.values()].sort((left, right) => (left.name || left.host).localeCompare(right.name || right.host));
|
|
194
|
+
}
|
|
195
|
+
function cloneJob(job) {
|
|
196
|
+
return {
|
|
197
|
+
...job,
|
|
198
|
+
candidates: job.candidates.map((candidate) => ({ ...candidate })),
|
|
199
|
+
warnings: [...job.warnings],
|
|
200
|
+
log: [...job.log],
|
|
201
|
+
options: {
|
|
202
|
+
...job.options,
|
|
203
|
+
targets: [...job.options.targets],
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|