@nordbyte/nordrelay 0.7.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 (46) hide show
  1. package/.env.example +35 -0
  2. package/README.md +109 -49
  3. package/dist/activity-events.js +2 -2
  4. package/dist/adapter-conformance.js +61 -0
  5. package/dist/bot.js +18 -31
  6. package/dist/channel-adapter.js +33 -6
  7. package/dist/channel-command-catalog.js +6 -0
  8. package/dist/channel-command-core.js +60 -0
  9. package/dist/channel-command-service.js +20 -4
  10. package/dist/channel-mirror-registry.js +9 -2
  11. package/dist/channel-prompt-engine.js +177 -0
  12. package/dist/channel-turn-lifecycle.js +73 -0
  13. package/dist/config-metadata.js +67 -8
  14. package/dist/config.js +48 -1
  15. package/dist/context-key.js +32 -0
  16. package/dist/discord-bot.js +99 -327
  17. package/dist/index.js +9 -0
  18. package/dist/metrics.js +2 -0
  19. package/dist/peer-client.js +33 -1
  20. package/dist/peer-readiness.js +77 -0
  21. package/dist/peer-runtime-service.js +22 -0
  22. package/dist/peer-store.js +13 -0
  23. package/dist/relay-runtime-helpers.js +3 -1
  24. package/dist/relay-runtime.js +7 -0
  25. package/dist/settings-wizard-test.js +216 -0
  26. package/dist/slack-artifacts.js +165 -0
  27. package/dist/slack-bot.js +1461 -0
  28. package/dist/slack-channel-runtime.js +147 -0
  29. package/dist/slack-command-surface.js +46 -0
  30. package/dist/slack-diagnostics.js +116 -0
  31. package/dist/slack-rate-limit.js +139 -0
  32. package/dist/user-management-crypto.js +38 -0
  33. package/dist/user-management-normalize.js +188 -0
  34. package/dist/user-management-types.js +1 -0
  35. package/dist/user-management.js +193 -196
  36. package/dist/web-api-contract.js +8 -0
  37. package/dist/web-dashboard-access-routes.js +62 -0
  38. package/dist/web-dashboard-assets.js +1 -0
  39. package/dist/web-dashboard-pages.js +14 -4
  40. package/dist/web-dashboard-peer-routes.js +32 -11
  41. package/dist/web-dashboard.js +34 -0
  42. package/dist/web-state.js +2 -2
  43. package/dist/webui-assets/dashboard.css +193 -0
  44. package/dist/webui-assets/dashboard.js +544 -144
  45. package/package.json +3 -1
  46. package/plugins/nordrelay/scripts/nordrelay.mjs +101 -10
@@ -1,6 +1,9 @@
1
1
  export const SECRET_KEYS = new Set([
2
2
  "TELEGRAM_BOT_TOKEN",
3
3
  "DISCORD_BOT_TOKEN",
4
+ "SLACK_BOT_TOKEN",
5
+ "SLACK_APP_TOKEN",
6
+ "SLACK_SIGNING_SECRET",
4
7
  "CODEX_API_KEY",
5
8
  "HERMES_API_KEY",
6
9
  "OPENCLAW_GATEWAY_TOKEN",
@@ -24,15 +27,34 @@ const DISCORD_SETTING_HELP = {
24
27
  DISCORD_QUIET_HOURS: "Use a local-time range like 22-7, off, or blank to inherit the channel-neutral quiet-hours setting.",
25
28
  DISCORD_AUTO_SEND_ARTIFACTS: "Overrides automatic artifact upload behavior for Discord only. Leave blank to use NORDRELAY_AUTO_SEND_ARTIFACTS.",
26
29
  };
30
+ const SLACK_SETTING_HELP = {
31
+ SLACK_ENABLED: "Create a Slack app, install it into the workspace, then set bot/app tokens before enabling Slack.",
32
+ SLACK_BOT_TOKEN: "Slack app OAuth & Permissions: copy the bot token that starts with xoxb-.",
33
+ SLACK_APP_TOKEN: "Slack app Basic Information: create an app-level token with connections:write. Required for Socket Mode.",
34
+ SLACK_SIGNING_SECRET: "Slack app Basic Information: copy Signing Secret. Required only when Socket Mode is disabled.",
35
+ SLACK_ALLOWED_TEAM_IDS: "Optional workspace allow-list. Copy Team IDs from Slack event payloads or app diagnostics.",
36
+ SLACK_ALLOWED_CHANNEL_IDS: "Optional channel allow-list before NordRelay user/group checks. Copy channel IDs from Slack channel details.",
37
+ SLACK_COMMAND: "Slash command configured in the Slack app. Defaults to /nordrelay.",
38
+ };
39
+ const TELEGRAM_SETTING_HELP = {
40
+ TELEGRAM_ENABLED: "Enable this only after the BotFather token is configured and NordRelay users/chats are allowed through the user management system.",
41
+ TELEGRAM_BOT_TOKEN: "Telegram BotFather: open @BotFather, create a bot with /newbot, then paste only the token value.",
42
+ TELEGRAM_TRANSPORT: "Use polling for the simplest setup. Use webhook only when this NordRelay instance is reachable from Telegram through public HTTPS.",
43
+ TELEGRAM_WEBHOOK_URL: "Public HTTPS base URL for Telegram webhook delivery, for example https://relay.example.com.",
44
+ TELEGRAM_WEBHOOK_HOST: "Local interface where NordRelay binds the webhook listener. Use 127.0.0.1 behind a reverse proxy or 0.0.0.0 only when the endpoint is protected.",
45
+ TELEGRAM_WEBHOOK_PORT: "Local port for the Telegram webhook listener.",
46
+ TELEGRAM_WEBHOOK_PATH: "Webhook request path registered with Telegram. It must start with /.",
47
+ TELEGRAM_WEBHOOK_SECRET: "Optional secret token Telegram sends in X-Telegram-Bot-Api-Secret-Token. Use a random value for webhook mode.",
48
+ };
27
49
  export const SETTING_DEFINITIONS = [
28
- setting("TELEGRAM_ENABLED", "Enable Telegram", "Telegram", "boolean", "Start the Telegram bot adapter.", true),
29
- setting("TELEGRAM_BOT_TOKEN", "Telegram bot token", "Telegram", "secret", "BotFather token.", true),
30
- setting("TELEGRAM_TRANSPORT", "Telegram transport", "Telegram", "string", "polling or webhook.", true, ["polling", "webhook"]),
31
- setting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "Telegram", "string", "Public base URL for webhook mode.", true),
32
- setting("TELEGRAM_WEBHOOK_HOST", "Webhook bind host", "Telegram", "string", "Local webhook bind host.", true),
33
- setting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "Telegram", "number", "Local webhook bind port.", true),
34
- setting("TELEGRAM_WEBHOOK_PATH", "Webhook path", "Telegram", "string", "Webhook request path.", true),
35
- setting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "Telegram", "secret", "Optional Telegram webhook secret token.", true),
50
+ telegramSetting("TELEGRAM_ENABLED", "Enable Telegram", "boolean", "Start the Telegram bot adapter.", true),
51
+ telegramSetting("TELEGRAM_BOT_TOKEN", "Telegram bot token", "secret", "BotFather token.", true),
52
+ telegramSetting("TELEGRAM_TRANSPORT", "Telegram transport", "string", "polling or webhook.", true, ["polling", "webhook"]),
53
+ telegramSetting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "string", "Public base URL for webhook mode.", true),
54
+ telegramSetting("TELEGRAM_WEBHOOK_HOST", "Webhook bind host", "string", "Local webhook bind host.", true),
55
+ telegramSetting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "number", "Local webhook bind port.", true),
56
+ telegramSetting("TELEGRAM_WEBHOOK_PATH", "Webhook path", "string", "Webhook request path.", true),
57
+ telegramSetting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "secret", "Optional Telegram webhook secret token.", true),
36
58
  discordSetting("DISCORD_ENABLED", "Enable Discord", "boolean", "Start the Discord bot adapter.", true),
37
59
  discordSetting("DISCORD_BOT_TOKEN", "Discord bot token", "secret", "Discord bot token.", true),
38
60
  discordSetting("DISCORD_CLIENT_ID", "Discord client ID", "string", "Discord application/client id used for slash command registration.", true),
@@ -47,6 +69,21 @@ export const SETTING_DEFINITIONS = [
47
69
  discordSetting("DISCORD_NOTIFY_MODE", "Discord notify override", "string", "Optional Discord override for completion notifications.", false, ["off", "minimal", "all"]),
48
70
  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
71
  discordSetting("DISCORD_AUTO_SEND_ARTIFACTS", "Discord auto-send artifacts override", "boolean", "Optional Discord override for automatic artifact summaries/uploads.", false),
72
+ slackSetting("SLACK_ENABLED", "Enable Slack", "boolean", "Start the Slack bot adapter.", true),
73
+ slackSetting("SLACK_BOT_TOKEN", "Slack bot token", "secret", "Slack bot token.", true),
74
+ slackSetting("SLACK_APP_TOKEN", "Slack app token", "secret", "Slack app-level token for Socket Mode.", true),
75
+ slackSetting("SLACK_SIGNING_SECRET", "Slack signing secret", "secret", "Slack signing secret for HTTP Events mode.", true),
76
+ slackSetting("SLACK_SOCKET_MODE", "Slack Socket Mode", "boolean", "Use Slack Socket Mode instead of an HTTP events receiver.", true),
77
+ slackSetting("SLACK_PORT", "Slack HTTP port", "number", "HTTP port used when Slack Socket Mode is disabled.", true),
78
+ slackSetting("SLACK_ALLOWED_TEAM_IDS", "Allowed Slack teams", "list", "Optional comma-separated Slack team/workspace allow-list.", true),
79
+ slackSetting("SLACK_ALLOWED_CHANNEL_IDS", "Allowed Slack channels", "list", "Optional comma-separated Slack channel allow-list before user/group checks.", true),
80
+ slackSetting("SLACK_MESSAGE_CONTENT_ENABLED", "Slack message content", "boolean", "Read regular Slack text messages as prompts.", true),
81
+ slackSetting("SLACK_COMMAND", "Slack Slash command", "string", "Slash command configured in Slack.", true),
82
+ slackSetting("SLACK_CLI_MIRROR_MODE", "Slack mirror override", "string", "Optional Slack override for CLI mirror mode. Uses the NordRelay default when unset.", false, ["off", "status", "final", "full"]),
83
+ slackSetting("SLACK_CLI_MIRROR_MIN_UPDATE_MS", "Slack mirror update override", "number", "Optional Slack override for mirrored edit interval.", true),
84
+ slackSetting("SLACK_NOTIFY_MODE", "Slack notify override", "string", "Optional Slack override for completion notifications.", false, ["off", "minimal", "all"]),
85
+ slackSetting("SLACK_QUIET_HOURS", "Slack quiet hours override", "string", "Optional Slack quiet hours override. Use HH-HH, off, or leave blank for default.", false),
86
+ slackSetting("SLACK_AUTO_SEND_ARTIFACTS", "Slack auto-send artifacts override", "boolean", "Optional Slack override for automatic artifact summaries/uploads.", false),
50
87
  setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
51
88
  setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
52
89
  setting("NORDRELAY_HERMES_ENABLED", "Enable Hermes", "Agents", "boolean", "Allow Hermes sessions through the Hermes API Server.", true),
@@ -166,6 +203,21 @@ const EXAMPLE_VALUES = {
166
203
  "DISCORD_NOTIFY_MODE": "",
167
204
  "DISCORD_QUIET_HOURS": "",
168
205
  "DISCORD_AUTO_SEND_ARTIFACTS": "",
206
+ "SLACK_ENABLED": "false",
207
+ "SLACK_BOT_TOKEN": "",
208
+ "SLACK_APP_TOKEN": "",
209
+ "SLACK_SIGNING_SECRET": "",
210
+ "SLACK_SOCKET_MODE": "true",
211
+ "SLACK_PORT": "3000",
212
+ "SLACK_ALLOWED_TEAM_IDS": "",
213
+ "SLACK_ALLOWED_CHANNEL_IDS": "",
214
+ "SLACK_MESSAGE_CONTENT_ENABLED": "true",
215
+ "SLACK_COMMAND": "/nordrelay",
216
+ "SLACK_CLI_MIRROR_MODE": "",
217
+ "SLACK_CLI_MIRROR_MIN_UPDATE_MS": "",
218
+ "SLACK_NOTIFY_MODE": "",
219
+ "SLACK_QUIET_HOURS": "",
220
+ "SLACK_AUTO_SEND_ARTIFACTS": "",
169
221
  "NORDRELAY_CODEX_ENABLED": "true",
170
222
  "NORDRELAY_PI_ENABLED": "false",
171
223
  "NORDRELAY_HERMES_ENABLED": "false",
@@ -276,6 +328,7 @@ const EXAMPLE_VALUES = {
276
328
  const GROUP_INTROS = {
277
329
  Telegram: "Telegram bot and transport settings.",
278
330
  Discord: "Discord bot settings. Discord is opt-in and uses the same NordRelay users, groups, and permissions as Telegram.",
331
+ Slack: "Slack bot settings. Slack is opt-in and uses the same NordRelay users, groups, and permissions as Telegram and Discord.",
279
332
  Agents: "Agent access. Codex is enabled by default; Pi, Hermes, OpenClaw, and Claude Code are opt-in.",
280
333
  Codex: "Codex defaults for newly created or reattached sessions.",
281
334
  Pi: "Pi coding agent defaults.",
@@ -318,3 +371,9 @@ function setting(key, label, group, kind, description, restartRequired, options,
318
371
  function discordSetting(key, label, kind, description, restartRequired, options) {
319
372
  return setting(key, label, "Discord", kind, description, restartRequired, options, DISCORD_SETTING_HELP[key]);
320
373
  }
374
+ function telegramSetting(key, label, kind, description, restartRequired, options) {
375
+ return setting(key, label, "Telegram", kind, description, restartRequired, options, TELEGRAM_SETTING_HELP[key]);
376
+ }
377
+ function slackSetting(key, label, kind, description, restartRequired, options) {
378
+ return setting(key, label, "Slack", kind, description, restartRequired, options, SLACK_SETTING_HELP[key]);
379
+ }
package/dist/config.js CHANGED
@@ -39,6 +39,20 @@ export function loadConfig() {
39
39
  const discordMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.DISCORD_CLI_MIRROR_MIN_UPDATE_MS), mirrorMinUpdateMs, "DISCORD_CLI_MIRROR_MIN_UPDATE_MS");
40
40
  const discordNotifyMode = parseNotifyMode(optionalString(process.env.DISCORD_NOTIFY_MODE), notifyMode);
41
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);
42
56
  const workspace = resolveWorkspace();
43
57
  const workspaceAllowedRoots = parsePathList(optionalString(process.env.WORKSPACE_ALLOWED_ROOTS));
44
58
  const workspaceWarnRoots = parsePathList(optionalString(process.env.WORKSPACE_WARN_ROOTS));
@@ -51,6 +65,7 @@ export function loadConfig() {
51
65
  const artifactIgnoreGlobs = parseOptionalStringList(optionalString(process.env.ARTIFACT_IGNORE_GLOBS));
52
66
  const telegramAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.TELEGRAM_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
53
67
  const discordAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.DISCORD_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
68
+ const slackAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.SLACK_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
54
69
  const codexEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_CODEX_ENABLED), true);
55
70
  const codexApiKey = optionalString(process.env.CODEX_API_KEY);
56
71
  const codexModel = optionalString(process.env.CODEX_MODEL);
@@ -130,7 +145,20 @@ export function loadConfig() {
130
145
  discordEnabled = false;
131
146
  adapterWarnings.push("Discord disabled: DISCORD_ENABLED=true requires DISCORD_BOT_TOKEN.");
132
147
  }
133
- if (!telegramEnabled && !discordEnabled) {
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.");
156
+ }
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) {
134
162
  const detail = adapterWarnings.length > 0 ? ` ${adapterWarnings.join(" ")}` : "";
135
163
  throw new Error(`At least one usable chat adapter must be enabled.${detail}`);
136
164
  }
@@ -170,6 +198,21 @@ export function loadConfig() {
170
198
  discordNotifyMode,
171
199
  discordQuietHours,
172
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,
173
216
  workspace,
174
217
  workspaceAllowedRoots,
175
218
  workspaceWarnRoots,
@@ -437,6 +480,10 @@ function parseDiscordCommandMode(raw) {
437
480
  console.warn(`Invalid DISCORD_COMMAND_MODE value: "${raw}". Expected slash, message, or both. Falling back to both.`);
438
481
  return "both";
439
482
  }
483
+ function parseSlackCommand(raw) {
484
+ const normalized = raw?.trim() || "/nordrelay";
485
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
486
+ }
440
487
  function parseWebhookPath(raw) {
441
488
  if (!raw) {
442
489
  return "/telegram/webhook";
@@ -77,6 +77,28 @@ export function parseDiscordContextKey(key) {
77
77
  threadId,
78
78
  };
79
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
+ }
80
102
  export function parseChannelContextKey(key) {
81
103
  const rawKey = String(key);
82
104
  if (isTelegramContextKey(rawKey)) {
@@ -98,6 +120,16 @@ export function parseChannelContextKey(key) {
98
120
  guildId: discord.guildId,
99
121
  };
100
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
+ }
101
133
  if (rawKey.startsWith("web:")) {
102
134
  return {
103
135
  channelId: "web",