@nordbyte/nordrelay 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/.env.example +9 -0
  2. package/README.md +81 -1206
  3. package/dist/{access-control.js → access/access-control.js} +1 -1
  4. package/dist/{audit-log.js → access/audit-log.js} +2 -2
  5. package/dist/{session-locks.js → access/session-locks.js} +1 -1
  6. package/dist/{user-management.js → access/user-management.js} +1 -1
  7. package/dist/{claude-code-cli.js → agents/claude-code/claude-code-cli.js} +2 -2
  8. package/dist/{claude-code-session.js → agents/claude-code/claude-code-session.js} +1 -1
  9. package/dist/{codex-cli.js → agents/codex/codex-cli.js} +14 -5
  10. package/dist/{codex-session.js → agents/codex/codex-session.js} +2 -4
  11. package/dist/{hermes-cli.js → agents/hermes/hermes-cli.js} +2 -2
  12. package/dist/{hermes-launch.js → agents/hermes/hermes-launch.js} +1 -1
  13. package/dist/{hermes-session.js → agents/hermes/hermes-session.js} +1 -1
  14. package/dist/{openclaw-cli.js → agents/openclaw/openclaw-cli.js} +2 -2
  15. package/dist/{openclaw-launch.js → agents/openclaw/openclaw-launch.js} +1 -1
  16. package/dist/{openclaw-session.js → agents/openclaw/openclaw-session.js} +1 -1
  17. package/dist/{pi-cli.js → agents/pi/pi-cli.js} +2 -2
  18. package/dist/{pi-launch.js → agents/pi/pi-launch.js} +1 -1
  19. package/dist/{pi-session.js → agents/pi/pi-session.js} +1 -1
  20. package/dist/{adapter-conformance.js → agents/shared/adapter-conformance.js} +2 -2
  21. package/dist/{agent-activity.js → agents/shared/agent-activity.js} +5 -5
  22. package/dist/agents/shared/agent-auth-commands.js +30 -0
  23. package/dist/{agent-factory.js → agents/shared/agent-factory.js} +5 -5
  24. package/dist/{agent-feature-matrix.js → agents/shared/agent-feature-matrix.js} +2 -2
  25. package/dist/{agent-updates.js → agents/shared/agent-updates.js} +7 -7
  26. package/dist/{discord-artifacts.js → channels/discord/discord-artifacts.js} +4 -4
  27. package/dist/{discord-bot.js → channels/discord/discord-bot.js} +164 -424
  28. package/dist/{discord-channel-runtime.js → channels/discord/discord-channel-runtime.js} +2 -2
  29. package/dist/{discord-command-surface.js → channels/discord/discord-command-surface.js} +3 -3
  30. package/dist/{bot-rendering.js → channels/shared/bot-rendering.js} +6 -6
  31. package/dist/{channel-actions.js → channels/shared/channel-actions.js} +4 -4
  32. package/dist/channels/shared/channel-bridge-controller.js +69 -0
  33. package/dist/channels/shared/channel-cli-artifacts.js +51 -0
  34. package/dist/{channel-command-service.js → channels/shared/channel-command-service.js} +51 -28
  35. package/dist/channels/shared/channel-external-mirror-controller.js +193 -0
  36. package/dist/channels/shared/channel-external-monitor.js +52 -0
  37. package/dist/{channel-mirror-registry.js → channels/shared/channel-mirror-registry.js} +14 -6
  38. package/dist/{channel-peer-prompt.js → channels/shared/channel-peer-prompt.js} +3 -3
  39. package/dist/{channel-turn-service.js → channels/shared/channel-turn-service.js} +2 -2
  40. package/dist/{context-key.js → channels/shared/context-key.js} +1 -1
  41. package/dist/{session-format.js → channels/shared/session-format.js} +2 -2
  42. package/dist/{slack-artifacts.js → channels/slack/slack-artifacts.js} +4 -4
  43. package/dist/{slack-bot.js → channels/slack/slack-bot.js} +159 -294
  44. package/dist/{slack-channel-runtime.js → channels/slack/slack-channel-runtime.js} +2 -2
  45. package/dist/{slack-command-surface.js → channels/slack/slack-command-surface.js} +2 -2
  46. package/dist/{slack-diagnostics.js → channels/slack/slack-diagnostics.js} +2 -2
  47. package/dist/{bot-ui.js → channels/telegram/bot-ui.js} +1 -1
  48. package/dist/{bot.js → channels/telegram/bot.js} +178 -427
  49. package/dist/{telegram-access-commands.js → channels/telegram/telegram-access-commands.js} +3 -3
  50. package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js} +4 -4
  51. package/dist/{telegram-agent-commands.js → channels/telegram/telegram-agent-commands.js} +9 -9
  52. package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js} +4 -4
  53. package/dist/{telegram-channel-runtime.js → channels/telegram/telegram-channel-runtime.js} +2 -2
  54. package/dist/{telegram-command-menu.js → channels/telegram/telegram-command-menu.js} +1 -1
  55. package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js} +7 -7
  56. package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js} +4 -4
  57. package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js} +5 -5
  58. package/dist/{telegram-output.js → channels/telegram/telegram-output.js} +2 -2
  59. package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js} +3 -3
  60. package/dist/{telegram-queue-commands.js → channels/telegram/telegram-queue-commands.js} +6 -6
  61. package/dist/{telegram-support-command.js → channels/telegram/telegram-support-command.js} +4 -4
  62. package/dist/{telegram-update-commands.js → channels/telegram/telegram-update-commands.js} +5 -5
  63. package/dist/{config-metadata.js → core/config-metadata.js} +8 -0
  64. package/dist/{config.js → core/config.js} +11 -3
  65. package/dist/index.js +27 -23
  66. package/dist/peers/peer-discovery-jobs.js +206 -0
  67. package/dist/peers/peer-discovery.js +223 -0
  68. package/dist/peers/peer-health-monitor.js +49 -0
  69. package/dist/{peer-identity.js → peers/peer-identity.js} +50 -1
  70. package/dist/{peer-runtime-service.js → peers/peer-runtime-service.js} +29 -7
  71. package/dist/{peer-server.js → peers/peer-server.js} +3 -2
  72. package/dist/{peer-store.js → peers/peer-store.js} +80 -9
  73. package/dist/{peer-types.js → peers/peer-types.js} +9 -0
  74. package/dist/peers/peer-web-proxy-contract.js +127 -0
  75. package/dist/{metrics.js → runtime/metrics.js} +5 -3
  76. package/dist/{relay-artifact-service.js → runtime/relay-artifact-service.js} +1 -1
  77. package/dist/runtime/relay-auth-service.js +63 -0
  78. package/dist/runtime/relay-dashboard-service.js +139 -0
  79. package/dist/{relay-external-activity-monitor.js → runtime/relay-external-activity-monitor.js} +140 -53
  80. package/dist/runtime/relay-runtime-active-sessions.js +387 -0
  81. package/dist/runtime/relay-runtime-dashboard.js +201 -0
  82. package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +307 -0
  83. package/dist/runtime/relay-runtime-sessions.js +623 -0
  84. package/dist/runtime/relay-runtime-types.js +1 -0
  85. package/dist/runtime/relay-runtime-updates-jobs.js +360 -0
  86. package/dist/runtime/relay-runtime.js +451 -0
  87. package/dist/runtime/runtime-cache.js +117 -0
  88. package/dist/{session-registry.js → state/session-registry.js} +3 -3
  89. package/dist/{operations.js → support/operations.js} +7 -7
  90. package/dist/{support-bundle.js → support/support-bundle.js} +1 -1
  91. package/dist/{web-api-contract.js → web/web-api-contract.js} +17 -3
  92. package/dist/web/web-api-types.js +1 -0
  93. package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +2 -2
  94. package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +24 -2
  95. package/dist/{web-dashboard-http.js → web/web-dashboard-http.js} +41 -5
  96. package/dist/{web-dashboard-pages.js → web/web-dashboard-pages.js} +37 -10
  97. package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +102 -7
  98. package/dist/web/web-dashboard-security.js +14 -0
  99. package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +12 -1
  100. package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
  101. package/dist/web/web-performance.js +60 -0
  102. package/dist/web/web-rate-limit.js +19 -0
  103. package/dist/{web-state.js → web/web-state.js} +74 -5
  104. package/dist/webui-assets/dashboard.css +171 -10
  105. package/dist/webui-assets/dashboard.js +514 -48
  106. package/dist/webui-assets/favicon.ico +0 -0
  107. package/dist/webui-assets/favicon.png +0 -0
  108. package/dist/webui-assets/logo.png +0 -0
  109. package/package.json +4 -3
  110. package/plugins/nordrelay/scripts/nordrelay.mjs +13 -4
  111. package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
  112. package/dist/relay-runtime.js +0 -1916
  113. package/dist/runtime-cache.js +0 -57
  114. /package/dist/{user-management-crypto.js → access/user-management-crypto.js} +0 -0
  115. /package/dist/{user-management-normalize.js → access/user-management-normalize.js} +0 -0
  116. /package/dist/{user-management-types.js → access/user-management-types.js} +0 -0
  117. /package/dist/{claude-code-auth.js → agents/claude-code/claude-code-auth.js} +0 -0
  118. /package/dist/{claude-code-launch.js → agents/claude-code/claude-code-launch.js} +0 -0
  119. /package/dist/{claude-code-state.js → agents/claude-code/claude-code-state.js} +0 -0
  120. /package/dist/{codex-auth.js → agents/codex/codex-auth.js} +0 -0
  121. /package/dist/{codex-config.js → agents/codex/codex-config.js} +0 -0
  122. /package/dist/{codex-launch.js → agents/codex/codex-launch.js} +0 -0
  123. /package/dist/{codex-state.js → agents/codex/codex-state.js} +0 -0
  124. /package/dist/{hermes-api.js → agents/hermes/hermes-api.js} +0 -0
  125. /package/dist/{hermes-auth.js → agents/hermes/hermes-auth.js} +0 -0
  126. /package/dist/{hermes-state.js → agents/hermes/hermes-state.js} +0 -0
  127. /package/dist/{openclaw-auth.js → agents/openclaw/openclaw-auth.js} +0 -0
  128. /package/dist/{openclaw-gateway.js → agents/openclaw/openclaw-gateway.js} +0 -0
  129. /package/dist/{openclaw-state.js → agents/openclaw/openclaw-state.js} +0 -0
  130. /package/dist/{pi-auth.js → agents/pi/pi-auth.js} +0 -0
  131. /package/dist/{pi-rpc.js → agents/pi/pi-rpc.js} +0 -0
  132. /package/dist/{pi-state.js → agents/pi/pi-state.js} +0 -0
  133. /package/dist/{agent-adapter.js → agents/shared/agent-adapter.js} +0 -0
  134. /package/dist/{agent.js → agents/shared/agent.js} +0 -0
  135. /package/dist/{artifacts.js → artifacts/artifacts.js} +0 -0
  136. /package/dist/{attachments.js → artifacts/attachments.js} +0 -0
  137. /package/dist/{voice.js → artifacts/voice.js} +0 -0
  138. /package/dist/{discord-rate-limit.js → channels/discord/discord-rate-limit.js} +0 -0
  139. /package/dist/{channel-adapter.js → channels/shared/channel-adapter.js} +0 -0
  140. /package/dist/{relay-runtime-types.js → channels/shared/channel-bridge-state.js} +0 -0
  141. /package/dist/{channel-command-catalog.js → channels/shared/channel-command-catalog.js} +0 -0
  142. /package/dist/{channel-command-core.js → channels/shared/channel-command-core.js} +0 -0
  143. /package/dist/{channel-prompt-engine.js → channels/shared/channel-prompt-engine.js} +0 -0
  144. /package/dist/{channel-runtime.js → channels/shared/channel-runtime.js} +0 -0
  145. /package/dist/{channel-turn-lifecycle.js → channels/shared/channel-turn-lifecycle.js} +0 -0
  146. /package/dist/{slack-rate-limit.js → channels/slack/slack-rate-limit.js} +0 -0
  147. /package/dist/{telegram-command-types.js → channels/telegram/telegram-command-types.js} +0 -0
  148. /package/dist/{telegram-rate-limit.js → channels/telegram/telegram-rate-limit.js} +0 -0
  149. /package/dist/{activity-events.js → core/activity-events.js} +0 -0
  150. /package/dist/{error-messages.js → core/error-messages.js} +0 -0
  151. /package/dist/{format.js → core/format.js} +0 -0
  152. /package/dist/{logger.js → core/logger.js} +0 -0
  153. /package/dist/{redaction.js → core/redaction.js} +0 -0
  154. /package/dist/{settings-service.js → core/settings-service.js} +0 -0
  155. /package/dist/{settings-wizard-test.js → core/settings-wizard-test.js} +0 -0
  156. /package/dist/{workspace-policy.js → core/workspace-policy.js} +0 -0
  157. /package/dist/{peer-auth.js → peers/peer-auth.js} +0 -0
  158. /package/dist/{peer-client.js → peers/peer-client.js} +0 -0
  159. /package/dist/{peer-context.js → peers/peer-context.js} +0 -0
  160. /package/dist/{peer-readiness.js → peers/peer-readiness.js} +0 -0
  161. /package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +0 -0
  162. /package/dist/{web-api-types.js → runtime/relay-runtime-delegate.js} +0 -0
  163. /package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +0 -0
  164. /package/dist/{remote-prompt.js → runtime/remote-prompt.js} +0 -0
  165. /package/dist/{bot-preferences.js → state/bot-preferences.js} +0 -0
  166. /package/dist/{job-store.js → state/job-store.js} +0 -0
  167. /package/dist/{persistence.js → state/persistence.js} +0 -0
  168. /package/dist/{prompt-store.js → state/prompt-store.js} +0 -0
  169. /package/dist/{state-backend.js → state/state-backend.js} +0 -0
  170. /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
  171. /package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +0 -0
  172. /package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +0 -0
  173. /package/dist/{web-dashboard-ui.js → web/web-dashboard-ui.js} +0 -0
@@ -1,6 +1,6 @@
1
- import { consumeRateLimit, resetRateLimit } from "./bot-rendering.js";
2
- import { friendlyErrorText } from "./error-messages.js";
3
- import { escapeHTML } from "./format.js";
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;
@@ -1,8 +1,8 @@
1
- import { permissionForCallbackData, permissionForCommand } from "./access-control.js";
2
- import { extractCommandName } from "./bot-rendering.js";
3
- import { escapeHTML } from "./format.js";
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 "./user-management.js";
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 "./agent.js";
3
- import { enabledAgents } from "./agent-factory.js";
4
- import { capabilitiesOf, idOf, labelOf, } from "./bot-rendering.js";
5
- import { checkAuthStatus } from "./codex-auth.js";
6
- import { contextKeyFromCtx } from "./context-key.js";
7
- import { friendlyErrorText } from "./error-messages.js";
8
- import { escapeHTML } from "./format.js";
9
- import { redactText } from "./redaction.js";
10
- import { renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
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) => {
@@ -1,8 +1,8 @@
1
1
  import { InlineKeyboard } from "grammy";
2
- import { getArtifactTurnReport, listRecentArtifactReports, removeArtifactTurn, } from "./artifacts.js";
3
- import { buildArtifactActionsKeyboard, filterArtifactReports, } from "./bot-rendering.js";
4
- import { renderArtifactReportsAction } from "./channel-actions.js";
5
- import { escapeHTML } from "./format.js";
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 "./channel-adapter.js";
3
- import { redactText } from "./redaction.js";
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 "./channel-command-catalog.js";
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]);
@@ -1,10 +1,10 @@
1
- import { getAgentDiagnostics } from "./agent-activity.js";
2
- import { formatQuietHours } from "./bot-preferences.js";
3
- import { cliPathOptions } from "./channel-command-service.js";
4
- import { checkAuthStatus } from "./codex-auth.js";
5
- import { contextKeyFromCtx } from "./context-key.js";
6
- import { getConnectorHealth, } from "./operations.js";
7
- import { renderAgentDiagnostics, renderDiagnosticsHTML, renderDiagnosticsPlain, renderHealthHTML, renderHealthPlain, } from "./bot-rendering.js";
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) {
@@ -1,8 +1,8 @@
1
1
  import { renderWelcomeFirstTime, renderWelcomeReturning, renderHelpMessage, } from "./bot-ui.js";
2
- import { authHelpText, capabilitiesOf, labelOf, } from "./bot-rendering.js";
3
- import { escapeHTML } from "./format.js";
4
- import { spawnConnectorRestart } from "./operations.js";
5
- import { renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
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) => {
@@ -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 "./agent-activity.js";
7
- import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, formatLockOwner, labelOf, parseActivityOptions, renderActivityTimeline, renderAuditEvents, renderProgressHTML, renderProgressPlain, renderSessionLocks, } from "./bot-rendering.js";
8
- import { escapeHTML } from "./format.js";
9
- import { renderSessionInfoHTML, renderSessionInfoPlain } from "./session-format.js";
10
- import { canWriteWithLock } from "./session-locks.js";
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 "./format.js";
7
- import { redactText } from "./redaction.js";
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;
@@ -1,6 +1,6 @@
1
- import { capabilitiesOf, labelOf, } from "./bot-rendering.js";
2
- import { escapeHTML } from "./format.js";
3
- import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "./workspace-policy.js";
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 "./channel-actions.js";
3
- import { contextKeyFromCtx } from "./context-key.js";
4
- import { friendlyErrorText } from "./error-messages.js";
5
- import { escapeHTML } from "./format.js";
6
- import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
7
- import { formatLocalDateTime } from "./bot-rendering.js";
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 "./context-key.js";
3
- import { getConnectorHealth, getVersionChecks } from "./operations.js";
4
- import { formatLocalDateTime } from "./bot-rendering.js";
5
- import { createSupportBundle } from "./support-bundle.js";
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 "./agent-adapter.js";
2
- import { agentLabel } from "./agent.js";
3
- import { parseAgentUpdateId, renderAgentUpdateJobAction, renderAgentUpdateJobsAction, renderAgentUpdateLogAction, renderAgentUpdatePickerAction, renderSelfUpdateStartedAction, } from "./channel-actions.js";
4
- import { escapeHTML } from "./format.js";
5
- import { spawnSelfUpdate } from "./operations.js";
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 "./codex-launch.js";
4
- import { CLAUDE_CODE_EFFORT_LEVELS, HERMES_REASONING_EFFORTS, OPENCLAW_THINKING_LEVELS, isAgentId, PI_THINKING_LEVELS, } from "./agent.js";
5
- import { parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
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
  /**
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 { startPeerServer } from "./peer-server.js";
24
- import { RelayRuntime } from "./relay-runtime.js";
25
- import { configureRedaction } from "./redaction.js";
26
- import { SessionRegistry } from "./session-registry.js";
27
- import { UserStore } from "./user-management.js";
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
+ }