@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordbyte/nordrelay",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Remote control plane for coding agents across messaging channels.",
5
5
  "type": "module",
6
6
  "author": "Ricardo",
@@ -64,6 +64,8 @@
64
64
  "@anthropic-ai/claude-agent-sdk": "^0.2.141",
65
65
  "@grammyjs/auto-retry": "^2.0.2",
66
66
  "@openai/codex-sdk": "^0.130.0",
67
+ "@slack/bolt": "^4.7.2",
68
+ "@slack/web-api": "^7.16.0",
67
69
  "discord.js": "^14.26.4",
68
70
  "grammy": "^1.41.1",
69
71
  "selfsigned": "^3.0.1",
@@ -76,10 +76,16 @@ function parseArgs(argv) {
76
76
  else if (arg === "--enable-discord") options.enableDiscord = true;
77
77
  else if (arg === "--discord-token") options.discordBotToken = requireValue(copy, ++i, arg);
78
78
  else if (arg === "--discord-client-id") options.discordClientId = requireValue(copy, ++i, arg);
79
+ else if (arg === "--enable-slack") options.enableSlack = true;
80
+ else if (arg === "--slack-bot-token") options.slackBotToken = requireValue(copy, ++i, arg);
81
+ else if (arg === "--slack-app-token") options.slackAppToken = requireValue(copy, ++i, arg);
82
+ else if (arg === "--slack-signing-secret") options.slackSigningSecret = requireValue(copy, ++i, arg);
79
83
  else if (arg === "--admin-email") options.adminEmail = requireValue(copy, ++i, arg);
80
84
  else if (arg === "--admin-name") options.adminName = requireValue(copy, ++i, arg);
81
85
  else if (arg === "--admin-password") options.adminPassword = requireValue(copy, ++i, arg);
82
86
  else if (arg === "--telegram-user-id") options.telegramUserId = requireValue(copy, ++i, arg);
87
+ else if (arg === "--slack-user-id") options.slackUserId = requireValue(copy, ++i, arg);
88
+ else if (arg === "--slack-team-id") options.slackTeamId = requireValue(copy, ++i, arg);
83
89
  else if (arg === "--state-backend") options.stateBackend = requireValue(copy, ++i, arg);
84
90
  else if (arg === "--enable-pi") options.enablePi = true;
85
91
  else if (arg === "--enable-hermes") options.enableHermes = true;
@@ -684,10 +690,22 @@ async function commandInit(options) {
684
690
  const discordClientId = enableDiscord === "true"
685
691
  ? options.discordClientId || process.env.DISCORD_CLIENT_ID || await ask(null, "Discord client ID", "")
686
692
  : "";
693
+ const enableSlack = options.enableSlack ? "true" : await askChoice(null, "Enable Slack", "false");
694
+ const slackBotToken = enableSlack === "true"
695
+ ? options.slackBotToken || process.env.SLACK_BOT_TOKEN || await ask(null, "Slack bot token", "")
696
+ : "";
697
+ const slackAppToken = enableSlack === "true"
698
+ ? options.slackAppToken || process.env.SLACK_APP_TOKEN || await ask(null, "Slack app-level token for Socket Mode", "")
699
+ : "";
700
+ const slackSigningSecret = enableSlack === "true"
701
+ ? options.slackSigningSecret || process.env.SLACK_SIGNING_SECRET || await ask(null, "Slack signing secret (optional for Socket Mode)", "")
702
+ : "";
687
703
  const adminEmail = options.adminEmail || await ask(null, "Admin email", "");
688
704
  const adminName = options.adminName || await ask(null, "Admin name", "Admin");
689
705
  const adminPassword = options.adminPassword || await askSecret(null, "Admin password", "");
690
706
  const telegramUserId = options.telegramUserId || await ask(null, "Optional Telegram user id to link", "");
707
+ const slackUserId = options.slackUserId || await ask(null, "Optional Slack user id to link", "");
708
+ const slackTeamId = slackUserId ? (options.slackTeamId || await ask(null, "Optional Slack team id for linked user", "")) : "";
691
709
  const enableCodex = options.disableCodex ? "false" : await askChoice(null, "Enable Codex", "true");
692
710
  const enablePi = options.enablePi ? "true" : await askChoice(null, "Enable Pi", "false");
693
711
  const enableHermes = options.enableHermes ? "true" : await askChoice(null, "Enable Hermes", "false");
@@ -697,7 +715,9 @@ async function commandInit(options) {
697
715
 
698
716
  if (enableTelegram === "true" && !telegramBotToken) throw new Error("Telegram bot token is required when Telegram is enabled.");
699
717
  if (enableDiscord === "true" && !discordBotToken) throw new Error("Discord bot token is required when Discord is enabled.");
700
- if (enableTelegram !== "true" && enableDiscord !== "true") throw new Error("At least one chat adapter must be enabled.");
718
+ if (enableSlack === "true" && !slackBotToken) throw new Error("Slack bot token is required when Slack is enabled.");
719
+ if (enableSlack === "true" && !slackAppToken) throw new Error("Slack app-level token is required for default Socket Mode.");
720
+ if (enableTelegram !== "true" && enableDiscord !== "true" && enableSlack !== "true") throw new Error("At least one chat adapter must be enabled.");
701
721
  if (!adminEmail) throw new Error("Admin email is required.");
702
722
  if (!adminPassword) throw new Error("Admin password is required.");
703
723
  if (enableCodex !== "true" && enablePi !== "true" && enableHermes !== "true" && enableOpenClaw !== "true" && enableClaudeCode !== "true") throw new Error("At least one agent must be enabled.");
@@ -722,6 +742,13 @@ async function commandInit(options) {
722
742
  "DISCORD_COMMAND_MODE=both",
723
743
  "DISCORD_MESSAGE_CONTENT_ENABLED=true",
724
744
  "DISCORD_AUTO_REGISTER_COMMANDS=true",
745
+ `SLACK_ENABLED=${enableSlack}`,
746
+ `SLACK_BOT_TOKEN=${slackBotToken}`,
747
+ `SLACK_APP_TOKEN=${slackAppToken}`,
748
+ `SLACK_SIGNING_SECRET=${slackSigningSecret}`,
749
+ "SLACK_SOCKET_MODE=true",
750
+ "SLACK_MESSAGE_CONTENT_ENABLED=true",
751
+ "SLACK_AUTO_SEND_ARTIFACTS=false",
725
752
  `NORDRELAY_CODEX_ENABLED=${enableCodex}`,
726
753
  `NORDRELAY_PI_ENABLED=${enablePi}`,
727
754
  `NORDRELAY_HERMES_ENABLED=${enableHermes}`,
@@ -754,6 +781,8 @@ async function commandInit(options) {
754
781
  displayName: adminName || adminEmail,
755
782
  password: adminPassword,
756
783
  telegramUserId: telegramUserId ? Number(telegramUserId) : undefined,
784
+ slackUserId: slackUserId || undefined,
785
+ slackTeamId: slackTeamId || undefined,
757
786
  });
758
787
  console.log(`Wrote ${envPath}`);
759
788
  console.log(`Created admin user ${adminEmail}.`);
@@ -789,7 +818,7 @@ function parsePeerFlags(argv) {
789
818
  const copy = [...argv];
790
819
  const subcommand = copy[0] && !copy[0].startsWith("-") ? copy.shift() : "list";
791
820
  const flags = { subcommand, url: undefined };
792
- if (["add", "test", "revoke"].includes(subcommand) && copy[0] && !copy[0].startsWith("-")) {
821
+ if (["add", "test", "check", "revoke"].includes(subcommand) && copy[0] && !copy[0].startsWith("-")) {
793
822
  flags.url = copy.shift();
794
823
  flags.id = flags.url;
795
824
  }
@@ -797,6 +826,7 @@ function parsePeerFlags(argv) {
797
826
  const arg = copy[i];
798
827
  if (arg === "--name") flags.name = requireValue(copy, ++i, arg);
799
828
  else if (arg === "--code") flags.code = requireValue(copy, ++i, arg);
829
+ else if (arg === "--expect-fingerprint") flags.expectFingerprint = requireValue(copy, ++i, arg);
800
830
  else if (arg === "--public-url") flags.publicUrl = requireValue(copy, ++i, arg);
801
831
  else if (arg === "--expires" || arg === "--expires-minutes") flags.expiresMinutes = Number.parseInt(requireValue(copy, ++i, arg), 10);
802
832
  else if (arg === "--scopes") flags.scopes = requireValue(copy, ++i, arg);
@@ -858,6 +888,15 @@ async function commandPeer(options) {
858
888
 
859
889
  if (flags.subcommand === "invite") {
860
890
  const url = process.env.NORDRELAY_PEER_PUBLIC_URL || `${process.env.NORDRELAY_PEER_TLS_ENABLED === "false" ? "http" : "https"}://${process.env.NORDRELAY_PEER_HOST || "127.0.0.1"}:${process.env.NORDRELAY_PEER_PORT || "31979"}`;
891
+ const peerEnabled = process.env.NORDRELAY_PEER_ENABLED === "true";
892
+ if (!peerEnabled) {
893
+ console.log("Warning: peer server is disabled. The invite can be created, but pairing will fail until NORDRELAY_PEER_ENABLED=true and NordRelay is restarted.");
894
+ } else {
895
+ const probe = await clientMod.checkPeerEndpoint(url, { timeoutMs: 2500 });
896
+ if (!probe.ok) {
897
+ console.log(`Warning: peer endpoint is not reachable from this machine: ${probe.detail}`);
898
+ }
899
+ }
861
900
  const created = store.createInvitation({
862
901
  name: flags.name,
863
902
  expiresInMs: Number.isFinite(flags.expiresMinutes) ? flags.expiresMinutes * 60 * 1000 : undefined,
@@ -896,13 +935,26 @@ async function commandPeer(options) {
896
935
  return;
897
936
  }
898
937
 
938
+ if (flags.subcommand === "check") {
939
+ const url = flags.url || await ask(null, "Peer URL", "");
940
+ const probe = await clientMod.checkPeerEndpoint(url, { expectedTlsFingerprint: flags.expectFingerprint });
941
+ console.log(`Peer endpoint: ${probe.url}`);
942
+ console.log(`Status: ${probe.ok ? "reachable" : "unreachable"}`);
943
+ if (probe.latencyMs !== undefined) console.log(`Latency: ${probe.latencyMs}ms`);
944
+ if (probe.statusCode !== undefined) console.log(`HTTP status: ${probe.statusCode}`);
945
+ if (probe.tlsFingerprint) console.log(`TLS fingerprint: ${probe.tlsFingerprint}`);
946
+ console.log(`Detail: ${probe.detail}`);
947
+ if (!probe.ok) process.exitCode = 1;
948
+ return;
949
+ }
950
+
899
951
  if (flags.subcommand === "revoke") {
900
952
  const id = flags.id || await ask(null, "Peer id", "");
901
953
  console.log(store.revokePeer(id) ? `Revoked peer ${id}.` : `Peer not found: ${id}`);
902
954
  return;
903
955
  }
904
956
 
905
- throw new Error("Usage: nordrelay peer [identity|list|invite|add|test|revoke]");
957
+ throw new Error("Usage: nordrelay peer [identity|list|invite|add|test|check|revoke]");
906
958
  }
907
959
 
908
960
  function parseUserFlags(argv) {
@@ -917,6 +969,8 @@ function parseUserFlags(argv) {
917
969
  else if (arg === "--group" || arg === "--groups") flags.groups = requireValue(copy, ++i, arg);
918
970
  else if (arg === "--telegram-user-id") flags.telegramUserId = Number.parseInt(requireValue(copy, ++i, arg), 10);
919
971
  else if (arg === "--discord-user-id") flags.discordUserId = requireValue(copy, ++i, arg);
972
+ else if (arg === "--slack-user-id") flags.slackUserId = requireValue(copy, ++i, arg);
973
+ else if (arg === "--slack-team-id") flags.slackTeamId = requireValue(copy, ++i, arg);
920
974
  else if (arg === "--user-id") flags.userId = requireValue(copy, ++i, arg);
921
975
  }
922
976
  return flags;
@@ -948,8 +1002,8 @@ async function commandUser(options) {
948
1002
  ? ["admin"]
949
1003
  : (flags.groups ? flags.groups.split(",").map((item) => item.trim()).filter(Boolean) : ["user"]);
950
1004
  const created = flags.subcommand === "create-admin"
951
- ? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId })
952
- : store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId });
1005
+ ? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId, slackUserId: flags.slackUserId, slackTeamId: flags.slackTeamId })
1006
+ : store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId, slackUserId: flags.slackUserId, slackTeamId: flags.slackTeamId });
953
1007
  console.log(`Created user ${created.user.email} (${created.groups.map((group) => group.name).join(", ")}).`);
954
1008
  return;
955
1009
  }
@@ -984,6 +1038,17 @@ async function commandUser(options) {
984
1038
  return;
985
1039
  }
986
1040
 
1041
+ if (flags.subcommand === "link-slack") {
1042
+ const email = flags.email || await ask(null, "Email", "");
1043
+ const slackUserId = flags.slackUserId || await ask(null, "Slack user id", "");
1044
+ const teamId = flags.slackTeamId || await ask(null, "Slack team id (optional)", "");
1045
+ const user = store.getUserByEmail(email);
1046
+ if (!user) throw new Error(`User not found: ${email}`);
1047
+ store.linkSlackUser(user.user.id, { slackUserId, teamId: teamId || undefined });
1048
+ console.log(`Linked Slack user ${slackUserId} to ${user.user.email}.`);
1049
+ return;
1050
+ }
1051
+
987
1052
  if (flags.subcommand === "link-code" || flags.subcommand === "telegram-link-code") {
988
1053
  const email = flags.email || await ask(null, "Email", "");
989
1054
  const user = store.getUserByEmail(email);
@@ -1004,7 +1069,17 @@ async function commandUser(options) {
1004
1069
  return;
1005
1070
  }
1006
1071
 
1007
- throw new Error("Usage: nordrelay user [list|create-admin|create|reset-password|link-telegram|link-discord|link-code|telegram-link-code|discord-link-code]");
1072
+ if (flags.subcommand === "slack-link-code") {
1073
+ const email = flags.email || await ask(null, "Email", "");
1074
+ const user = store.getUserByEmail(email);
1075
+ if (!user) throw new Error(`User not found: ${email}`);
1076
+ const code = store.createSlackLinkCode(user.user.id);
1077
+ console.log(`Slack link code for ${user.user.email}: ${code.code}`);
1078
+ console.log(`Expires: ${code.expiresAt}`);
1079
+ return;
1080
+ }
1081
+
1082
+ throw new Error("Usage: nordrelay user [list|create-admin|create|reset-password|link-telegram|link-discord|link-slack|link-code|telegram-link-code|discord-link-code|slack-link-code]");
1008
1083
  }
1009
1084
 
1010
1085
  async function commandDoctor(options) {
@@ -1016,24 +1091,39 @@ async function commandDoctor(options) {
1016
1091
  checks.push(check("Node.js >= 22", Number.parseInt(process.versions.node.split(".")[0], 10) >= 22, process.version));
1017
1092
  const telegramRequested = process.env.TELEGRAM_ENABLED !== "false";
1018
1093
  const discordRequested = process.env.DISCORD_ENABLED === "true";
1094
+ const slackRequested = process.env.SLACK_ENABLED === "true";
1095
+ const slackSocketMode = process.env.SLACK_SOCKET_MODE !== "false";
1019
1096
  const telegramUsable = telegramRequested && Boolean(process.env.TELEGRAM_BOT_TOKEN);
1020
1097
  const discordUsable = discordRequested && Boolean(process.env.DISCORD_BOT_TOKEN);
1098
+ const slackUsable = slackRequested && Boolean(process.env.SLACK_BOT_TOKEN) && (slackSocketMode ? Boolean(process.env.SLACK_APP_TOKEN) : Boolean(process.env.SLACK_SIGNING_SECRET));
1021
1099
  checks.push(check(
1022
1100
  "Telegram bot token",
1023
1101
  !telegramRequested || telegramUsable,
1024
1102
  telegramRequested ? (telegramUsable ? "configured" : "missing; Telegram adapter will be disabled") : "disabled",
1025
- telegramRequested && !discordUsable ? "fail" : "warn",
1103
+ telegramRequested && !discordUsable && !slackUsable ? "fail" : "warn",
1026
1104
  ));
1027
1105
  checks.push(check(
1028
1106
  "Discord bot token",
1029
1107
  !discordRequested || discordUsable,
1030
1108
  discordRequested ? (discordUsable ? "configured" : "missing; Discord adapter will be disabled") : "disabled",
1031
- discordRequested && !telegramUsable ? "fail" : "warn",
1109
+ discordRequested && !telegramUsable && !slackUsable ? "fail" : "warn",
1110
+ ));
1111
+ checks.push(check(
1112
+ "Slack bot token",
1113
+ !slackRequested || Boolean(process.env.SLACK_BOT_TOKEN),
1114
+ slackRequested ? (process.env.SLACK_BOT_TOKEN ? "configured" : "missing; Slack adapter will be disabled") : "disabled",
1115
+ slackRequested && !telegramUsable && !discordUsable ? "fail" : "warn",
1116
+ ));
1117
+ checks.push(check(
1118
+ slackSocketMode ? "Slack app token" : "Slack signing secret",
1119
+ !slackRequested || slackUsable,
1120
+ slackRequested ? (slackUsable ? "configured" : `missing; ${slackSocketMode ? "Socket Mode requires SLACK_APP_TOKEN" : "HTTP mode requires SLACK_SIGNING_SECRET"}`) : "disabled",
1121
+ slackRequested && !telegramUsable && !discordUsable ? "fail" : "warn",
1032
1122
  ));
1033
1123
  checks.push(check(
1034
1124
  "Usable chat adapter",
1035
- telegramUsable || discordUsable,
1036
- telegramUsable && discordUsable ? "Telegram and Discord" : telegramUsable ? "Telegram" : discordUsable ? "Discord" : "none",
1125
+ telegramUsable || discordUsable || slackUsable,
1126
+ [telegramUsable ? "Telegram" : "", discordUsable ? "Discord" : "", slackUsable ? "Slack" : ""].filter(Boolean).join(" and ") || "none",
1037
1127
  "fail",
1038
1128
  ));
1039
1129
  checks.push(check("Discord client ID", !discordUsable || Boolean(process.env.DISCORD_CLIENT_ID), discordUsable ? (process.env.DISCORD_CLIENT_ID ? "configured" : "missing; slash command auto-registration disabled") : "disabled", "warn"));
@@ -1042,6 +1132,7 @@ async function commandDoctor(options) {
1042
1132
  checks.push(check("WebUI login", true, "required for every dashboard request"));
1043
1133
  checks.push(check("Telegram access", true, "requires linked active users and enabled group chats"));
1044
1134
  checks.push(check("Discord access", true, "requires linked active users and enabled channels"));
1135
+ checks.push(check("Slack access", true, "requires linked active users and enabled channels"));
1045
1136
  const peerEnabled = process.env.NORDRELAY_PEER_ENABLED === "true";
1046
1137
  const peerTlsEnabled = process.env.NORDRELAY_PEER_TLS_ENABLED !== "false";
1047
1138
  const peerHost = process.env.NORDRELAY_PEER_HOST || "127.0.0.1";