@nordbyte/nordrelay 0.6.0 → 0.8.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 (62) hide show
  1. package/.env.example +52 -0
  2. package/README.md +171 -50
  3. package/dist/access-control.js +6 -1
  4. package/dist/activity-events.js +2 -2
  5. package/dist/adapter-conformance.js +61 -0
  6. package/dist/bot-preferences.js +1 -0
  7. package/dist/bot.js +95 -37
  8. package/dist/channel-adapter.js +44 -11
  9. package/dist/channel-command-catalog.js +94 -0
  10. package/dist/channel-command-core.js +60 -0
  11. package/dist/channel-command-service.js +230 -1
  12. package/dist/channel-mirror-registry.js +84 -0
  13. package/dist/channel-peer-prompt.js +95 -0
  14. package/dist/channel-prompt-engine.js +177 -0
  15. package/dist/channel-runtime.js +12 -5
  16. package/dist/channel-turn-lifecycle.js +73 -0
  17. package/dist/codex-state.js +114 -78
  18. package/dist/config-metadata.js +82 -8
  19. package/dist/config.js +79 -7
  20. package/dist/context-key.js +42 -0
  21. package/dist/discord-bot.js +173 -342
  22. package/dist/discord-command-surface.js +11 -73
  23. package/dist/index.js +29 -0
  24. package/dist/metrics.js +48 -0
  25. package/dist/peer-auth.js +85 -0
  26. package/dist/peer-client.js +288 -0
  27. package/dist/peer-context.js +21 -0
  28. package/dist/peer-identity.js +127 -0
  29. package/dist/peer-readiness.js +77 -0
  30. package/dist/peer-runtime-service.js +658 -0
  31. package/dist/peer-server.js +220 -0
  32. package/dist/peer-store.js +307 -0
  33. package/dist/peer-types.js +52 -0
  34. package/dist/relay-runtime-helpers.js +210 -0
  35. package/dist/relay-runtime.js +79 -274
  36. package/dist/remote-prompt.js +98 -0
  37. package/dist/settings-wizard-test.js +216 -0
  38. package/dist/slack-artifacts.js +165 -0
  39. package/dist/slack-bot.js +1461 -0
  40. package/dist/slack-channel-runtime.js +147 -0
  41. package/dist/slack-command-surface.js +46 -0
  42. package/dist/slack-diagnostics.js +116 -0
  43. package/dist/slack-rate-limit.js +139 -0
  44. package/dist/telegram-command-menu.js +3 -53
  45. package/dist/telegram-general-commands.js +14 -0
  46. package/dist/telegram-preference-commands.js +23 -127
  47. package/dist/user-management-crypto.js +38 -0
  48. package/dist/user-management-normalize.js +188 -0
  49. package/dist/user-management-types.js +1 -0
  50. package/dist/user-management.js +193 -196
  51. package/dist/web-api-contract.js +16 -0
  52. package/dist/web-dashboard-access-routes.js +62 -0
  53. package/dist/web-dashboard-assets.js +1 -0
  54. package/dist/web-dashboard-pages.js +26 -4
  55. package/dist/web-dashboard-peer-routes.js +225 -0
  56. package/dist/web-dashboard-ui.js +1 -0
  57. package/dist/web-dashboard.js +46 -0
  58. package/dist/web-state.js +2 -2
  59. package/dist/webui-assets/dashboard.css +193 -0
  60. package/dist/webui-assets/dashboard.js +870 -57
  61. package/package.json +5 -2
  62. package/plugins/nordrelay/scripts/nordrelay.mjs +468 -11
package/dist/config.js CHANGED
@@ -5,8 +5,9 @@ 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 telegramEnabled = parseBooleanEnv(optionalString(process.env.TELEGRAM_ENABLED), true);
9
- const telegramBotToken = telegramEnabled ? 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) ?? "";
10
11
  const telegramRateLimitMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS), 80, "TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS");
11
12
  const telegramEditMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_EDIT_MIN_INTERVAL_MS), 1_200, "TELEGRAM_EDIT_MIN_INTERVAL_MS");
12
13
  const mirrorMode = parseMirrorMode(optionalString(process.env.NORDRELAY_CLI_MIRROR_MODE), "status");
@@ -25,7 +26,7 @@ export function loadConfig() {
25
26
  const telegramWebhookPort = parsePositiveIntegerEnv(optionalString(process.env.TELEGRAM_WEBHOOK_PORT), 8080, "TELEGRAM_WEBHOOK_PORT");
26
27
  const telegramWebhookPath = parseWebhookPath(optionalString(process.env.TELEGRAM_WEBHOOK_PATH));
27
28
  const telegramWebhookSecret = optionalString(process.env.TELEGRAM_WEBHOOK_SECRET);
28
- const discordEnabled = parseBooleanEnv(optionalString(process.env.DISCORD_ENABLED), false);
29
+ const requestedDiscordEnabled = parseBooleanEnv(optionalString(process.env.DISCORD_ENABLED), false);
29
30
  const discordBotToken = optionalString(process.env.DISCORD_BOT_TOKEN);
30
31
  const discordClientId = optionalString(process.env.DISCORD_CLIENT_ID);
31
32
  const discordGuildIds = parseOptionalStringList(optionalString(process.env.DISCORD_GUILD_IDS));
@@ -38,6 +39,20 @@ export function loadConfig() {
38
39
  const discordMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.DISCORD_CLI_MIRROR_MIN_UPDATE_MS), mirrorMinUpdateMs, "DISCORD_CLI_MIRROR_MIN_UPDATE_MS");
39
40
  const discordNotifyMode = parseNotifyMode(optionalString(process.env.DISCORD_NOTIFY_MODE), notifyMode);
40
41
  const discordQuietHours = parseQuietHoursOverride(process.env.DISCORD_QUIET_HOURS, quietHours);
42
+ const requestedSlackEnabled = parseBooleanEnv(optionalString(process.env.SLACK_ENABLED), false);
43
+ const slackBotToken = optionalString(process.env.SLACK_BOT_TOKEN);
44
+ const slackAppToken = optionalString(process.env.SLACK_APP_TOKEN);
45
+ const slackSigningSecret = optionalString(process.env.SLACK_SIGNING_SECRET);
46
+ const slackSocketMode = parseBooleanEnv(optionalString(process.env.SLACK_SOCKET_MODE), true);
47
+ const slackPort = parsePositiveIntegerEnv(optionalString(process.env.SLACK_PORT), 3000, "SLACK_PORT");
48
+ const slackAllowedTeamIds = parseOptionalStringList(optionalString(process.env.SLACK_ALLOWED_TEAM_IDS));
49
+ const slackAllowedChannelIds = parseOptionalStringList(optionalString(process.env.SLACK_ALLOWED_CHANNEL_IDS));
50
+ const slackMessageContentEnabled = parseBooleanEnv(optionalString(process.env.SLACK_MESSAGE_CONTENT_ENABLED), true);
51
+ const slackCommand = parseSlackCommand(optionalString(process.env.SLACK_COMMAND));
52
+ const slackMirrorMode = parseMirrorMode(optionalString(process.env.SLACK_CLI_MIRROR_MODE), mirrorMode);
53
+ const slackMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.SLACK_CLI_MIRROR_MIN_UPDATE_MS), mirrorMinUpdateMs, "SLACK_CLI_MIRROR_MIN_UPDATE_MS");
54
+ const slackNotifyMode = parseNotifyMode(optionalString(process.env.SLACK_NOTIFY_MODE), notifyMode);
55
+ const slackQuietHours = parseQuietHoursOverride(process.env.SLACK_QUIET_HOURS, quietHours);
41
56
  const workspace = resolveWorkspace();
42
57
  const workspaceAllowedRoots = parsePathList(optionalString(process.env.WORKSPACE_ALLOWED_ROOTS));
43
58
  const workspaceWarnRoots = parsePathList(optionalString(process.env.WORKSPACE_WARN_ROOTS));
@@ -50,6 +65,7 @@ export function loadConfig() {
50
65
  const artifactIgnoreGlobs = parseOptionalStringList(optionalString(process.env.ARTIFACT_IGNORE_GLOBS));
51
66
  const telegramAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.TELEGRAM_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
52
67
  const discordAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.DISCORD_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
68
+ const slackAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.SLACK_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
53
69
  const codexEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_CODEX_ENABLED), true);
54
70
  const codexApiKey = optionalString(process.env.CODEX_API_KEY);
55
71
  const codexModel = optionalString(process.env.CODEX_MODEL);
@@ -108,16 +124,46 @@ export function loadConfig() {
108
124
  const sessionLockTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_SESSION_LOCK_TTL_MS), 30 * 60 * 1000, "NORDRELAY_SESSION_LOCK_TTL_MS");
109
125
  const dashboardCacheTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_DASHBOARD_CACHE_TTL_MS), 10_000, "NORDRELAY_DASHBOARD_CACHE_TTL_MS");
110
126
  const unifiedJobMaxItems = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_UNIFIED_JOB_MAX_ITEMS), 1000, "NORDRELAY_UNIFIED_JOB_MAX_ITEMS");
127
+ const peerEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_ENABLED), false);
128
+ const peerName = optionalString(process.env.NORDRELAY_PEER_NAME);
129
+ const peerHost = optionalString(process.env.NORDRELAY_PEER_HOST) ?? "127.0.0.1";
130
+ const peerPort = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_PEER_PORT), 31979, "NORDRELAY_PEER_PORT");
131
+ const peerPublicUrl = optionalString(process.env.NORDRELAY_PEER_PUBLIC_URL);
132
+ const peerTlsEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_TLS_ENABLED), true);
133
+ const peerRequireTls = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_REQUIRE_TLS), true);
134
+ let telegramEnabled = requestedTelegramEnabled;
111
135
  if (telegramEnabled && telegramTransport === "webhook" && !telegramWebhookUrl) {
112
- throw new Error("TELEGRAM_TRANSPORT=webhook requires TELEGRAM_WEBHOOK_URL");
136
+ telegramEnabled = false;
137
+ adapterWarnings.push("Telegram disabled: TELEGRAM_TRANSPORT=webhook requires TELEGRAM_WEBHOOK_URL.");
113
138
  }
139
+ if (telegramEnabled && !telegramBotToken) {
140
+ telegramEnabled = false;
141
+ adapterWarnings.push("Telegram disabled: TELEGRAM_BOT_TOKEN is missing.");
142
+ }
143
+ let discordEnabled = requestedDiscordEnabled;
114
144
  if (discordEnabled && !discordBotToken) {
115
- throw new Error("DISCORD_ENABLED=true requires DISCORD_BOT_TOKEN");
145
+ discordEnabled = false;
146
+ adapterWarnings.push("Discord disabled: DISCORD_ENABLED=true requires DISCORD_BOT_TOKEN.");
147
+ }
148
+ let slackEnabled = requestedSlackEnabled;
149
+ if (slackEnabled && !slackBotToken) {
150
+ slackEnabled = false;
151
+ adapterWarnings.push("Slack disabled: SLACK_ENABLED=true requires SLACK_BOT_TOKEN.");
152
+ }
153
+ if (slackEnabled && slackSocketMode && !slackAppToken) {
154
+ slackEnabled = false;
155
+ adapterWarnings.push("Slack disabled: SLACK_SOCKET_MODE=true requires SLACK_APP_TOKEN.");
116
156
  }
117
- if (!telegramEnabled && !discordEnabled) {
118
- throw new Error("At least one chat adapter must be enabled.");
157
+ if (slackEnabled && !slackSocketMode && !slackSigningSecret) {
158
+ slackEnabled = false;
159
+ adapterWarnings.push("Slack disabled: SLACK_SOCKET_MODE=false requires SLACK_SIGNING_SECRET.");
160
+ }
161
+ if (!telegramEnabled && !discordEnabled && !slackEnabled) {
162
+ const detail = adapterWarnings.length > 0 ? ` ${adapterWarnings.join(" ")}` : "";
163
+ throw new Error(`At least one usable chat adapter must be enabled.${detail}`);
119
164
  }
120
165
  return {
166
+ adapterWarnings,
121
167
  telegramEnabled,
122
168
  telegramBotToken,
123
169
  telegramRateLimitMinIntervalMs,
@@ -152,6 +198,21 @@ export function loadConfig() {
152
198
  discordNotifyMode,
153
199
  discordQuietHours,
154
200
  discordAutoSendArtifacts,
201
+ slackEnabled,
202
+ slackBotToken,
203
+ slackAppToken,
204
+ slackSigningSecret,
205
+ slackSocketMode,
206
+ slackPort,
207
+ slackAllowedTeamIds,
208
+ slackAllowedChannelIds,
209
+ slackMessageContentEnabled,
210
+ slackCommand,
211
+ slackMirrorMode,
212
+ slackMirrorMinUpdateMs,
213
+ slackNotifyMode,
214
+ slackQuietHours,
215
+ slackAutoSendArtifacts,
155
216
  workspace,
156
217
  workspaceAllowedRoots,
157
218
  workspaceWarnRoots,
@@ -220,6 +281,13 @@ export function loadConfig() {
220
281
  sessionLockTtlMs,
221
282
  dashboardCacheTtlMs,
222
283
  unifiedJobMaxItems,
284
+ peerEnabled,
285
+ peerName,
286
+ peerHost,
287
+ peerPort,
288
+ peerPublicUrl,
289
+ peerTlsEnabled,
290
+ peerRequireTls,
223
291
  };
224
292
  }
225
293
  /**
@@ -412,6 +480,10 @@ function parseDiscordCommandMode(raw) {
412
480
  console.warn(`Invalid DISCORD_COMMAND_MODE value: "${raw}". Expected slash, message, or both. Falling back to both.`);
413
481
  return "both";
414
482
  }
483
+ function parseSlackCommand(raw) {
484
+ const normalized = raw?.trim() || "/nordrelay";
485
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
486
+ }
415
487
  function parseWebhookPath(raw) {
416
488
  if (!raw) {
417
489
  return "/telegram/webhook";
@@ -1,3 +1,4 @@
1
+ import { parsePeerRuntimeContextKey } from "./peer-context.js";
1
2
  export function telegramContextKeyFromMessage(chatId, messageThreadId) {
2
3
  if (messageThreadId !== undefined) {
3
4
  return `${chatId}:${messageThreadId}`;
@@ -76,6 +77,28 @@ export function parseDiscordContextKey(key) {
76
77
  threadId,
77
78
  };
78
79
  }
80
+ export function slackContextKey(input) {
81
+ const teamId = input.teamId || "team";
82
+ const thread = input.threadTs && input.threadTs !== input.channelId ? `:${input.threadTs}` : "";
83
+ return `slack:${teamId}:${input.channelId}${thread}`;
84
+ }
85
+ export function isSlackContextKey(key) {
86
+ return /^slack:[^:]+:[^:]+(?::[^:]+)?$/.test(key);
87
+ }
88
+ export function parseSlackContextKey(key) {
89
+ if (!isSlackContextKey(key)) {
90
+ return null;
91
+ }
92
+ const [, team, channelId, threadTs] = key.split(":");
93
+ if (!channelId) {
94
+ return null;
95
+ }
96
+ return {
97
+ teamId: team === "team" ? undefined : team,
98
+ channelId,
99
+ threadTs,
100
+ };
101
+ }
79
102
  export function parseChannelContextKey(key) {
80
103
  const rawKey = String(key);
81
104
  if (isTelegramContextKey(rawKey)) {
@@ -97,6 +120,16 @@ export function parseChannelContextKey(key) {
97
120
  guildId: discord.guildId,
98
121
  };
99
122
  }
123
+ const slack = parseSlackContextKey(rawKey);
124
+ if (slack) {
125
+ return {
126
+ channelId: "slack",
127
+ contextKey: rawKey,
128
+ chatId: slack.channelId,
129
+ topicId: slack.threadTs,
130
+ guildId: slack.teamId,
131
+ };
132
+ }
100
133
  if (rawKey.startsWith("web:")) {
101
134
  return {
102
135
  channelId: "web",
@@ -111,6 +144,15 @@ export function parseChannelContextKey(key) {
111
144
  chatId: rawKey.slice("cli:".length) || "local",
112
145
  };
113
146
  }
147
+ const peer = parsePeerRuntimeContextKey(rawKey);
148
+ if (peer) {
149
+ return {
150
+ channelId: "peer",
151
+ contextKey: rawKey,
152
+ chatId: peer.peerId,
153
+ topicId: peer.sourceContextKey,
154
+ };
155
+ }
114
156
  return null;
115
157
  }
116
158
  export function channelIdForContextKey(key) {