@nordbyte/nordrelay 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.env.example +80 -11
  2. package/README.md +154 -22
  3. package/dist/access-control.js +7 -1
  4. package/dist/activity-events.js +44 -0
  5. package/dist/audit-log.js +40 -2
  6. package/dist/bot-preferences.js +1 -0
  7. package/dist/bot-rendering.js +10 -7
  8. package/dist/bot.js +535 -11
  9. package/dist/channel-actions.js +7 -2
  10. package/dist/channel-adapter.js +40 -7
  11. package/dist/channel-command-catalog.js +88 -0
  12. package/dist/channel-command-service.js +369 -0
  13. package/dist/channel-mirror-registry.js +77 -0
  14. package/dist/channel-peer-prompt.js +95 -0
  15. package/dist/channel-runtime.js +12 -5
  16. package/dist/channel-turn-service.js +237 -0
  17. package/dist/codex-state.js +114 -78
  18. package/dist/config-metadata.js +93 -13
  19. package/dist/config.js +103 -8
  20. package/dist/context-key.js +87 -5
  21. package/dist/discord-artifacts.js +165 -0
  22. package/dist/discord-bot.js +2073 -0
  23. package/dist/discord-channel-runtime.js +133 -0
  24. package/dist/discord-command-surface.js +57 -0
  25. package/dist/discord-rate-limit.js +141 -0
  26. package/dist/index.js +36 -5
  27. package/dist/job-store.js +127 -0
  28. package/dist/metrics.js +87 -0
  29. package/dist/peer-auth.js +85 -0
  30. package/dist/peer-client.js +256 -0
  31. package/dist/peer-context.js +21 -0
  32. package/dist/peer-identity.js +127 -0
  33. package/dist/peer-runtime-service.js +636 -0
  34. package/dist/peer-server.js +220 -0
  35. package/dist/peer-store.js +294 -0
  36. package/dist/peer-types.js +52 -0
  37. package/dist/relay-external-activity-monitor.js +47 -6
  38. package/dist/relay-runtime-helpers.js +208 -0
  39. package/dist/relay-runtime.js +897 -394
  40. package/dist/remote-prompt.js +98 -0
  41. package/dist/runtime-cache.js +57 -0
  42. package/dist/session-locks.js +10 -7
  43. package/dist/support-bundle.js +1 -0
  44. package/dist/telegram-access-commands.js +15 -2
  45. package/dist/telegram-access-middleware.js +16 -3
  46. package/dist/telegram-agent-commands.js +25 -0
  47. package/dist/telegram-artifact-commands.js +46 -0
  48. package/dist/telegram-command-menu.js +3 -53
  49. package/dist/telegram-diagnostics-command.js +5 -50
  50. package/dist/telegram-general-commands.js +16 -6
  51. package/dist/telegram-operational-commands.js +14 -6
  52. package/dist/telegram-preference-commands.js +23 -127
  53. package/dist/telegram-queue-commands.js +74 -4
  54. package/dist/telegram-support-command.js +7 -0
  55. package/dist/telegram-update-commands.js +27 -0
  56. package/dist/user-management.js +208 -0
  57. package/dist/web-api-contract.js +17 -0
  58. package/dist/web-dashboard-access-routes.js +74 -1
  59. package/dist/web-dashboard-artifact-routes.js +3 -3
  60. package/dist/web-dashboard-assets.js +2 -0
  61. package/dist/web-dashboard-pages.js +109 -13
  62. package/dist/web-dashboard-peer-routes.js +204 -0
  63. package/dist/web-dashboard-runtime-routes.js +53 -8
  64. package/dist/web-dashboard-session-routes.js +27 -20
  65. package/dist/web-dashboard-ui.js +2 -0
  66. package/dist/web-dashboard.js +160 -6
  67. package/dist/web-state.js +33 -2
  68. package/dist/webui-assets/dashboard.css +75 -1
  69. package/dist/webui-assets/dashboard.js +779 -55
  70. package/package.json +5 -2
  71. package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
@@ -1,5 +1,6 @@
1
1
  export const SECRET_KEYS = new Set([
2
2
  "TELEGRAM_BOT_TOKEN",
3
+ "DISCORD_BOT_TOKEN",
3
4
  "CODEX_API_KEY",
4
5
  "HERMES_API_KEY",
5
6
  "OPENCLAW_GATEWAY_TOKEN",
@@ -7,7 +8,24 @@ export const SECRET_KEYS = new Set([
7
8
  "OPENAI_API_KEY",
8
9
  "TELEGRAM_WEBHOOK_SECRET",
9
10
  ]);
11
+ const DISCORD_SETTING_HELP = {
12
+ DISCORD_ENABLED: "Set this to true after you create the Discord application, configure the bot token, and invite the bot to the server.",
13
+ DISCORD_BOT_TOKEN: "Discord Developer Portal: open your application, go to Bot, then copy or reset the bot token. Store only the token value here.",
14
+ DISCORD_CLIENT_ID: "Discord Developer Portal: open your application, go to General Information, then copy Application ID. This is the client id used for slash commands.",
15
+ DISCORD_GUILD_IDS: "Enable Developer Mode in Discord, right-click a server, choose Copy Server ID, then paste one or more comma-separated ids for fast guild slash-command registration.",
16
+ DISCORD_ALLOWED_GUILD_IDS: "Enable Developer Mode in Discord, right-click each allowed server, choose Copy Server ID, then enter the comma-separated allow-list. Leave blank to rely on NordRelay user/group access.",
17
+ DISCORD_ALLOWED_CHANNEL_IDS: "Enable Developer Mode in Discord, right-click a channel or thread, choose Copy Channel ID or Copy Thread ID, then enter the comma-separated allow-list. Leave blank to rely on registered channel access.",
18
+ DISCORD_MESSAGE_CONTENT_ENABLED: "Turn this on only if the bot has Message Content Intent enabled in the Discord Developer Portal under Bot > Privileged Gateway Intents.",
19
+ DISCORD_COMMAND_MODE: "Use slash for registered slash commands only, message for text commands like /session only, or both when Message Content Intent is enabled.",
20
+ DISCORD_AUTO_REGISTER_COMMANDS: "When enabled, NordRelay registers slash commands at startup. Guild ids update quickly; global command registration can take longer to appear in Discord.",
21
+ DISCORD_CLI_MIRROR_MODE: "Overrides the channel-neutral mirror default for Discord only. Leave blank to use NORDRELAY_CLI_MIRROR_MODE.",
22
+ DISCORD_CLI_MIRROR_MIN_UPDATE_MS: "Discord edit/update throttle for mirrored CLI activity. Increase this if Discord rate limits streaming status updates.",
23
+ DISCORD_NOTIFY_MODE: "Overrides the channel-neutral completion notification default for Discord only. Leave blank to use NORDRELAY_NOTIFY_MODE.",
24
+ DISCORD_QUIET_HOURS: "Use a local-time range like 22-7, off, or blank to inherit the channel-neutral quiet-hours setting.",
25
+ DISCORD_AUTO_SEND_ARTIFACTS: "Overrides automatic artifact upload behavior for Discord only. Leave blank to use NORDRELAY_AUTO_SEND_ARTIFACTS.",
26
+ };
10
27
  export const SETTING_DEFINITIONS = [
28
+ setting("TELEGRAM_ENABLED", "Enable Telegram", "Telegram", "boolean", "Start the Telegram bot adapter.", true),
11
29
  setting("TELEGRAM_BOT_TOKEN", "Telegram bot token", "Telegram", "secret", "BotFather token.", true),
12
30
  setting("TELEGRAM_TRANSPORT", "Telegram transport", "Telegram", "string", "polling or webhook.", true, ["polling", "webhook"]),
13
31
  setting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "Telegram", "string", "Public base URL for webhook mode.", true),
@@ -15,6 +33,20 @@ export const SETTING_DEFINITIONS = [
15
33
  setting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "Telegram", "number", "Local webhook bind port.", true),
16
34
  setting("TELEGRAM_WEBHOOK_PATH", "Webhook path", "Telegram", "string", "Webhook request path.", true),
17
35
  setting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "Telegram", "secret", "Optional Telegram webhook secret token.", true),
36
+ discordSetting("DISCORD_ENABLED", "Enable Discord", "boolean", "Start the Discord bot adapter.", true),
37
+ discordSetting("DISCORD_BOT_TOKEN", "Discord bot token", "secret", "Discord bot token.", true),
38
+ discordSetting("DISCORD_CLIENT_ID", "Discord client ID", "string", "Discord application/client id used for slash command registration.", true),
39
+ discordSetting("DISCORD_GUILD_IDS", "Discord guild IDs", "list", "Comma-separated guild ids for instant guild slash-command registration.", true),
40
+ discordSetting("DISCORD_ALLOWED_GUILD_IDS", "Allowed Discord guilds", "list", "Optional comma-separated guild allow-list.", true),
41
+ discordSetting("DISCORD_ALLOWED_CHANNEL_IDS", "Allowed Discord channels", "list", "Optional comma-separated channel allow-list before user/group checks.", true),
42
+ discordSetting("DISCORD_MESSAGE_CONTENT_ENABLED", "Message content intent", "boolean", "Read regular Discord text messages as prompts. Requires enabling the privileged intent in Discord.", true),
43
+ discordSetting("DISCORD_COMMAND_MODE", "Discord command mode", "string", "slash, message, or both.", true, ["slash", "message", "both"]),
44
+ discordSetting("DISCORD_AUTO_REGISTER_COMMANDS", "Auto-register slash commands", "boolean", "Register Discord slash commands on startup when client id is configured.", true),
45
+ discordSetting("DISCORD_CLI_MIRROR_MODE", "Discord mirror override", "string", "Optional Discord override for CLI mirror mode. Uses the NordRelay default when unset.", false, ["off", "status", "final", "full"]),
46
+ discordSetting("DISCORD_CLI_MIRROR_MIN_UPDATE_MS", "Discord mirror update override", "number", "Optional Discord override for mirrored edit interval.", true),
47
+ discordSetting("DISCORD_NOTIFY_MODE", "Discord notify override", "string", "Optional Discord override for completion notifications.", false, ["off", "minimal", "all"]),
48
+ discordSetting("DISCORD_QUIET_HOURS", "Discord quiet hours override", "string", "Optional Discord quiet hours override. Use HH-HH, off, or leave blank for default.", false),
49
+ discordSetting("DISCORD_AUTO_SEND_ARTIFACTS", "Discord auto-send artifacts override", "boolean", "Optional Discord override for automatic artifact summaries/uploads.", false),
18
50
  setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
19
51
  setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
20
52
  setting("NORDRELAY_HERMES_ENABLED", "Enable Hermes", "Agents", "boolean", "Allow Hermes sessions through the Hermes API Server.", true),
@@ -69,10 +101,15 @@ export const SETTING_DEFINITIONS = [
69
101
  setting("ENABLE_TELEGRAM_REACTIONS", "Enable Telegram reactions", "Operations", "boolean", "Send Telegram reactions.", true),
70
102
  setting("TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS", "Telegram send interval", "Operations", "number", "Minimum send interval.", true),
71
103
  setting("TELEGRAM_EDIT_MIN_INTERVAL_MS", "Telegram edit interval", "Operations", "number", "Minimum edit interval.", true),
72
- setting("TELEGRAM_CLI_MIRROR_MODE", "CLI mirror mode", "Operations", "string", "off, status, final, or full.", false, ["off", "status", "final", "full"]),
73
- setting("TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS", "CLI mirror update interval", "Operations", "number", "Minimum mirrored edit interval.", true),
74
- setting("TELEGRAM_NOTIFY_MODE", "Notify mode", "Operations", "string", "off, minimal, or all.", false, ["off", "minimal", "all"]),
75
- setting("TELEGRAM_QUIET_HOURS", "Quiet hours", "Operations", "string", "HH-HH or blank.", false),
104
+ 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"]),
105
+ setting("NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS", "Default mirror update interval", "Operations", "number", "Default minimum mirrored edit interval.", true),
106
+ setting("NORDRELAY_NOTIFY_MODE", "Default notify mode", "Operations", "string", "Default completion notifications: off, minimal, or all.", false, ["off", "minimal", "all"]),
107
+ setting("NORDRELAY_QUIET_HOURS", "Default quiet hours", "Operations", "string", "Default quiet hours. Use HH-HH, off, or leave blank.", false),
108
+ setting("NORDRELAY_AUTO_SEND_ARTIFACTS", "Default auto-send artifacts", "Operations", "boolean", "Default automatic artifact summaries/uploads for chat adapters.", false),
109
+ setting("TELEGRAM_CLI_MIRROR_MODE", "Telegram mirror override", "Operations", "string", "Optional Telegram override for CLI mirror mode. Uses the NordRelay default when unset.", false, ["off", "status", "final", "full"]),
110
+ setting("TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS", "Telegram mirror update override", "Operations", "number", "Optional Telegram override for mirrored edit interval.", true),
111
+ setting("TELEGRAM_NOTIFY_MODE", "Telegram notify override", "Operations", "string", "Optional Telegram override for completion notifications.", false, ["off", "minimal", "all"]),
112
+ setting("TELEGRAM_QUIET_HOURS", "Telegram quiet hours override", "Operations", "string", "Optional Telegram quiet hours override. Use HH-HH, off, or leave blank for default.", false),
76
113
  setting("TELEGRAM_REDACT_PATTERNS", "Redaction patterns", "Operations", "list", "Additional comma-separated regex patterns.", true),
77
114
  setting("NORDRELAY_UPDATE_METHOD", "Update method", "Operations", "string", "auto, npm, or git.", true, ["auto", "npm", "git"]),
78
115
  setting("MAX_FILE_SIZE", "Max file size", "Artifacts", "number", "Max inbound/outbound file size.", true),
@@ -81,14 +118,23 @@ export const SETTING_DEFINITIONS = [
81
118
  setting("ARTIFACT_MAX_INBOX_DIRS", "Max inbox dirs", "Artifacts", "number", "Maximum inbox dirs retained.", true),
82
119
  setting("ARTIFACT_IGNORE_DIRS", "Artifact ignore dirs", "Artifacts", "list", "Extra ignored dirs or relative paths.", true),
83
120
  setting("ARTIFACT_IGNORE_GLOBS", "Artifact ignore globs", "Artifacts", "list", "Extra ignored glob patterns.", true),
84
- setting("TELEGRAM_AUTO_SEND_ARTIFACTS", "Auto-send artifacts", "Artifacts", "boolean", "Automatically send artifact files.", false),
121
+ setting("TELEGRAM_AUTO_SEND_ARTIFACTS", "Telegram auto-send artifacts override", "Artifacts", "boolean", "Optional Telegram override for automatic artifact summaries/uploads.", false),
85
122
  setting("WORKSPACE_ALLOWED_ROOTS", "Workspace allowed roots", "Workspace", "list", "Restrict selectable workspaces.", true),
86
123
  setting("WORKSPACE_WARN_ROOTS", "Workspace warn roots", "Workspace", "list", "Warn for broad workspace roots.", true),
87
124
  setting("NORDRELAY_STATE_BACKEND", "State backend", "Workspace", "string", "json or sqlite.", true, ["json", "sqlite"]),
88
125
  setting("NORDRELAY_AUDIT_MAX_EVENTS", "Audit max events", "Workspace", "number", "Retained audit events.", true),
89
126
  setting("NORDRELAY_SESSION_LOCK_TTL_MS", "Session lock TTL", "Workspace", "number", "Write-lock TTL.", true),
127
+ setting("NORDRELAY_DASHBOARD_CACHE_TTL_MS", "Dashboard cache TTL", "Workspace", "number", "Stale-while-refresh TTL for expensive dashboard API snapshots.", true),
128
+ setting("NORDRELAY_UNIFIED_JOB_MAX_ITEMS", "Unified job history", "Workspace", "number", "Maximum persisted unified jobs retained for the WebUI jobs view.", true),
90
129
  setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
91
130
  setting("NORDRELAY_CLI_VERSION_CACHE_TTL_MS", "CLI version cache TTL", "Workspace", "number", "Installed agent CLI version cache TTL.", true),
131
+ setting("NORDRELAY_PEER_ENABLED", "Enable peer server", "Peers", "boolean", "Expose the dedicated authenticated NordRelay peer API.", true),
132
+ setting("NORDRELAY_PEER_NAME", "Peer display name", "Peers", "string", "Human-readable name shown to paired NordRelay instances.", true),
133
+ setting("NORDRELAY_PEER_HOST", "Peer bind host", "Peers", "string", "Bind host for the peer API. Use 127.0.0.1 for local-only or a LAN/interface IP when explicitly exposing peers.", true),
134
+ setting("NORDRELAY_PEER_PORT", "Peer port", "Peers", "number", "Port for the peer API.", true),
135
+ setting("NORDRELAY_PEER_PUBLIC_URL", "Peer public URL", "Peers", "string", "Optional public URL other instances should use for this node.", true),
136
+ setting("NORDRELAY_PEER_TLS_ENABLED", "Peer TLS enabled", "Peers", "boolean", "Serve the peer API over HTTPS with an automatically generated local certificate.", true),
137
+ setting("NORDRELAY_PEER_REQUIRE_TLS", "Require peer TLS", "Peers", "boolean", "Reject plaintext peer serving on non-loopback hosts.", true),
92
138
  setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
93
139
  setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
94
140
  setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
@@ -104,7 +150,22 @@ export const SETTING_DEFINITIONS = [
104
150
  setting("NORDRELAY_ENV_FILE", "Env file path", "Dashboard", "string", "Optional explicit env-file path used by the CLI wrapper and dashboard.", true),
105
151
  ];
106
152
  const EXAMPLE_VALUES = {
153
+ "TELEGRAM_ENABLED": "true",
107
154
  "TELEGRAM_BOT_TOKEN": "123456789:replace-me",
155
+ "DISCORD_ENABLED": "false",
156
+ "DISCORD_BOT_TOKEN": "",
157
+ "DISCORD_CLIENT_ID": "",
158
+ "DISCORD_GUILD_IDS": "",
159
+ "DISCORD_ALLOWED_GUILD_IDS": "",
160
+ "DISCORD_ALLOWED_CHANNEL_IDS": "",
161
+ "DISCORD_MESSAGE_CONTENT_ENABLED": "true",
162
+ "DISCORD_COMMAND_MODE": "both",
163
+ "DISCORD_AUTO_REGISTER_COMMANDS": "true",
164
+ "DISCORD_CLI_MIRROR_MODE": "",
165
+ "DISCORD_CLI_MIRROR_MIN_UPDATE_MS": "",
166
+ "DISCORD_NOTIFY_MODE": "",
167
+ "DISCORD_QUIET_HOURS": "",
168
+ "DISCORD_AUTO_SEND_ARTIFACTS": "",
108
169
  "NORDRELAY_CODEX_ENABLED": "true",
109
170
  "NORDRELAY_PI_ENABLED": "false",
110
171
  "NORDRELAY_HERMES_ENABLED": "false",
@@ -165,9 +226,14 @@ const EXAMPLE_VALUES = {
165
226
  "TELEGRAM_WEBHOOK_PORT": "8080",
166
227
  "TELEGRAM_WEBHOOK_PATH": "/telegram/webhook",
167
228
  "TELEGRAM_WEBHOOK_SECRET": "",
168
- "TELEGRAM_CLI_MIRROR_MODE": "status",
169
- "TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS": "4000",
170
- "TELEGRAM_NOTIFY_MODE": "minimal",
229
+ "NORDRELAY_CLI_MIRROR_MODE": "status",
230
+ "NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS": "4000",
231
+ "NORDRELAY_NOTIFY_MODE": "minimal",
232
+ "NORDRELAY_QUIET_HOURS": "",
233
+ "NORDRELAY_AUTO_SEND_ARTIFACTS": "false",
234
+ "TELEGRAM_CLI_MIRROR_MODE": "",
235
+ "TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS": "",
236
+ "TELEGRAM_NOTIFY_MODE": "",
171
237
  "TELEGRAM_QUIET_HOURS": "",
172
238
  "TELEGRAM_REDACT_PATTERNS": "",
173
239
  "MAX_FILE_SIZE": "20971520",
@@ -176,12 +242,21 @@ const EXAMPLE_VALUES = {
176
242
  "ARTIFACT_MAX_INBOX_DIRS": "30",
177
243
  "ARTIFACT_IGNORE_DIRS": "",
178
244
  "ARTIFACT_IGNORE_GLOBS": "",
179
- "TELEGRAM_AUTO_SEND_ARTIFACTS": "false",
245
+ "TELEGRAM_AUTO_SEND_ARTIFACTS": "",
180
246
  "NORDRELAY_STATE_BACKEND": "json",
181
247
  "NORDRELAY_AUDIT_MAX_EVENTS": "1000",
182
248
  "NORDRELAY_SESSION_LOCK_TTL_MS": "1800000",
249
+ "NORDRELAY_DASHBOARD_CACHE_TTL_MS": "10000",
250
+ "NORDRELAY_UNIFIED_JOB_MAX_ITEMS": "1000",
183
251
  "NORDRELAY_VERSION_CACHE_TTL_MS": "3600000",
184
252
  "NORDRELAY_CLI_VERSION_CACHE_TTL_MS": "60000",
253
+ "NORDRELAY_PEER_ENABLED": "false",
254
+ "NORDRELAY_PEER_NAME": "",
255
+ "NORDRELAY_PEER_HOST": "127.0.0.1",
256
+ "NORDRELAY_PEER_PORT": "31979",
257
+ "NORDRELAY_PEER_PUBLIC_URL": "",
258
+ "NORDRELAY_PEER_TLS_ENABLED": "true",
259
+ "NORDRELAY_PEER_REQUIRE_TLS": "true",
185
260
  "NORDRELAY_DASHBOARD_HOST": "127.0.0.1",
186
261
  "NORDRELAY_DASHBOARD_PORT": "31878",
187
262
  "NORDRELAY_ENV_FILE": "",
@@ -199,7 +274,8 @@ const EXAMPLE_VALUES = {
199
274
  "FASTER_WHISPER_TIMEOUT_MS": "600000",
200
275
  };
201
276
  const GROUP_INTROS = {
202
- Telegram: "Required Telegram bot and transport settings.",
277
+ Telegram: "Telegram bot and transport settings.",
278
+ Discord: "Discord bot settings. Discord is opt-in and uses the same NordRelay users, groups, and permissions as Telegram.",
203
279
  Agents: "Agent access. Codex is enabled by default; Pi, Hermes, OpenClaw, and Claude Code are opt-in.",
204
280
  Codex: "Codex defaults for newly created or reattached sessions.",
205
281
  Pi: "Pi coding agent defaults.",
@@ -209,6 +285,7 @@ const GROUP_INTROS = {
209
285
  Operations: "Runtime output, logging, update, and Telegram behavior controls.",
210
286
  Artifacts: "File, artifact, and retention controls.",
211
287
  Workspace: "State and workspace guardrails.",
288
+ Peers: "Optional NordRelay-to-NordRelay federation. Pairing is explicit, authenticated, scoped, and TLS-protected.",
212
289
  Voice: "Optional voice transcription settings.",
213
290
  Dashboard: "Local WebUI dashboard. User login is required for every page, API route, SSE stream, artifact download, and health endpoint.",
214
291
  };
@@ -218,7 +295,7 @@ export function envExampleValue(key) {
218
295
  export function renderEnvExample() {
219
296
  const lines = [
220
297
  "# NordRelay runtime config example.",
221
- "# Access is managed with NordRelay users, groups, linked Telegram identities, and enabled Telegram group chats.",
298
+ "# Access is managed with NordRelay users, groups, linked chat identities, and enabled group/guild channels.",
222
299
  "# Create the first admin with `nordrelay init` or `nordrelay user create-admin`.",
223
300
  ];
224
301
  let currentGroup = "";
@@ -235,6 +312,9 @@ export function renderEnvExample() {
235
312
  }
236
313
  return `${lines.join('\n')}\n`;
237
314
  }
238
- function setting(key, label, group, kind, description, restartRequired, options) {
239
- return { key, label, group, kind, description, restartRequired, options };
315
+ function setting(key, label, group, kind, description, restartRequired, options, help) {
316
+ return { key, label, group, kind, description, help, restartRequired, options };
317
+ }
318
+ function discordSetting(key, label, kind, description, restartRequired, options) {
319
+ return setting(key, label, "Discord", kind, description, restartRequired, options, DISCORD_SETTING_HELP[key]);
240
320
  }
package/dist/config.js CHANGED
@@ -5,13 +5,20 @@ import { CLAUDE_CODE_EFFORT_LEVELS, HERMES_REASONING_EFFORTS, OPENCLAW_THINKING_
5
5
  import { parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
6
6
  export function loadConfig() {
7
7
  loadEnvFile(path.resolve(process.cwd(), ".env"));
8
- const telegramBotToken = requireEnv("TELEGRAM_BOT_TOKEN");
8
+ const adapterWarnings = [];
9
+ const requestedTelegramEnabled = parseBooleanEnv(optionalString(process.env.TELEGRAM_ENABLED), true);
10
+ const telegramBotToken = optionalString(process.env.TELEGRAM_BOT_TOKEN) ?? "";
9
11
  const telegramRateLimitMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS), 80, "TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS");
10
12
  const telegramEditMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_EDIT_MIN_INTERVAL_MS), 1_200, "TELEGRAM_EDIT_MIN_INTERVAL_MS");
11
- const telegramMirrorMode = parseMirrorMode(optionalString(process.env.TELEGRAM_CLI_MIRROR_MODE), "status");
12
- const telegramMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS), 4_000, "TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS");
13
- const telegramNotifyMode = parseNotifyMode(optionalString(process.env.TELEGRAM_NOTIFY_MODE), "minimal");
14
- const telegramQuietHours = parseQuietHours(optionalString(process.env.TELEGRAM_QUIET_HOURS));
13
+ const mirrorMode = parseMirrorMode(optionalString(process.env.NORDRELAY_CLI_MIRROR_MODE), "status");
14
+ const mirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS), 4_000, "NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS");
15
+ const notifyMode = parseNotifyMode(optionalString(process.env.NORDRELAY_NOTIFY_MODE), "minimal");
16
+ const quietHours = parseQuietHoursOverride(process.env.NORDRELAY_QUIET_HOURS, null);
17
+ const autoSendArtifacts = parseBooleanEnv(optionalString(process.env.NORDRELAY_AUTO_SEND_ARTIFACTS), false);
18
+ const telegramMirrorMode = parseMirrorMode(optionalString(process.env.TELEGRAM_CLI_MIRROR_MODE), mirrorMode);
19
+ const telegramMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS), mirrorMinUpdateMs, "TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS");
20
+ const telegramNotifyMode = parseNotifyMode(optionalString(process.env.TELEGRAM_NOTIFY_MODE), notifyMode);
21
+ const telegramQuietHours = parseQuietHoursOverride(process.env.TELEGRAM_QUIET_HOURS, quietHours);
15
22
  const telegramRedactPatterns = parseOptionalStringList(optionalString(process.env.TELEGRAM_REDACT_PATTERNS));
16
23
  const telegramTransport = parseTelegramTransport(optionalString(process.env.TELEGRAM_TRANSPORT));
17
24
  const telegramWebhookUrl = optionalString(process.env.TELEGRAM_WEBHOOK_URL);
@@ -19,6 +26,19 @@ export function loadConfig() {
19
26
  const telegramWebhookPort = parsePositiveIntegerEnv(optionalString(process.env.TELEGRAM_WEBHOOK_PORT), 8080, "TELEGRAM_WEBHOOK_PORT");
20
27
  const telegramWebhookPath = parseWebhookPath(optionalString(process.env.TELEGRAM_WEBHOOK_PATH));
21
28
  const telegramWebhookSecret = optionalString(process.env.TELEGRAM_WEBHOOK_SECRET);
29
+ const requestedDiscordEnabled = parseBooleanEnv(optionalString(process.env.DISCORD_ENABLED), false);
30
+ const discordBotToken = optionalString(process.env.DISCORD_BOT_TOKEN);
31
+ const discordClientId = optionalString(process.env.DISCORD_CLIENT_ID);
32
+ const discordGuildIds = parseOptionalStringList(optionalString(process.env.DISCORD_GUILD_IDS));
33
+ const discordAllowedGuildIds = parseOptionalStringList(optionalString(process.env.DISCORD_ALLOWED_GUILD_IDS));
34
+ const discordAllowedChannelIds = parseOptionalStringList(optionalString(process.env.DISCORD_ALLOWED_CHANNEL_IDS));
35
+ const discordMessageContentEnabled = parseBooleanEnv(optionalString(process.env.DISCORD_MESSAGE_CONTENT_ENABLED), true);
36
+ const discordCommandMode = parseDiscordCommandMode(optionalString(process.env.DISCORD_COMMAND_MODE));
37
+ const discordAutoRegisterCommands = parseBooleanEnv(optionalString(process.env.DISCORD_AUTO_REGISTER_COMMANDS), true);
38
+ const discordMirrorMode = parseMirrorMode(optionalString(process.env.DISCORD_CLI_MIRROR_MODE), mirrorMode);
39
+ const discordMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.DISCORD_CLI_MIRROR_MIN_UPDATE_MS), mirrorMinUpdateMs, "DISCORD_CLI_MIRROR_MIN_UPDATE_MS");
40
+ const discordNotifyMode = parseNotifyMode(optionalString(process.env.DISCORD_NOTIFY_MODE), notifyMode);
41
+ const discordQuietHours = parseQuietHoursOverride(process.env.DISCORD_QUIET_HOURS, quietHours);
22
42
  const workspace = resolveWorkspace();
23
43
  const workspaceAllowedRoots = parsePathList(optionalString(process.env.WORKSPACE_ALLOWED_ROOTS));
24
44
  const workspaceWarnRoots = parsePathList(optionalString(process.env.WORKSPACE_WARN_ROOTS));
@@ -29,7 +49,8 @@ export function loadConfig() {
29
49
  const artifactMaxInboxDirs = parsePositiveIntegerEnv(optionalString(process.env.ARTIFACT_MAX_INBOX_DIRS), 30, "ARTIFACT_MAX_INBOX_DIRS");
30
50
  const artifactIgnoreDirs = parseOptionalStringList(optionalString(process.env.ARTIFACT_IGNORE_DIRS));
31
51
  const artifactIgnoreGlobs = parseOptionalStringList(optionalString(process.env.ARTIFACT_IGNORE_GLOBS));
32
- const telegramAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.TELEGRAM_AUTO_SEND_ARTIFACTS), false);
52
+ const telegramAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.TELEGRAM_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
53
+ const discordAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.DISCORD_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
33
54
  const codexEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_CODEX_ENABLED), true);
34
55
  const codexApiKey = optionalString(process.env.CODEX_API_KEY);
35
56
  const codexModel = optionalString(process.env.CODEX_MODEL);
@@ -86,13 +107,44 @@ export function loadConfig() {
86
107
  const voiceTranscribeOnly = parseBooleanEnv(optionalString(process.env.VOICE_TRANSCRIBE_ONLY), false);
87
108
  const auditMaxEvents = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_AUDIT_MAX_EVENTS), 1000, "NORDRELAY_AUDIT_MAX_EVENTS");
88
109
  const sessionLockTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_SESSION_LOCK_TTL_MS), 30 * 60 * 1000, "NORDRELAY_SESSION_LOCK_TTL_MS");
89
- if (telegramTransport === "webhook" && !telegramWebhookUrl) {
90
- throw new Error("TELEGRAM_TRANSPORT=webhook requires TELEGRAM_WEBHOOK_URL");
110
+ const dashboardCacheTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_DASHBOARD_CACHE_TTL_MS), 10_000, "NORDRELAY_DASHBOARD_CACHE_TTL_MS");
111
+ const unifiedJobMaxItems = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_UNIFIED_JOB_MAX_ITEMS), 1000, "NORDRELAY_UNIFIED_JOB_MAX_ITEMS");
112
+ const peerEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_ENABLED), false);
113
+ const peerName = optionalString(process.env.NORDRELAY_PEER_NAME);
114
+ const peerHost = optionalString(process.env.NORDRELAY_PEER_HOST) ?? "127.0.0.1";
115
+ const peerPort = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_PEER_PORT), 31979, "NORDRELAY_PEER_PORT");
116
+ const peerPublicUrl = optionalString(process.env.NORDRELAY_PEER_PUBLIC_URL);
117
+ const peerTlsEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_TLS_ENABLED), true);
118
+ const peerRequireTls = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_REQUIRE_TLS), true);
119
+ let telegramEnabled = requestedTelegramEnabled;
120
+ if (telegramEnabled && telegramTransport === "webhook" && !telegramWebhookUrl) {
121
+ telegramEnabled = false;
122
+ adapterWarnings.push("Telegram disabled: TELEGRAM_TRANSPORT=webhook requires TELEGRAM_WEBHOOK_URL.");
123
+ }
124
+ if (telegramEnabled && !telegramBotToken) {
125
+ telegramEnabled = false;
126
+ adapterWarnings.push("Telegram disabled: TELEGRAM_BOT_TOKEN is missing.");
127
+ }
128
+ let discordEnabled = requestedDiscordEnabled;
129
+ if (discordEnabled && !discordBotToken) {
130
+ discordEnabled = false;
131
+ adapterWarnings.push("Discord disabled: DISCORD_ENABLED=true requires DISCORD_BOT_TOKEN.");
132
+ }
133
+ if (!telegramEnabled && !discordEnabled) {
134
+ const detail = adapterWarnings.length > 0 ? ` ${adapterWarnings.join(" ")}` : "";
135
+ throw new Error(`At least one usable chat adapter must be enabled.${detail}`);
91
136
  }
92
137
  return {
138
+ adapterWarnings,
139
+ telegramEnabled,
93
140
  telegramBotToken,
94
141
  telegramRateLimitMinIntervalMs,
95
142
  telegramEditMinIntervalMs,
143
+ mirrorMode,
144
+ mirrorMinUpdateMs,
145
+ notifyMode,
146
+ quietHours,
147
+ autoSendArtifacts,
96
148
  telegramMirrorMode,
97
149
  telegramMirrorMinUpdateMs,
98
150
  telegramNotifyMode,
@@ -104,6 +156,20 @@ export function loadConfig() {
104
156
  telegramWebhookPort,
105
157
  telegramWebhookPath,
106
158
  telegramWebhookSecret,
159
+ discordEnabled,
160
+ discordBotToken,
161
+ discordClientId,
162
+ discordGuildIds,
163
+ discordAllowedGuildIds,
164
+ discordAllowedChannelIds,
165
+ discordMessageContentEnabled,
166
+ discordCommandMode,
167
+ discordAutoRegisterCommands,
168
+ discordMirrorMode,
169
+ discordMirrorMinUpdateMs,
170
+ discordNotifyMode,
171
+ discordQuietHours,
172
+ discordAutoSendArtifacts,
107
173
  workspace,
108
174
  workspaceAllowedRoots,
109
175
  workspaceWarnRoots,
@@ -170,6 +236,15 @@ export function loadConfig() {
170
236
  voiceTranscribeOnly,
171
237
  auditMaxEvents,
172
238
  sessionLockTtlMs,
239
+ dashboardCacheTtlMs,
240
+ unifiedJobMaxItems,
241
+ peerEnabled,
242
+ peerName,
243
+ peerHost,
244
+ peerPort,
245
+ peerPublicUrl,
246
+ peerTlsEnabled,
247
+ peerRequireTls,
173
248
  };
174
249
  }
175
250
  /**
@@ -224,6 +299,16 @@ function optionalString(value) {
224
299
  const trimmed = value?.trim();
225
300
  return trimmed ? trimmed : undefined;
226
301
  }
302
+ function parseQuietHoursOverride(value, fallback) {
303
+ const normalized = value?.trim().toLowerCase();
304
+ if (!normalized) {
305
+ return fallback;
306
+ }
307
+ if (normalized === "off" || normalized === "none" || normalized === "false" || normalized === "0") {
308
+ return null;
309
+ }
310
+ return parseQuietHours(normalized);
311
+ }
227
312
  function parseOptionalStringList(raw) {
228
313
  if (!raw) {
229
314
  return [];
@@ -342,6 +427,16 @@ function parseTelegramTransport(raw) {
342
427
  console.warn(`Invalid TELEGRAM_TRANSPORT value: "${raw}". Expected polling or webhook. Falling back to polling.`);
343
428
  return "polling";
344
429
  }
430
+ function parseDiscordCommandMode(raw) {
431
+ if (!raw) {
432
+ return "both";
433
+ }
434
+ if (raw === "slash" || raw === "message" || raw === "both") {
435
+ return raw;
436
+ }
437
+ console.warn(`Invalid DISCORD_COMMAND_MODE value: "${raw}". Expected slash, message, or both. Falling back to both.`);
438
+ return "both";
439
+ }
345
440
  function parseWebhookPath(raw) {
346
441
  if (!raw) {
347
442
  return "/telegram/webhook";
@@ -1,25 +1,36 @@
1
- export function contextKeyFromMessage(chatId, messageThreadId) {
1
+ import { parsePeerRuntimeContextKey } from "./peer-context.js";
2
+ export function telegramContextKeyFromMessage(chatId, messageThreadId) {
2
3
  if (messageThreadId !== undefined) {
3
4
  return `${chatId}:${messageThreadId}`;
4
5
  }
5
6
  return `${chatId}`;
6
7
  }
7
- export function contextKeyFromCtx(ctx) {
8
+ export function telegramContextKeyFromCtx(ctx) {
8
9
  const chatId = ctx.chat?.id;
9
10
  if (chatId === undefined) {
10
11
  return null;
11
12
  }
12
13
  const threadId = ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id;
13
- return contextKeyFromMessage(chatId, threadId);
14
+ return telegramContextKeyFromMessage(chatId, threadId);
14
15
  }
15
- export function parseContextKey(key) {
16
+ export function parseTelegramContextKey(key) {
16
17
  const parts = key.split(":");
17
18
  const chatId = Number(parts[0]);
18
19
  const messageThreadId = parts[1] ? Number(parts[1]) : undefined;
19
20
  return { chatId, messageThreadId };
20
21
  }
22
+ export function contextKeyFromMessage(chatId, messageThreadId) {
23
+ return telegramContextKeyFromMessage(chatId, messageThreadId);
24
+ }
25
+ export function contextKeyFromCtx(ctx) {
26
+ return telegramContextKeyFromCtx(ctx);
27
+ }
28
+ export function parseContextKey(key) {
29
+ return parseTelegramContextKey(key);
30
+ }
21
31
  export function isTopicContextKey(key) {
22
- return key.includes(":");
32
+ const parsed = parseChannelContextKey(key);
33
+ return Boolean(parsed?.topicId);
23
34
  }
24
35
  export function isTelegramContextKey(key) {
25
36
  const parts = key.split(":");
@@ -44,3 +55,74 @@ export function isTelegramContextKey(key) {
44
55
  const threadId = Number(threadIdText);
45
56
  return Number.isSafeInteger(threadId) && threadId > 0;
46
57
  }
58
+ export function discordContextKey(input) {
59
+ const guildId = input.guildId || "dm";
60
+ const topic = input.threadId && input.threadId !== input.channelId ? `:${input.threadId}` : "";
61
+ return `discord:${guildId}:${input.channelId}${topic}`;
62
+ }
63
+ export function isDiscordContextKey(key) {
64
+ return /^discord:[^:]+:[^:]+(?::[^:]+)?$/.test(key);
65
+ }
66
+ export function parseDiscordContextKey(key) {
67
+ if (!isDiscordContextKey(key)) {
68
+ return null;
69
+ }
70
+ const [, guild, channelId, threadId] = key.split(":");
71
+ if (!channelId) {
72
+ return null;
73
+ }
74
+ return {
75
+ guildId: guild === "dm" ? undefined : guild,
76
+ channelId,
77
+ threadId,
78
+ };
79
+ }
80
+ export function parseChannelContextKey(key) {
81
+ const rawKey = String(key);
82
+ if (isTelegramContextKey(rawKey)) {
83
+ const parsed = parseTelegramContextKey(rawKey);
84
+ return {
85
+ channelId: "telegram",
86
+ contextKey: rawKey,
87
+ chatId: String(parsed.chatId),
88
+ topicId: parsed.messageThreadId === undefined ? undefined : String(parsed.messageThreadId),
89
+ };
90
+ }
91
+ const discord = parseDiscordContextKey(rawKey);
92
+ if (discord) {
93
+ return {
94
+ channelId: "discord",
95
+ contextKey: rawKey,
96
+ chatId: discord.channelId,
97
+ topicId: discord.threadId,
98
+ guildId: discord.guildId,
99
+ };
100
+ }
101
+ if (rawKey.startsWith("web:")) {
102
+ return {
103
+ channelId: "web",
104
+ contextKey: rawKey,
105
+ chatId: rawKey.slice("web:".length) || "dashboard",
106
+ };
107
+ }
108
+ if (rawKey.startsWith("cli:")) {
109
+ return {
110
+ channelId: "cli",
111
+ contextKey: rawKey,
112
+ chatId: rawKey.slice("cli:".length) || "local",
113
+ };
114
+ }
115
+ const peer = parsePeerRuntimeContextKey(rawKey);
116
+ if (peer) {
117
+ return {
118
+ channelId: "peer",
119
+ contextKey: rawKey,
120
+ chatId: peer.peerId,
121
+ topicId: peer.sourceContextKey,
122
+ };
123
+ }
124
+ return null;
125
+ }
126
+ export function channelIdForContextKey(key) {
127
+ return parseChannelContextKey(key)?.channelId ?? "cli";
128
+ }