@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
@@ -0,0 +1,127 @@
1
+ import { createHash, generateKeyPairSync, randomBytes, sign, verify, } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import selfsigned from "selfsigned";
6
+ import { readJsonFileWithBackup, writeJsonFileAtomic, writeTextFileAtomic } from "./persistence.js";
7
+ const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
8
+ export function loadOrCreatePeerIdentity(home = process.env.NORDRELAY_HOME || DEFAULT_HOME, name) {
9
+ const filePath = path.join(home, "identity.json");
10
+ const existing = readJsonFileWithBackup(filePath).value;
11
+ if (existing?.nodeId && existing.publicKey && existing.privateKey && existing.fingerprint) {
12
+ return {
13
+ public: {
14
+ nodeId: existing.nodeId,
15
+ name: existing.name || defaultNodeName(),
16
+ publicKey: existing.publicKey,
17
+ fingerprint: existing.fingerprint,
18
+ createdAt: existing.createdAt,
19
+ },
20
+ privateKey: existing.privateKey,
21
+ };
22
+ }
23
+ const pair = generateKeyPairSync("ed25519", {
24
+ publicKeyEncoding: { type: "spki", format: "pem" },
25
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
26
+ });
27
+ const publicKey = pair.publicKey.toString();
28
+ const createdAt = new Date().toISOString();
29
+ const identity = {
30
+ nodeId: createNodeId(publicKey),
31
+ name: name?.trim() || defaultNodeName(),
32
+ publicKey,
33
+ privateKey: pair.privateKey.toString(),
34
+ fingerprint: fingerprintForPublicKey(publicKey),
35
+ createdAt,
36
+ };
37
+ mkdirSync(path.dirname(filePath), { recursive: true });
38
+ writeJsonFileAtomic(filePath, identity);
39
+ chmodSync(filePath, 0o600);
40
+ return {
41
+ public: {
42
+ nodeId: identity.nodeId,
43
+ name: identity.name,
44
+ publicKey: identity.publicKey,
45
+ fingerprint: identity.fingerprint,
46
+ createdAt: identity.createdAt,
47
+ },
48
+ privateKey: identity.privateKey,
49
+ };
50
+ }
51
+ export function ensurePeerTlsFiles(home = process.env.NORDRELAY_HOME || DEFAULT_HOME, identity) {
52
+ const certDir = path.join(home, "tls");
53
+ const certPath = path.join(certDir, "peer.crt");
54
+ const keyPath = path.join(certDir, "peer.key");
55
+ if (existsSync(certPath) && existsSync(keyPath)) {
56
+ const cert = readFileSync(certPath, "utf8");
57
+ const key = readFileSync(keyPath, "utf8");
58
+ return { certPath, keyPath, cert, key, fingerprint: fingerprintForCertificate(cert) };
59
+ }
60
+ mkdirSync(certDir, { recursive: true });
61
+ const attrs = [{ name: "commonName", value: identity?.nodeId ?? "nordrelay-peer" }];
62
+ const generated = selfsigned.generate(attrs, {
63
+ algorithm: "sha256",
64
+ days: 3650,
65
+ keySize: 2048,
66
+ extensions: [
67
+ { name: "basicConstraints", cA: false },
68
+ { name: "keyUsage", digitalSignature: true, keyEncipherment: true },
69
+ { name: "extKeyUsage", serverAuth: true },
70
+ {
71
+ name: "subjectAltName",
72
+ altNames: [
73
+ { type: 2, value: "localhost" },
74
+ { type: 7, ip: "127.0.0.1" },
75
+ { type: 7, ip: "::1" },
76
+ ],
77
+ },
78
+ ],
79
+ });
80
+ writeTextFileAtomic(certPath, generated.cert);
81
+ writeTextFileAtomic(keyPath, generated.private);
82
+ chmodSync(certPath, 0o600);
83
+ chmodSync(keyPath, 0o600);
84
+ return {
85
+ certPath,
86
+ keyPath,
87
+ cert: generated.cert,
88
+ key: generated.private,
89
+ fingerprint: fingerprintForCertificate(generated.cert),
90
+ };
91
+ }
92
+ export function signPeerPayload(privateKey, payload) {
93
+ return sign(null, Buffer.from(payload, "utf8"), privateKey).toString("base64url");
94
+ }
95
+ export function verifyPeerPayload(publicKey, payload, signature) {
96
+ try {
97
+ return verify(null, Buffer.from(payload, "utf8"), publicKey, Buffer.from(signature, "base64url"));
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ export function createPairingSignaturePayload(nodeId, timestamp, code) {
104
+ return `${nodeId}\n${timestamp}\n${code}`;
105
+ }
106
+ export function createSharedSecret() {
107
+ return randomBytes(32).toString("base64url");
108
+ }
109
+ export function fingerprintForPublicKey(publicKey) {
110
+ return formatFingerprint(createHash("sha256").update(publicKey).digest("hex"));
111
+ }
112
+ export function fingerprintForCertificate(certPem) {
113
+ const body = certPem
114
+ .replace(/-----BEGIN CERTIFICATE-----/g, "")
115
+ .replace(/-----END CERTIFICATE-----/g, "")
116
+ .replace(/\s+/g, "");
117
+ return formatFingerprint(createHash("sha256").update(Buffer.from(body, "base64")).digest("hex"));
118
+ }
119
+ function createNodeId(publicKey) {
120
+ return createHash("sha256").update(publicKey).digest("hex").slice(0, 16);
121
+ }
122
+ function defaultNodeName() {
123
+ return `${os.hostname()} (${process.platform})`;
124
+ }
125
+ function formatFingerprint(hex) {
126
+ return hex.match(/.{1,2}/g)?.join(":") ?? hex;
127
+ }
@@ -0,0 +1,77 @@
1
+ import net from "node:net";
2
+ export async function buildPeerReadiness(config) {
3
+ const listenUrl = peerListenUrl(config);
4
+ const localListening = await checkLocalPort(config.peerHost, config.peerPort);
5
+ const loopbackOnly = isLoopbackUrl(listenUrl);
6
+ const bindLoopbackOnly = isLoopbackHost(config.peerHost);
7
+ const warnings = [];
8
+ if (!config.peerEnabled) {
9
+ warnings.push("Peer server is disabled. Invites can be created, but pairing will fail until NORDRELAY_PEER_ENABLED=true and NordRelay is restarted.");
10
+ }
11
+ if (config.peerEnabled && !localListening) {
12
+ warnings.push(`Peer server is enabled, but no listener was detected on ${connectHostForBindHost(config.peerHost)}:${config.peerPort}.`);
13
+ }
14
+ if (loopbackOnly) {
15
+ warnings.push("Listen URL uses a loopback host. Other machines cannot reach this URL unless they run on the same host.");
16
+ }
17
+ if (bindLoopbackOnly && !loopbackOnly) {
18
+ warnings.push("Peer server is bound to loopback. Remote access requires a local tunnel, reverse proxy, or port forward to this host.");
19
+ }
20
+ if (!config.peerTlsEnabled && (!loopbackOnly || !bindLoopbackOnly)) {
21
+ warnings.push("Peer TLS is disabled. Use TLS for non-loopback or internet-reachable peer endpoints.");
22
+ }
23
+ return {
24
+ enabled: config.peerEnabled,
25
+ listenUrl,
26
+ bindHost: config.peerHost,
27
+ port: config.peerPort,
28
+ tlsEnabled: config.peerTlsEnabled,
29
+ requireTls: config.peerRequireTls,
30
+ localListening,
31
+ loopbackOnly,
32
+ bindLoopbackOnly,
33
+ manualCheckCommand: `nordrelay peer check ${listenUrl}`,
34
+ warnings,
35
+ };
36
+ }
37
+ export function peerListenUrl(config) {
38
+ if (config.peerPublicUrl)
39
+ return config.peerPublicUrl;
40
+ const scheme = config.peerTlsEnabled ? "https" : "http";
41
+ const host = config.peerHost === "0.0.0.0" || config.peerHost === "::" ? "127.0.0.1" : config.peerHost;
42
+ const displayHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
43
+ return `${scheme}://${displayHost}:${config.peerPort}`;
44
+ }
45
+ function checkLocalPort(host, port) {
46
+ return new Promise((resolve) => {
47
+ const socket = net.createConnection({ host: connectHostForBindHost(host), port });
48
+ const finish = (ok) => {
49
+ socket.removeAllListeners();
50
+ socket.destroy();
51
+ resolve(ok);
52
+ };
53
+ socket.setTimeout(1_500);
54
+ socket.once("connect", () => finish(true));
55
+ socket.once("timeout", () => finish(false));
56
+ socket.once("error", () => finish(false));
57
+ });
58
+ }
59
+ function connectHostForBindHost(host) {
60
+ if (!host || host === "0.0.0.0")
61
+ return "127.0.0.1";
62
+ if (host === "::")
63
+ return "::1";
64
+ return host;
65
+ }
66
+ function isLoopbackUrl(value) {
67
+ try {
68
+ return isLoopbackHost(new URL(value).hostname);
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ function isLoopbackHost(host) {
75
+ const normalized = host.replace(/^\[|\]$/g, "").toLowerCase();
76
+ return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || normalized.startsWith("127.");
77
+ }