@nordbyte/nordrelay 0.7.0 → 0.8.1

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 (47) hide show
  1. package/.env.example +35 -0
  2. package/README.md +118 -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 +90 -2
  20. package/dist/peer-readiness.js +77 -0
  21. package/dist/peer-runtime-service.js +22 -0
  22. package/dist/peer-server.js +20 -4
  23. package/dist/peer-store.js +17 -2
  24. package/dist/relay-runtime-helpers.js +3 -1
  25. package/dist/relay-runtime.js +7 -0
  26. package/dist/settings-wizard-test.js +216 -0
  27. package/dist/slack-artifacts.js +165 -0
  28. package/dist/slack-bot.js +1461 -0
  29. package/dist/slack-channel-runtime.js +147 -0
  30. package/dist/slack-command-surface.js +46 -0
  31. package/dist/slack-diagnostics.js +116 -0
  32. package/dist/slack-rate-limit.js +139 -0
  33. package/dist/user-management-crypto.js +38 -0
  34. package/dist/user-management-normalize.js +188 -0
  35. package/dist/user-management-types.js +1 -0
  36. package/dist/user-management.js +193 -196
  37. package/dist/web-api-contract.js +8 -0
  38. package/dist/web-dashboard-access-routes.js +62 -0
  39. package/dist/web-dashboard-assets.js +1 -0
  40. package/dist/web-dashboard-pages.js +14 -4
  41. package/dist/web-dashboard-peer-routes.js +32 -11
  42. package/dist/web-dashboard.js +34 -0
  43. package/dist/web-state.js +2 -2
  44. package/dist/webui-assets/dashboard.css +193 -0
  45. package/dist/webui-assets/dashboard.js +546 -145
  46. package/package.json +3 -1
  47. package/plugins/nordrelay/scripts/nordrelay.mjs +105 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordbyte/nordrelay",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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,
@@ -876,16 +915,19 @@ async function commandPeer(options) {
876
915
  if (flags.subcommand === "add") {
877
916
  const url = flags.url || await ask(null, "Peer URL", "");
878
917
  const code = flags.code || await ask(null, "Pairing code", "");
918
+ const configuredPublicUrl = process.env.NORDRELAY_PEER_ENABLED === "true" ? process.env.NORDRELAY_PEER_PUBLIC_URL : undefined;
919
+ const publicUrl = flags.publicUrl || configuredPublicUrl;
879
920
  const result = await clientMod.pairPeer({
880
921
  url,
881
922
  code,
882
923
  name: flags.name,
883
- publicUrl: flags.publicUrl,
924
+ publicUrl,
884
925
  }, identity, store);
885
926
  console.log(`Added peer ${result.peer.name} (${result.peer.id}).`);
886
927
  console.log(`Node: ${result.peer.nodeId}`);
887
928
  console.log(`Fingerprint: ${result.peer.fingerprint}`);
888
929
  if (result.tlsFingerprint) console.log(`TLS fingerprint: ${result.tlsFingerprint}`);
930
+ if (publicUrl) console.log(`Shared public URL: ${publicUrl}`);
889
931
  return;
890
932
  }
891
933
 
@@ -896,13 +938,26 @@ async function commandPeer(options) {
896
938
  return;
897
939
  }
898
940
 
941
+ if (flags.subcommand === "check") {
942
+ const url = flags.url || await ask(null, "Peer URL", "");
943
+ const probe = await clientMod.checkPeerEndpoint(url, { expectedTlsFingerprint: flags.expectFingerprint });
944
+ console.log(`Peer endpoint: ${probe.url}`);
945
+ console.log(`Status: ${probe.ok ? "reachable" : "unreachable"}`);
946
+ if (probe.latencyMs !== undefined) console.log(`Latency: ${probe.latencyMs}ms`);
947
+ if (probe.statusCode !== undefined) console.log(`HTTP status: ${probe.statusCode}`);
948
+ if (probe.tlsFingerprint) console.log(`TLS fingerprint: ${probe.tlsFingerprint}`);
949
+ console.log(`Detail: ${probe.detail}`);
950
+ if (!probe.ok) process.exitCode = 1;
951
+ return;
952
+ }
953
+
899
954
  if (flags.subcommand === "revoke") {
900
955
  const id = flags.id || await ask(null, "Peer id", "");
901
956
  console.log(store.revokePeer(id) ? `Revoked peer ${id}.` : `Peer not found: ${id}`);
902
957
  return;
903
958
  }
904
959
 
905
- throw new Error("Usage: nordrelay peer [identity|list|invite|add|test|revoke]");
960
+ throw new Error("Usage: nordrelay peer [identity|list|invite|add|test|check|revoke]");
906
961
  }
907
962
 
908
963
  function parseUserFlags(argv) {
@@ -917,6 +972,8 @@ function parseUserFlags(argv) {
917
972
  else if (arg === "--group" || arg === "--groups") flags.groups = requireValue(copy, ++i, arg);
918
973
  else if (arg === "--telegram-user-id") flags.telegramUserId = Number.parseInt(requireValue(copy, ++i, arg), 10);
919
974
  else if (arg === "--discord-user-id") flags.discordUserId = requireValue(copy, ++i, arg);
975
+ else if (arg === "--slack-user-id") flags.slackUserId = requireValue(copy, ++i, arg);
976
+ else if (arg === "--slack-team-id") flags.slackTeamId = requireValue(copy, ++i, arg);
920
977
  else if (arg === "--user-id") flags.userId = requireValue(copy, ++i, arg);
921
978
  }
922
979
  return flags;
@@ -948,8 +1005,8 @@ async function commandUser(options) {
948
1005
  ? ["admin"]
949
1006
  : (flags.groups ? flags.groups.split(",").map((item) => item.trim()).filter(Boolean) : ["user"]);
950
1007
  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 });
1008
+ ? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId, slackUserId: flags.slackUserId, slackTeamId: flags.slackTeamId })
1009
+ : store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId, slackUserId: flags.slackUserId, slackTeamId: flags.slackTeamId });
953
1010
  console.log(`Created user ${created.user.email} (${created.groups.map((group) => group.name).join(", ")}).`);
954
1011
  return;
955
1012
  }
@@ -984,6 +1041,17 @@ async function commandUser(options) {
984
1041
  return;
985
1042
  }
986
1043
 
1044
+ if (flags.subcommand === "link-slack") {
1045
+ const email = flags.email || await ask(null, "Email", "");
1046
+ const slackUserId = flags.slackUserId || await ask(null, "Slack user id", "");
1047
+ const teamId = flags.slackTeamId || await ask(null, "Slack team id (optional)", "");
1048
+ const user = store.getUserByEmail(email);
1049
+ if (!user) throw new Error(`User not found: ${email}`);
1050
+ store.linkSlackUser(user.user.id, { slackUserId, teamId: teamId || undefined });
1051
+ console.log(`Linked Slack user ${slackUserId} to ${user.user.email}.`);
1052
+ return;
1053
+ }
1054
+
987
1055
  if (flags.subcommand === "link-code" || flags.subcommand === "telegram-link-code") {
988
1056
  const email = flags.email || await ask(null, "Email", "");
989
1057
  const user = store.getUserByEmail(email);
@@ -1004,7 +1072,17 @@ async function commandUser(options) {
1004
1072
  return;
1005
1073
  }
1006
1074
 
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]");
1075
+ if (flags.subcommand === "slack-link-code") {
1076
+ const email = flags.email || await ask(null, "Email", "");
1077
+ const user = store.getUserByEmail(email);
1078
+ if (!user) throw new Error(`User not found: ${email}`);
1079
+ const code = store.createSlackLinkCode(user.user.id);
1080
+ console.log(`Slack link code for ${user.user.email}: ${code.code}`);
1081
+ console.log(`Expires: ${code.expiresAt}`);
1082
+ return;
1083
+ }
1084
+
1085
+ 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
1086
  }
1009
1087
 
1010
1088
  async function commandDoctor(options) {
@@ -1016,24 +1094,39 @@ async function commandDoctor(options) {
1016
1094
  checks.push(check("Node.js >= 22", Number.parseInt(process.versions.node.split(".")[0], 10) >= 22, process.version));
1017
1095
  const telegramRequested = process.env.TELEGRAM_ENABLED !== "false";
1018
1096
  const discordRequested = process.env.DISCORD_ENABLED === "true";
1097
+ const slackRequested = process.env.SLACK_ENABLED === "true";
1098
+ const slackSocketMode = process.env.SLACK_SOCKET_MODE !== "false";
1019
1099
  const telegramUsable = telegramRequested && Boolean(process.env.TELEGRAM_BOT_TOKEN);
1020
1100
  const discordUsable = discordRequested && Boolean(process.env.DISCORD_BOT_TOKEN);
1101
+ const slackUsable = slackRequested && Boolean(process.env.SLACK_BOT_TOKEN) && (slackSocketMode ? Boolean(process.env.SLACK_APP_TOKEN) : Boolean(process.env.SLACK_SIGNING_SECRET));
1021
1102
  checks.push(check(
1022
1103
  "Telegram bot token",
1023
1104
  !telegramRequested || telegramUsable,
1024
1105
  telegramRequested ? (telegramUsable ? "configured" : "missing; Telegram adapter will be disabled") : "disabled",
1025
- telegramRequested && !discordUsable ? "fail" : "warn",
1106
+ telegramRequested && !discordUsable && !slackUsable ? "fail" : "warn",
1026
1107
  ));
1027
1108
  checks.push(check(
1028
1109
  "Discord bot token",
1029
1110
  !discordRequested || discordUsable,
1030
1111
  discordRequested ? (discordUsable ? "configured" : "missing; Discord adapter will be disabled") : "disabled",
1031
- discordRequested && !telegramUsable ? "fail" : "warn",
1112
+ discordRequested && !telegramUsable && !slackUsable ? "fail" : "warn",
1113
+ ));
1114
+ checks.push(check(
1115
+ "Slack bot token",
1116
+ !slackRequested || Boolean(process.env.SLACK_BOT_TOKEN),
1117
+ slackRequested ? (process.env.SLACK_BOT_TOKEN ? "configured" : "missing; Slack adapter will be disabled") : "disabled",
1118
+ slackRequested && !telegramUsable && !discordUsable ? "fail" : "warn",
1119
+ ));
1120
+ checks.push(check(
1121
+ slackSocketMode ? "Slack app token" : "Slack signing secret",
1122
+ !slackRequested || slackUsable,
1123
+ slackRequested ? (slackUsable ? "configured" : `missing; ${slackSocketMode ? "Socket Mode requires SLACK_APP_TOKEN" : "HTTP mode requires SLACK_SIGNING_SECRET"}`) : "disabled",
1124
+ slackRequested && !telegramUsable && !discordUsable ? "fail" : "warn",
1032
1125
  ));
1033
1126
  checks.push(check(
1034
1127
  "Usable chat adapter",
1035
- telegramUsable || discordUsable,
1036
- telegramUsable && discordUsable ? "Telegram and Discord" : telegramUsable ? "Telegram" : discordUsable ? "Discord" : "none",
1128
+ telegramUsable || discordUsable || slackUsable,
1129
+ [telegramUsable ? "Telegram" : "", discordUsable ? "Discord" : "", slackUsable ? "Slack" : ""].filter(Boolean).join(" and ") || "none",
1037
1130
  "fail",
1038
1131
  ));
1039
1132
  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 +1135,7 @@ async function commandDoctor(options) {
1042
1135
  checks.push(check("WebUI login", true, "required for every dashboard request"));
1043
1136
  checks.push(check("Telegram access", true, "requires linked active users and enabled group chats"));
1044
1137
  checks.push(check("Discord access", true, "requires linked active users and enabled channels"));
1138
+ checks.push(check("Slack access", true, "requires linked active users and enabled channels"));
1045
1139
  const peerEnabled = process.env.NORDRELAY_PEER_ENABLED === "true";
1046
1140
  const peerTlsEnabled = process.env.NORDRELAY_PEER_TLS_ENABLED !== "false";
1047
1141
  const peerHost = process.env.NORDRELAY_PEER_HOST || "127.0.0.1";