@nordbyte/nordrelay 0.5.2 → 0.7.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 (71) hide show
  1. package/.env.example +80 -11
  2. package/README.md +154 -22
  3. package/dist/access-control.js +7 -1
  4. package/dist/activity-events.js +44 -0
  5. package/dist/audit-log.js +40 -2
  6. package/dist/bot-preferences.js +1 -0
  7. package/dist/bot-rendering.js +10 -7
  8. package/dist/bot.js +535 -11
  9. package/dist/channel-actions.js +7 -2
  10. package/dist/channel-adapter.js +40 -7
  11. package/dist/channel-command-catalog.js +88 -0
  12. package/dist/channel-command-service.js +369 -0
  13. package/dist/channel-mirror-registry.js +77 -0
  14. package/dist/channel-peer-prompt.js +95 -0
  15. package/dist/channel-runtime.js +12 -5
  16. package/dist/channel-turn-service.js +237 -0
  17. package/dist/codex-state.js +114 -78
  18. package/dist/config-metadata.js +93 -13
  19. package/dist/config.js +103 -8
  20. package/dist/context-key.js +87 -5
  21. package/dist/discord-artifacts.js +165 -0
  22. package/dist/discord-bot.js +2073 -0
  23. package/dist/discord-channel-runtime.js +133 -0
  24. package/dist/discord-command-surface.js +57 -0
  25. package/dist/discord-rate-limit.js +141 -0
  26. package/dist/index.js +36 -5
  27. package/dist/job-store.js +127 -0
  28. package/dist/metrics.js +87 -0
  29. package/dist/peer-auth.js +85 -0
  30. package/dist/peer-client.js +256 -0
  31. package/dist/peer-context.js +21 -0
  32. package/dist/peer-identity.js +127 -0
  33. package/dist/peer-runtime-service.js +636 -0
  34. package/dist/peer-server.js +220 -0
  35. package/dist/peer-store.js +294 -0
  36. package/dist/peer-types.js +52 -0
  37. package/dist/relay-external-activity-monitor.js +47 -6
  38. package/dist/relay-runtime-helpers.js +208 -0
  39. package/dist/relay-runtime.js +897 -394
  40. package/dist/remote-prompt.js +98 -0
  41. package/dist/runtime-cache.js +57 -0
  42. package/dist/session-locks.js +10 -7
  43. package/dist/support-bundle.js +1 -0
  44. package/dist/telegram-access-commands.js +15 -2
  45. package/dist/telegram-access-middleware.js +16 -3
  46. package/dist/telegram-agent-commands.js +25 -0
  47. package/dist/telegram-artifact-commands.js +46 -0
  48. package/dist/telegram-command-menu.js +3 -53
  49. package/dist/telegram-diagnostics-command.js +5 -50
  50. package/dist/telegram-general-commands.js +16 -6
  51. package/dist/telegram-operational-commands.js +14 -6
  52. package/dist/telegram-preference-commands.js +23 -127
  53. package/dist/telegram-queue-commands.js +74 -4
  54. package/dist/telegram-support-command.js +7 -0
  55. package/dist/telegram-update-commands.js +27 -0
  56. package/dist/user-management.js +208 -0
  57. package/dist/web-api-contract.js +17 -0
  58. package/dist/web-dashboard-access-routes.js +74 -1
  59. package/dist/web-dashboard-artifact-routes.js +3 -3
  60. package/dist/web-dashboard-assets.js +2 -0
  61. package/dist/web-dashboard-pages.js +109 -13
  62. package/dist/web-dashboard-peer-routes.js +204 -0
  63. package/dist/web-dashboard-runtime-routes.js +53 -8
  64. package/dist/web-dashboard-session-routes.js +27 -20
  65. package/dist/web-dashboard-ui.js +2 -0
  66. package/dist/web-dashboard.js +160 -6
  67. package/dist/web-state.js +33 -2
  68. package/dist/webui-assets/dashboard.css +75 -1
  69. package/dist/webui-assets/dashboard.js +779 -55
  70. package/package.json +5 -2
  71. package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
@@ -0,0 +1,85 @@
1
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
3
+ const NONCE_TTL_MS = 10 * 60 * 1000;
4
+ export class PeerNonceCache {
5
+ seen = new Map();
6
+ consume(peerId, nonce) {
7
+ const now = Date.now();
8
+ for (const [key, expiresAt] of this.seen.entries()) {
9
+ if (expiresAt <= now) {
10
+ this.seen.delete(key);
11
+ }
12
+ }
13
+ const key = `${peerId}:${nonce}`;
14
+ if (this.seen.has(key)) {
15
+ return false;
16
+ }
17
+ this.seen.set(key, now + NONCE_TTL_MS);
18
+ return true;
19
+ }
20
+ }
21
+ export function signPeerRequest(peer, method, pathname, body = "") {
22
+ const timestamp = new Date().toISOString();
23
+ const nonce = randomBytes(16).toString("base64url");
24
+ const bodyHash = hashBody(body);
25
+ const signature = createSignature(peer.secret, canonicalRequest(method, pathname, timestamp, nonce, bodyHash));
26
+ return {
27
+ timestamp,
28
+ nonce,
29
+ bodyHash,
30
+ headers: {
31
+ "x-nordrelay-peer-id": peer.id,
32
+ "x-nordrelay-peer-timestamp": timestamp,
33
+ "x-nordrelay-peer-nonce": nonce,
34
+ "x-nordrelay-peer-body-sha256": bodyHash,
35
+ "x-nordrelay-peer-signature": signature,
36
+ },
37
+ };
38
+ }
39
+ export function verifyPeerRequest(options) {
40
+ const timestamp = header(options.req, "x-nordrelay-peer-timestamp");
41
+ const nonce = header(options.req, "x-nordrelay-peer-nonce");
42
+ const bodyHash = header(options.req, "x-nordrelay-peer-body-sha256");
43
+ const signature = header(options.req, "x-nordrelay-peer-signature");
44
+ if (!timestamp || !nonce || !bodyHash || !signature) {
45
+ throw new Error("Missing peer authentication headers.");
46
+ }
47
+ const time = Date.parse(timestamp);
48
+ if (!Number.isFinite(time) || Math.abs(Date.now() - time) > MAX_CLOCK_SKEW_MS) {
49
+ throw new Error("Peer request timestamp is outside the allowed clock skew.");
50
+ }
51
+ if (hashBody(options.body) !== bodyHash) {
52
+ throw new Error("Peer request body hash mismatch.");
53
+ }
54
+ if (!options.nonces.consume(options.peer.id, nonce)) {
55
+ throw new Error("Replay detected for peer request.");
56
+ }
57
+ const expected = createSignature(options.peer.secret, canonicalRequest(options.method, options.pathname, timestamp, nonce, bodyHash));
58
+ if (!safeEqual(signature, expected)) {
59
+ throw new Error("Invalid peer request signature.");
60
+ }
61
+ }
62
+ export function header(req, name) {
63
+ const value = req.headers[name.toLowerCase()];
64
+ return Array.isArray(value) ? value[0] ?? "" : value ?? "";
65
+ }
66
+ export function hashBody(body) {
67
+ return createHash("sha256").update(body).digest("hex");
68
+ }
69
+ function canonicalRequest(method, pathname, timestamp, nonce, bodyHash) {
70
+ return [
71
+ method.toUpperCase(),
72
+ pathname,
73
+ timestamp,
74
+ nonce,
75
+ bodyHash,
76
+ ].join("\n");
77
+ }
78
+ function createSignature(secret, value) {
79
+ return createHmac("sha256", secret).update(value).digest("base64url");
80
+ }
81
+ function safeEqual(left, right) {
82
+ const leftBuffer = Buffer.from(left);
83
+ const rightBuffer = Buffer.from(right);
84
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
85
+ }
@@ -0,0 +1,256 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import { createPairingSignaturePayload, signPeerPayload, fingerprintForPublicKey, } from "./peer-identity.js";
4
+ import { signPeerRequest } from "./peer-auth.js";
5
+ import { PeerStore } from "./peer-store.js";
6
+ import { PEER_PROTOCOL_VERSION, } from "./peer-types.js";
7
+ export async function pairPeer(options, identity, store = new PeerStore()) {
8
+ const timestamp = new Date().toISOString();
9
+ const payload = createPairingSignaturePayload(identity.public.nodeId, timestamp, options.code);
10
+ const body = {
11
+ code: options.code,
12
+ name: options.name,
13
+ publicUrl: options.publicUrl,
14
+ identity: identity.public,
15
+ timestamp,
16
+ signature: signPeerPayload(identity.privateKey, payload),
17
+ };
18
+ const result = await requestJson({
19
+ url: joinPeerUrl(options.url, "/peer/pair"),
20
+ method: "POST",
21
+ body,
22
+ allowSelfSigned: true,
23
+ });
24
+ if (result.data.protocolVersion !== PEER_PROTOCOL_VERSION) {
25
+ throw new Error(`Unsupported peer protocol version: ${result.data.protocolVersion}`);
26
+ }
27
+ if (fingerprintForPublicKey(result.data.identity.publicKey) !== result.data.identity.fingerprint) {
28
+ throw new Error("Remote peer identity fingerprint does not match its public key.");
29
+ }
30
+ const peer = store.upsertPeer({
31
+ id: result.data.peerId,
32
+ name: result.data.identity.name,
33
+ url: normalizePeerUrl(options.url),
34
+ nodeId: result.data.identity.nodeId,
35
+ publicKey: result.data.identity.publicKey,
36
+ fingerprint: result.data.identity.fingerprint,
37
+ tlsFingerprint: result.tlsFingerprint,
38
+ secret: result.data.secret,
39
+ enabled: true,
40
+ direction: "outbound",
41
+ scopes: result.data.scopes,
42
+ allowedAgents: result.data.allowedAgents,
43
+ allowedWorkspaceRoots: result.data.allowedWorkspaceRoots,
44
+ workspaceAliases: result.data.workspaceAliases,
45
+ });
46
+ return { peer, tlsFingerprint: result.tlsFingerprint };
47
+ }
48
+ export class RemoteRelayClient {
49
+ store;
50
+ constructor(store = new PeerStore()) {
51
+ this.store = store;
52
+ }
53
+ async rpc(peerId, type, payload, actor) {
54
+ const peer = this.requiredPeer(peerId);
55
+ const body = {
56
+ protocolVersion: PEER_PROTOCOL_VERSION,
57
+ type,
58
+ payload,
59
+ actor,
60
+ };
61
+ const bodyText = JSON.stringify(body);
62
+ const signed = signPeerRequest(peer, "POST", "/peer/rpc", bodyText);
63
+ try {
64
+ const startedAt = Date.now();
65
+ const result = await requestJson({
66
+ url: joinPeerUrl(requiredPeerUrl(peer), "/peer/rpc"),
67
+ method: "POST",
68
+ bodyText,
69
+ headers: signed.headers,
70
+ expectedTlsFingerprint: peer.tlsFingerprint,
71
+ allowSelfSigned: Boolean(peer.tlsFingerprint),
72
+ });
73
+ this.store.markSeen(peer.id, healthPatchFromRpc(type, result.data.ok ? result.data.data : null, Date.now() - startedAt));
74
+ if (!result.data.ok) {
75
+ throw new Error(result.data.error);
76
+ }
77
+ return result.data.data;
78
+ }
79
+ catch (error) {
80
+ this.store.markError(peer.id, error instanceof Error ? error.message : String(error));
81
+ throw error;
82
+ }
83
+ }
84
+ async webProxy(peerId, payload, actor, sourceContextKey) {
85
+ return this.rpc(peerId, "web.proxy", sourceContextKey ? { ...payload, contextKey: sourceContextKey } : payload, actor);
86
+ }
87
+ subscribe(peerId, onEvent, onError, sourceContextKey) {
88
+ const peer = this.requiredPeer(peerId);
89
+ const url = new URL(joinPeerUrl(requiredPeerUrl(peer), "/peer/events"));
90
+ if (sourceContextKey) {
91
+ url.searchParams.set("contextKey", sourceContextKey);
92
+ }
93
+ const signed = signPeerRequest(peer, "GET", `${url.pathname}${url.search}`, "");
94
+ const transport = url.protocol === "https:" ? https : http;
95
+ const req = transport.request({
96
+ method: "GET",
97
+ protocol: url.protocol,
98
+ hostname: url.hostname,
99
+ port: url.port,
100
+ path: `${url.pathname}${url.search}`,
101
+ headers: signed.headers,
102
+ rejectUnauthorized: false,
103
+ }, (res) => {
104
+ try {
105
+ assertTlsFingerprint(res.socket, peer.tlsFingerprint);
106
+ }
107
+ catch (error) {
108
+ req.destroy(error);
109
+ return;
110
+ }
111
+ if ((res.statusCode ?? 500) >= 400) {
112
+ req.destroy(new Error(`Peer events failed with HTTP ${res.statusCode}`));
113
+ return;
114
+ }
115
+ this.store.markSeen(peer.id, { remoteStatus: "online" });
116
+ let buffer = "";
117
+ res.setEncoding("utf8");
118
+ res.on("data", (chunk) => {
119
+ buffer += chunk;
120
+ let separator = buffer.indexOf("\n\n");
121
+ while (separator !== -1) {
122
+ const frame = buffer.slice(0, separator);
123
+ buffer = buffer.slice(separator + 2);
124
+ const data = frame.split(/\n/).find((line) => line.startsWith("data:"))?.slice(5).trim();
125
+ if (data) {
126
+ try {
127
+ onEvent(JSON.parse(data));
128
+ }
129
+ catch {
130
+ // Ignore malformed event frames from a broken peer.
131
+ }
132
+ }
133
+ separator = buffer.indexOf("\n\n");
134
+ }
135
+ });
136
+ });
137
+ req.on("error", (error) => {
138
+ this.store.markError(peer.id, error.message);
139
+ onError?.(error);
140
+ });
141
+ req.end();
142
+ return {
143
+ close: () => req.destroy(),
144
+ };
145
+ }
146
+ requiredPeer(peerId) {
147
+ const peer = this.store.get(peerId);
148
+ if (!peer) {
149
+ throw new Error("Peer not found.");
150
+ }
151
+ if (!peer.enabled) {
152
+ throw new Error("Peer is disabled.");
153
+ }
154
+ return peer;
155
+ }
156
+ }
157
+ function healthPatchFromRpc(type, data, latencyMs) {
158
+ if (type !== "peer.ping" || !data || typeof data !== "object") {
159
+ return { latencyMs, remoteStatus: "online" };
160
+ }
161
+ const record = data;
162
+ return {
163
+ latencyMs,
164
+ remoteVersion: typeof record.version === "string" ? record.version : undefined,
165
+ remoteStatus: typeof record.status === "string" ? record.status : "online",
166
+ };
167
+ }
168
+ async function requestJson(options) {
169
+ const url = new URL(options.url);
170
+ const bodyText = options.bodyText ?? (options.body === undefined ? "" : JSON.stringify(options.body));
171
+ const transport = url.protocol === "https:" ? https : http;
172
+ return await new Promise((resolve, reject) => {
173
+ const req = transport.request({
174
+ method: options.method,
175
+ protocol: url.protocol,
176
+ hostname: url.hostname,
177
+ port: url.port,
178
+ path: `${url.pathname}${url.search}`,
179
+ headers: {
180
+ "content-type": "application/json",
181
+ "content-length": Buffer.byteLength(bodyText),
182
+ ...(options.headers ?? {}),
183
+ },
184
+ rejectUnauthorized: options.allowSelfSigned ? false : undefined,
185
+ }, (res) => {
186
+ let tlsFingerprint;
187
+ try {
188
+ tlsFingerprint = assertTlsFingerprint(res.socket, options.expectedTlsFingerprint);
189
+ }
190
+ catch (error) {
191
+ reject(error);
192
+ req.destroy();
193
+ return;
194
+ }
195
+ const chunks = [];
196
+ res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
197
+ res.on("end", () => {
198
+ const text = Buffer.concat(chunks).toString("utf8");
199
+ let data = {};
200
+ try {
201
+ data = text ? JSON.parse(text) : {};
202
+ }
203
+ catch {
204
+ reject(new Error(`Peer returned invalid JSON: ${text.slice(0, 200)}`));
205
+ return;
206
+ }
207
+ if ((res.statusCode ?? 500) >= 400) {
208
+ const message = data && typeof data === "object" && "error" in data ? String(data.error) : `HTTP ${res.statusCode}`;
209
+ reject(new Error(message));
210
+ return;
211
+ }
212
+ resolve({ data: data, tlsFingerprint });
213
+ });
214
+ });
215
+ req.on("error", reject);
216
+ if (bodyText)
217
+ req.write(bodyText);
218
+ req.end();
219
+ });
220
+ }
221
+ function assertTlsFingerprint(socket, expected) {
222
+ if (!socket.encrypted) {
223
+ if (expected) {
224
+ throw new Error("Expected a TLS peer connection, but the peer used plaintext HTTP.");
225
+ }
226
+ return undefined;
227
+ }
228
+ const certificate = socket.getPeerCertificate();
229
+ const actual = normalizeFingerprint(certificate?.fingerprint256);
230
+ if (expected && actual !== normalizeFingerprint(expected)) {
231
+ throw new Error("Peer TLS certificate fingerprint mismatch.");
232
+ }
233
+ return actual;
234
+ }
235
+ function requiredPeerUrl(peer) {
236
+ if (!peer.url) {
237
+ throw new Error(`Peer ${peer.name} has no URL.`);
238
+ }
239
+ return peer.url;
240
+ }
241
+ function joinPeerUrl(base, route) {
242
+ const url = new URL(normalizePeerUrl(base));
243
+ url.pathname = route;
244
+ url.search = "";
245
+ return url.toString();
246
+ }
247
+ function normalizePeerUrl(value) {
248
+ const url = new URL(value);
249
+ url.pathname = "";
250
+ url.search = "";
251
+ url.hash = "";
252
+ return url.toString().replace(/\/$/, "");
253
+ }
254
+ function normalizeFingerprint(value) {
255
+ return value?.trim().toLowerCase();
256
+ }
@@ -0,0 +1,21 @@
1
+ const DEFAULT_SOURCE_CONTEXT = "web:dashboard";
2
+ export function peerRuntimeContextKey(peer, sourceContextKey) {
3
+ const source = sourceContextKey?.trim() || DEFAULT_SOURCE_CONTEXT;
4
+ return `peer:${encodeContextPart(peer.id || peer.nodeId)}:${encodeContextPart(source)}`;
5
+ }
6
+ export function parsePeerRuntimeContextKey(key) {
7
+ const match = /^peer:([^:]+):(.+)$/.exec(key);
8
+ if (!match?.[1] || !match[2]) {
9
+ return null;
10
+ }
11
+ return {
12
+ peerId: decodeContextPart(match[1]),
13
+ sourceContextKey: decodeContextPart(match[2]),
14
+ };
15
+ }
16
+ function encodeContextPart(value) {
17
+ return Buffer.from(value, "utf8").toString("base64url");
18
+ }
19
+ function decodeContextPart(value) {
20
+ return Buffer.from(value, "base64url").toString("utf8");
21
+ }
@@ -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
+ }