@nordbyte/nordrelay 0.8.0 → 0.8.2
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.
- package/.env.example +9 -0
- package/README.md +81 -1197
- package/dist/{access-control.js → access/access-control.js} +1 -1
- package/dist/{audit-log.js → access/audit-log.js} +2 -2
- package/dist/{session-locks.js → access/session-locks.js} +1 -1
- package/dist/{user-management.js → access/user-management.js} +1 -1
- package/dist/{claude-code-cli.js → agents/claude-code/claude-code-cli.js} +2 -2
- package/dist/{claude-code-session.js → agents/claude-code/claude-code-session.js} +1 -1
- package/dist/{codex-cli.js → agents/codex/codex-cli.js} +14 -5
- package/dist/{codex-session.js → agents/codex/codex-session.js} +2 -4
- package/dist/{hermes-cli.js → agents/hermes/hermes-cli.js} +2 -2
- package/dist/{hermes-launch.js → agents/hermes/hermes-launch.js} +1 -1
- package/dist/{hermes-session.js → agents/hermes/hermes-session.js} +1 -1
- package/dist/{openclaw-cli.js → agents/openclaw/openclaw-cli.js} +2 -2
- package/dist/{openclaw-launch.js → agents/openclaw/openclaw-launch.js} +1 -1
- package/dist/{openclaw-session.js → agents/openclaw/openclaw-session.js} +1 -1
- package/dist/{pi-cli.js → agents/pi/pi-cli.js} +2 -2
- package/dist/{pi-launch.js → agents/pi/pi-launch.js} +1 -1
- package/dist/{pi-session.js → agents/pi/pi-session.js} +1 -1
- package/dist/{adapter-conformance.js → agents/shared/adapter-conformance.js} +2 -2
- package/dist/{agent-activity.js → agents/shared/agent-activity.js} +5 -5
- package/dist/agents/shared/agent-auth-commands.js +30 -0
- package/dist/{agent-factory.js → agents/shared/agent-factory.js} +5 -5
- package/dist/{agent-feature-matrix.js → agents/shared/agent-feature-matrix.js} +2 -2
- package/dist/{agent-updates.js → agents/shared/agent-updates.js} +7 -7
- package/dist/{discord-artifacts.js → channels/discord/discord-artifacts.js} +4 -4
- package/dist/{discord-bot.js → channels/discord/discord-bot.js} +164 -424
- package/dist/{discord-channel-runtime.js → channels/discord/discord-channel-runtime.js} +2 -2
- package/dist/{discord-command-surface.js → channels/discord/discord-command-surface.js} +3 -3
- package/dist/{bot-rendering.js → channels/shared/bot-rendering.js} +6 -6
- package/dist/{channel-actions.js → channels/shared/channel-actions.js} +4 -4
- package/dist/channels/shared/channel-bridge-controller.js +69 -0
- package/dist/channels/shared/channel-cli-artifacts.js +51 -0
- package/dist/{channel-command-service.js → channels/shared/channel-command-service.js} +51 -28
- package/dist/channels/shared/channel-external-mirror-controller.js +193 -0
- package/dist/channels/shared/channel-external-monitor.js +52 -0
- package/dist/{channel-mirror-registry.js → channels/shared/channel-mirror-registry.js} +14 -6
- package/dist/{channel-peer-prompt.js → channels/shared/channel-peer-prompt.js} +3 -3
- package/dist/{channel-turn-service.js → channels/shared/channel-turn-service.js} +2 -2
- package/dist/{context-key.js → channels/shared/context-key.js} +1 -1
- package/dist/{session-format.js → channels/shared/session-format.js} +2 -2
- package/dist/{slack-artifacts.js → channels/slack/slack-artifacts.js} +4 -4
- package/dist/{slack-bot.js → channels/slack/slack-bot.js} +159 -294
- package/dist/{slack-channel-runtime.js → channels/slack/slack-channel-runtime.js} +2 -2
- package/dist/{slack-command-surface.js → channels/slack/slack-command-surface.js} +2 -2
- package/dist/{slack-diagnostics.js → channels/slack/slack-diagnostics.js} +2 -2
- package/dist/{bot-ui.js → channels/telegram/bot-ui.js} +1 -1
- package/dist/{bot.js → channels/telegram/bot.js} +178 -427
- package/dist/{telegram-access-commands.js → channels/telegram/telegram-access-commands.js} +3 -3
- package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js} +4 -4
- package/dist/{telegram-agent-commands.js → channels/telegram/telegram-agent-commands.js} +9 -9
- package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js} +4 -4
- package/dist/{telegram-channel-runtime.js → channels/telegram/telegram-channel-runtime.js} +2 -2
- package/dist/{telegram-command-menu.js → channels/telegram/telegram-command-menu.js} +1 -1
- package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js} +7 -7
- package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js} +4 -4
- package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js} +5 -5
- package/dist/{telegram-output.js → channels/telegram/telegram-output.js} +2 -2
- package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js} +3 -3
- package/dist/{telegram-queue-commands.js → channels/telegram/telegram-queue-commands.js} +6 -6
- package/dist/{telegram-support-command.js → channels/telegram/telegram-support-command.js} +4 -4
- package/dist/{telegram-update-commands.js → channels/telegram/telegram-update-commands.js} +5 -5
- package/dist/{config-metadata.js → core/config-metadata.js} +8 -0
- package/dist/{config.js → core/config.js} +11 -3
- package/dist/index.js +27 -23
- package/dist/{peer-client.js → peers/peer-client.js} +57 -1
- package/dist/peers/peer-discovery-jobs.js +206 -0
- package/dist/peers/peer-discovery.js +223 -0
- package/dist/peers/peer-health-monitor.js +49 -0
- package/dist/{peer-identity.js → peers/peer-identity.js} +50 -1
- package/dist/{peer-runtime-service.js → peers/peer-runtime-service.js} +29 -7
- package/dist/{peer-server.js → peers/peer-server.js} +23 -6
- package/dist/{peer-store.js → peers/peer-store.js} +84 -11
- package/dist/{peer-types.js → peers/peer-types.js} +9 -0
- package/dist/peers/peer-web-proxy-contract.js +127 -0
- package/dist/{metrics.js → runtime/metrics.js} +5 -3
- package/dist/{relay-artifact-service.js → runtime/relay-artifact-service.js} +1 -1
- package/dist/runtime/relay-auth-service.js +63 -0
- package/dist/runtime/relay-dashboard-service.js +139 -0
- package/dist/{relay-external-activity-monitor.js → runtime/relay-external-activity-monitor.js} +140 -53
- package/dist/runtime/relay-runtime-active-sessions.js +387 -0
- package/dist/runtime/relay-runtime-dashboard.js +201 -0
- package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +307 -0
- package/dist/runtime/relay-runtime-sessions.js +623 -0
- package/dist/runtime/relay-runtime-types.js +1 -0
- package/dist/runtime/relay-runtime-updates-jobs.js +360 -0
- package/dist/runtime/relay-runtime.js +451 -0
- package/dist/runtime/runtime-cache.js +117 -0
- package/dist/{session-registry.js → state/session-registry.js} +3 -3
- package/dist/{operations.js → support/operations.js} +7 -7
- package/dist/{support-bundle.js → support/support-bundle.js} +1 -1
- package/dist/{web-api-contract.js → web/web-api-contract.js} +17 -3
- package/dist/web/web-api-types.js +1 -0
- package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +2 -2
- package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +24 -2
- package/dist/{web-dashboard-http.js → web/web-dashboard-http.js} +41 -5
- package/dist/{web-dashboard-pages.js → web/web-dashboard-pages.js} +37 -10
- package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +102 -7
- package/dist/web/web-dashboard-security.js +14 -0
- package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +12 -1
- package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
- package/dist/web/web-performance.js +60 -0
- package/dist/web/web-rate-limit.js +19 -0
- package/dist/{web-state.js → web/web-state.js} +74 -5
- package/dist/webui-assets/dashboard.css +171 -10
- package/dist/webui-assets/dashboard.js +515 -48
- package/dist/webui-assets/favicon.ico +0 -0
- package/dist/webui-assets/favicon.png +0 -0
- package/dist/webui-assets/logo.png +0 -0
- package/package.json +4 -3
- package/plugins/nordrelay/scripts/nordrelay.mjs +17 -5
- package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
- package/dist/relay-runtime.js +0 -1916
- package/dist/runtime-cache.js +0 -57
- /package/dist/{user-management-crypto.js → access/user-management-crypto.js} +0 -0
- /package/dist/{user-management-normalize.js → access/user-management-normalize.js} +0 -0
- /package/dist/{user-management-types.js → access/user-management-types.js} +0 -0
- /package/dist/{claude-code-auth.js → agents/claude-code/claude-code-auth.js} +0 -0
- /package/dist/{claude-code-launch.js → agents/claude-code/claude-code-launch.js} +0 -0
- /package/dist/{claude-code-state.js → agents/claude-code/claude-code-state.js} +0 -0
- /package/dist/{codex-auth.js → agents/codex/codex-auth.js} +0 -0
- /package/dist/{codex-config.js → agents/codex/codex-config.js} +0 -0
- /package/dist/{codex-launch.js → agents/codex/codex-launch.js} +0 -0
- /package/dist/{codex-state.js → agents/codex/codex-state.js} +0 -0
- /package/dist/{hermes-api.js → agents/hermes/hermes-api.js} +0 -0
- /package/dist/{hermes-auth.js → agents/hermes/hermes-auth.js} +0 -0
- /package/dist/{hermes-state.js → agents/hermes/hermes-state.js} +0 -0
- /package/dist/{openclaw-auth.js → agents/openclaw/openclaw-auth.js} +0 -0
- /package/dist/{openclaw-gateway.js → agents/openclaw/openclaw-gateway.js} +0 -0
- /package/dist/{openclaw-state.js → agents/openclaw/openclaw-state.js} +0 -0
- /package/dist/{pi-auth.js → agents/pi/pi-auth.js} +0 -0
- /package/dist/{pi-rpc.js → agents/pi/pi-rpc.js} +0 -0
- /package/dist/{pi-state.js → agents/pi/pi-state.js} +0 -0
- /package/dist/{agent-adapter.js → agents/shared/agent-adapter.js} +0 -0
- /package/dist/{agent.js → agents/shared/agent.js} +0 -0
- /package/dist/{artifacts.js → artifacts/artifacts.js} +0 -0
- /package/dist/{attachments.js → artifacts/attachments.js} +0 -0
- /package/dist/{voice.js → artifacts/voice.js} +0 -0
- /package/dist/{discord-rate-limit.js → channels/discord/discord-rate-limit.js} +0 -0
- /package/dist/{channel-adapter.js → channels/shared/channel-adapter.js} +0 -0
- /package/dist/{relay-runtime-types.js → channels/shared/channel-bridge-state.js} +0 -0
- /package/dist/{channel-command-catalog.js → channels/shared/channel-command-catalog.js} +0 -0
- /package/dist/{channel-command-core.js → channels/shared/channel-command-core.js} +0 -0
- /package/dist/{channel-prompt-engine.js → channels/shared/channel-prompt-engine.js} +0 -0
- /package/dist/{channel-runtime.js → channels/shared/channel-runtime.js} +0 -0
- /package/dist/{channel-turn-lifecycle.js → channels/shared/channel-turn-lifecycle.js} +0 -0
- /package/dist/{slack-rate-limit.js → channels/slack/slack-rate-limit.js} +0 -0
- /package/dist/{telegram-command-types.js → channels/telegram/telegram-command-types.js} +0 -0
- /package/dist/{telegram-rate-limit.js → channels/telegram/telegram-rate-limit.js} +0 -0
- /package/dist/{activity-events.js → core/activity-events.js} +0 -0
- /package/dist/{error-messages.js → core/error-messages.js} +0 -0
- /package/dist/{format.js → core/format.js} +0 -0
- /package/dist/{logger.js → core/logger.js} +0 -0
- /package/dist/{redaction.js → core/redaction.js} +0 -0
- /package/dist/{settings-service.js → core/settings-service.js} +0 -0
- /package/dist/{settings-wizard-test.js → core/settings-wizard-test.js} +0 -0
- /package/dist/{workspace-policy.js → core/workspace-policy.js} +0 -0
- /package/dist/{peer-auth.js → peers/peer-auth.js} +0 -0
- /package/dist/{peer-context.js → peers/peer-context.js} +0 -0
- /package/dist/{peer-readiness.js → peers/peer-readiness.js} +0 -0
- /package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +0 -0
- /package/dist/{web-api-types.js → runtime/relay-runtime-delegate.js} +0 -0
- /package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +0 -0
- /package/dist/{remote-prompt.js → runtime/remote-prompt.js} +0 -0
- /package/dist/{bot-preferences.js → state/bot-preferences.js} +0 -0
- /package/dist/{job-store.js → state/job-store.js} +0 -0
- /package/dist/{persistence.js → state/persistence.js} +0 -0
- /package/dist/{prompt-store.js → state/prompt-store.js} +0 -0
- /package/dist/{state-backend.js → state/state-backend.js} +0 -0
- /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
- /package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +0 -0
- /package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +0 -0
- /package/dist/{web-dashboard-ui.js → web/web-dashboard-ui.js} +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { createServer as createHttpServer } from "node:http";
|
|
2
2
|
import { createServer as createHttpsServer } from "node:https";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
|
-
import { friendlyErrorText } from "
|
|
4
|
+
import { friendlyErrorText } from "../core/error-messages.js";
|
|
5
5
|
import { createPairingSignaturePayload, createSharedSecret, ensurePeerTlsFiles, fingerprintForPublicKey, loadOrCreatePeerIdentity, verifyPeerPayload, } from "./peer-identity.js";
|
|
6
6
|
import { header, PeerNonceCache, verifyPeerRequest } from "./peer-auth.js";
|
|
7
|
+
import { checkPeerIdentityEndpoint } from "./peer-client.js";
|
|
7
8
|
import { peerRuntimeContextKey } from "./peer-context.js";
|
|
8
9
|
import { PeerStore } from "./peer-store.js";
|
|
9
10
|
import { PeerRuntimeService, peerError } from "./peer-runtime-service.js";
|
|
10
11
|
import { PEER_PROTOCOL_VERSION, } from "./peer-types.js";
|
|
11
|
-
import { RelayRuntime } from "
|
|
12
|
+
import { RelayRuntime } from "../runtime/relay-runtime.js";
|
|
12
13
|
export async function startPeerServer(options) {
|
|
13
14
|
const { config, runtime } = options;
|
|
14
15
|
if (!config.peerEnabled) {
|
|
@@ -78,7 +79,7 @@ export async function startPeerServer(options) {
|
|
|
78
79
|
if (req.method === "POST" && url.pathname === "/peer/pair") {
|
|
79
80
|
const bodyText = await readBody(req, 128 * 1024);
|
|
80
81
|
const body = parseJson(bodyText);
|
|
81
|
-
const response = handlePair(body);
|
|
82
|
+
const response = await handlePair(body);
|
|
82
83
|
sendJson(res, 201, response);
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
@@ -123,7 +124,7 @@ export async function startPeerServer(options) {
|
|
|
123
124
|
sendJson(res, status, { error: friendlyErrorText(error) });
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
function handlePair(body) {
|
|
127
|
+
async function handlePair(body) {
|
|
127
128
|
if (!body?.identity?.nodeId || !body.identity.publicKey || !body.code || !body.signature || !body.timestamp) {
|
|
128
129
|
throw new Error("Invalid peer pairing request.");
|
|
129
130
|
}
|
|
@@ -140,17 +141,21 @@ export async function startPeerServer(options) {
|
|
|
140
141
|
if (!verifyPeerPayload(body.identity.publicKey, signaturePayload, body.signature)) {
|
|
141
142
|
throw new Error("Invalid peer pairing signature.");
|
|
142
143
|
}
|
|
144
|
+
const publicUrl = body.publicUrl?.trim() || undefined;
|
|
145
|
+
const publicUrlTlsFingerprint = publicUrl ? await verifyPairingPublicUrl(publicUrl, body.identity) : undefined;
|
|
143
146
|
const invitation = store.consumeInvitation(body.code, body.identity.nodeId);
|
|
144
147
|
const secret = createSharedSecret();
|
|
145
148
|
const peer = store.upsertPeer({
|
|
146
149
|
name: body.name?.trim() || body.identity.name || invitation.name,
|
|
147
|
-
|
|
150
|
+
group: invitation.group,
|
|
151
|
+
url: publicUrl,
|
|
148
152
|
nodeId: body.identity.nodeId,
|
|
149
153
|
publicKey: body.identity.publicKey,
|
|
150
154
|
fingerprint: body.identity.fingerprint,
|
|
155
|
+
tlsFingerprint: publicUrl ? publicUrlTlsFingerprint ?? null : undefined,
|
|
151
156
|
secret,
|
|
152
157
|
enabled: true,
|
|
153
|
-
direction:
|
|
158
|
+
direction: publicUrl ? "bidirectional" : "inbound",
|
|
154
159
|
scopes: invitation.scopes,
|
|
155
160
|
allowedAgents: invitation.allowedAgents,
|
|
156
161
|
allowedWorkspaceRoots: invitation.allowedWorkspaceRoots,
|
|
@@ -167,6 +172,18 @@ export async function startPeerServer(options) {
|
|
|
167
172
|
workspaceAliases: peer.workspaceAliases,
|
|
168
173
|
};
|
|
169
174
|
}
|
|
175
|
+
async function verifyPairingPublicUrl(publicUrl, expectedIdentity) {
|
|
176
|
+
const probe = await checkPeerIdentityEndpoint(publicUrl, { timeoutMs: 4_000 });
|
|
177
|
+
if (!probe.ok || !probe.identity) {
|
|
178
|
+
throw new Error(`Peer public URL is not reachable or does not expose a valid NordRelay identity: ${probe.detail}`);
|
|
179
|
+
}
|
|
180
|
+
if (probe.identity.nodeId !== expectedIdentity.nodeId ||
|
|
181
|
+
probe.identity.publicKey !== expectedIdentity.publicKey ||
|
|
182
|
+
probe.identity.fingerprint !== expectedIdentity.fingerprint) {
|
|
183
|
+
throw new Error("Peer public URL identity does not match the pairing identity.");
|
|
184
|
+
}
|
|
185
|
+
return probe.tlsFingerprint;
|
|
186
|
+
}
|
|
170
187
|
function authenticate(req, method, pathname, body) {
|
|
171
188
|
const peerId = header(req, "x-nordrelay-peer-id");
|
|
172
189
|
const peer = peerId ? store.get(peerId) : null;
|
|
@@ -2,13 +2,14 @@ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypt
|
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { ALL_PERMISSIONS } from "
|
|
6
|
-
import { AGENT_IDS, isAgentId } from "
|
|
7
|
-
import { readJsonFileWithBackup, writeJsonFileAtomic } from "
|
|
5
|
+
import { ALL_PERMISSIONS } from "../access/access-control.js";
|
|
6
|
+
import { AGENT_IDS, isAgentId } from "../agents/shared/agent.js";
|
|
7
|
+
import { readJsonFileWithBackup, writeJsonFileAtomic } from "../state/persistence.js";
|
|
8
8
|
import { DEFAULT_PEER_SCOPES, publicInvitation, publicPeer, } from "./peer-types.js";
|
|
9
9
|
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
10
10
|
const INVITE_CODE_BYTES = 18;
|
|
11
11
|
const MAX_INVITATION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
const MAX_HEALTH_HISTORY = 20;
|
|
12
13
|
export class PeerStore {
|
|
13
14
|
filePath;
|
|
14
15
|
constructor(home = process.env.NORDRELAY_HOME || DEFAULT_HOME) {
|
|
@@ -22,6 +23,7 @@ export class PeerStore {
|
|
|
22
23
|
listenUrl: options.listenUrl,
|
|
23
24
|
requireTls: options.requireTls,
|
|
24
25
|
readiness: options.readiness,
|
|
26
|
+
groups: listGroups(payload),
|
|
25
27
|
peers: payload.peers.map(publicPeer),
|
|
26
28
|
invitations: payload.invitations.map(publicInvitation),
|
|
27
29
|
};
|
|
@@ -44,6 +46,7 @@ export class PeerStore {
|
|
|
44
46
|
const invitation = {
|
|
45
47
|
id: randomUUID().replace(/-/g, "").slice(0, 12),
|
|
46
48
|
name: options.name?.trim() || "NordRelay peer",
|
|
49
|
+
group: normalizeGroup(options.group),
|
|
47
50
|
codeHash: hashSecret(code),
|
|
48
51
|
createdAt: now.toISOString(),
|
|
49
52
|
expiresAt: expiresAt.toISOString(),
|
|
@@ -88,10 +91,13 @@ export class PeerStore {
|
|
|
88
91
|
const existing = payload.peers.find((peer) => peer.nodeId === input.nodeId || (input.id && peer.id === input.id));
|
|
89
92
|
if (existing) {
|
|
90
93
|
existing.name = input.name.trim() || existing.name;
|
|
94
|
+
existing.group = normalizeGroup(input.group) ?? existing.group;
|
|
91
95
|
existing.url = input.url ?? existing.url;
|
|
92
96
|
existing.publicKey = input.publicKey;
|
|
93
97
|
existing.fingerprint = input.fingerprint;
|
|
94
|
-
|
|
98
|
+
if (input.tlsFingerprint !== undefined) {
|
|
99
|
+
existing.tlsFingerprint = input.tlsFingerprint || undefined;
|
|
100
|
+
}
|
|
95
101
|
existing.secret = input.secret;
|
|
96
102
|
existing.enabled = input.enabled ?? existing.enabled;
|
|
97
103
|
existing.direction = mergeDirection(existing.direction, input.direction ?? existing.direction);
|
|
@@ -107,11 +113,12 @@ export class PeerStore {
|
|
|
107
113
|
const record = {
|
|
108
114
|
id: input.id ?? randomUUID().replace(/-/g, "").slice(0, 12),
|
|
109
115
|
name: input.name.trim() || "NordRelay peer",
|
|
116
|
+
group: normalizeGroup(input.group),
|
|
110
117
|
url: input.url,
|
|
111
118
|
nodeId: input.nodeId,
|
|
112
119
|
publicKey: input.publicKey,
|
|
113
120
|
fingerprint: input.fingerprint,
|
|
114
|
-
tlsFingerprint: input.tlsFingerprint,
|
|
121
|
+
tlsFingerprint: input.tlsFingerprint || undefined,
|
|
115
122
|
secret: input.secret,
|
|
116
123
|
enabled: input.enabled ?? true,
|
|
117
124
|
direction: input.direction ?? "outbound",
|
|
@@ -121,6 +128,7 @@ export class PeerStore {
|
|
|
121
128
|
workspaceAliases: normalizeWorkspaceAliases(input.workspaceAliases ?? {}),
|
|
122
129
|
createdAt: now,
|
|
123
130
|
updatedAt: now,
|
|
131
|
+
healthHistory: [],
|
|
124
132
|
};
|
|
125
133
|
payload.peers.push(record);
|
|
126
134
|
next = clonePeer(record);
|
|
@@ -139,6 +147,8 @@ export class PeerStore {
|
|
|
139
147
|
}
|
|
140
148
|
if (patch.name !== undefined)
|
|
141
149
|
peer.name = patch.name.trim() || peer.name;
|
|
150
|
+
if (patch.group !== undefined)
|
|
151
|
+
peer.group = normalizeGroup(patch.group);
|
|
142
152
|
if (patch.url !== undefined)
|
|
143
153
|
peer.url = patch.url.trim() || undefined;
|
|
144
154
|
if (patch.enabled !== undefined)
|
|
@@ -159,18 +169,53 @@ export class PeerStore {
|
|
|
159
169
|
}
|
|
160
170
|
return next;
|
|
161
171
|
}
|
|
172
|
+
updatePeerTlsFingerprint(id, tlsFingerprint) {
|
|
173
|
+
let next = null;
|
|
174
|
+
this.mutatePayload((payload) => {
|
|
175
|
+
const peer = payload.peers.find((candidate) => candidate.id === id || candidate.nodeId === id);
|
|
176
|
+
if (!peer) {
|
|
177
|
+
throw new Error("Peer not found.");
|
|
178
|
+
}
|
|
179
|
+
peer.tlsFingerprint = tlsFingerprint || undefined;
|
|
180
|
+
peer.updatedAt = new Date().toISOString();
|
|
181
|
+
next = clonePeer(peer);
|
|
182
|
+
});
|
|
183
|
+
if (!next) {
|
|
184
|
+
throw new Error("Peer not found.");
|
|
185
|
+
}
|
|
186
|
+
return next;
|
|
187
|
+
}
|
|
162
188
|
markSeen(id, patch = {}) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
189
|
+
const checkedAt = new Date().toISOString();
|
|
190
|
+
this.patchPeer(id, (peer) => ({
|
|
191
|
+
lastSeenAt: checkedAt,
|
|
192
|
+
lastCheckedAt: checkedAt,
|
|
166
193
|
lastLatencyMs: patch.latencyMs,
|
|
167
194
|
remoteVersion: patch.remoteVersion,
|
|
168
195
|
remoteStatus: patch.remoteStatus ?? "online",
|
|
169
196
|
lastError: undefined,
|
|
170
|
-
|
|
197
|
+
healthHistory: appendHealthSample(peer.healthHistory, {
|
|
198
|
+
checkedAt,
|
|
199
|
+
status: "online",
|
|
200
|
+
latencyMs: patch.latencyMs,
|
|
201
|
+
remoteVersion: patch.remoteVersion,
|
|
202
|
+
remoteStatus: patch.remoteStatus ?? "online",
|
|
203
|
+
}),
|
|
204
|
+
}));
|
|
171
205
|
}
|
|
172
206
|
markError(id, error) {
|
|
173
|
-
|
|
207
|
+
const checkedAt = new Date().toISOString();
|
|
208
|
+
this.patchPeer(id, (peer) => ({
|
|
209
|
+
lastError: error,
|
|
210
|
+
remoteStatus: "offline",
|
|
211
|
+
lastCheckedAt: checkedAt,
|
|
212
|
+
updatedAt: checkedAt,
|
|
213
|
+
healthHistory: appendHealthSample(peer.healthHistory, {
|
|
214
|
+
checkedAt,
|
|
215
|
+
status: "offline",
|
|
216
|
+
error,
|
|
217
|
+
}),
|
|
218
|
+
}));
|
|
174
219
|
}
|
|
175
220
|
revokePeer(id) {
|
|
176
221
|
let removed = false;
|
|
@@ -198,7 +243,7 @@ export class PeerStore {
|
|
|
198
243
|
const peer = payload.peers.find((candidate) => candidate.id === id || candidate.nodeId === id);
|
|
199
244
|
if (!peer)
|
|
200
245
|
return;
|
|
201
|
-
Object.assign(peer, patch);
|
|
246
|
+
Object.assign(peer, typeof patch === "function" ? patch(peer) : patch);
|
|
202
247
|
});
|
|
203
248
|
}
|
|
204
249
|
mutatePayload(mutator) {
|
|
@@ -217,13 +262,16 @@ export class PeerStore {
|
|
|
217
262
|
version: 1,
|
|
218
263
|
peers: payload.peers.filter(isPeerRecord).map((peer) => ({
|
|
219
264
|
...peer,
|
|
265
|
+
group: normalizeGroup(peer.group),
|
|
220
266
|
scopes: normalizeScopes(peer.scopes),
|
|
221
267
|
allowedAgents: normalizeAgents(peer.allowedAgents),
|
|
222
268
|
allowedWorkspaceRoots: normalizeWorkspaceRoots(peer.allowedWorkspaceRoots),
|
|
223
269
|
workspaceAliases: normalizeWorkspaceAliases(peer.workspaceAliases ?? {}),
|
|
270
|
+
healthHistory: normalizeHealthHistory(peer.healthHistory),
|
|
224
271
|
})),
|
|
225
272
|
invitations: payload.invitations.filter(isInvitationRecord).map((invitation) => ({
|
|
226
273
|
...invitation,
|
|
274
|
+
group: normalizeGroup(invitation.group),
|
|
227
275
|
scopes: normalizeScopes(invitation.scopes),
|
|
228
276
|
allowedAgents: normalizeAgents(invitation.allowedAgents),
|
|
229
277
|
allowedWorkspaceRoots: normalizeWorkspaceRoots(invitation.allowedWorkspaceRoots),
|
|
@@ -270,6 +318,30 @@ function normalizeWorkspaceAliases(value) {
|
|
|
270
318
|
}
|
|
271
319
|
return aliases;
|
|
272
320
|
}
|
|
321
|
+
function normalizeGroup(value) {
|
|
322
|
+
const group = typeof value === "string" ? value.trim() : "";
|
|
323
|
+
return group ? group.slice(0, 80) : undefined;
|
|
324
|
+
}
|
|
325
|
+
function normalizeHealthHistory(value) {
|
|
326
|
+
if (!Array.isArray(value)) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
return value
|
|
330
|
+
.filter((item) => {
|
|
331
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
332
|
+
return false;
|
|
333
|
+
const record = item;
|
|
334
|
+
return typeof record.checkedAt === "string" && (record.status === "online" || record.status === "offline");
|
|
335
|
+
})
|
|
336
|
+
.slice(-MAX_HEALTH_HISTORY)
|
|
337
|
+
.map((item) => ({ ...item }));
|
|
338
|
+
}
|
|
339
|
+
function appendHealthSample(history, sample) {
|
|
340
|
+
return [...normalizeHealthHistory(history), sample].slice(-MAX_HEALTH_HISTORY);
|
|
341
|
+
}
|
|
342
|
+
function listGroups(payload) {
|
|
343
|
+
return [...new Set(payload.peers.map((peer) => normalizeGroup(peer.group)).filter((group) => Boolean(group)))].sort();
|
|
344
|
+
}
|
|
273
345
|
function clonePeer(peer) {
|
|
274
346
|
return {
|
|
275
347
|
...peer,
|
|
@@ -277,6 +349,7 @@ function clonePeer(peer) {
|
|
|
277
349
|
allowedAgents: [...peer.allowedAgents],
|
|
278
350
|
allowedWorkspaceRoots: [...peer.allowedWorkspaceRoots],
|
|
279
351
|
workspaceAliases: { ...peer.workspaceAliases },
|
|
352
|
+
healthHistory: normalizeHealthHistory(peer.healthHistory),
|
|
280
353
|
};
|
|
281
354
|
}
|
|
282
355
|
function mergeDirection(left, right) {
|
|
@@ -16,6 +16,7 @@ export function publicPeer(record) {
|
|
|
16
16
|
return {
|
|
17
17
|
id: record.id,
|
|
18
18
|
name: record.name,
|
|
19
|
+
group: record.group,
|
|
19
20
|
url: record.url,
|
|
20
21
|
nodeId: record.nodeId,
|
|
21
22
|
fingerprint: record.fingerprint,
|
|
@@ -34,12 +35,20 @@ export function publicPeer(record) {
|
|
|
34
35
|
remoteVersion: record.remoteVersion,
|
|
35
36
|
remoteStatus: record.remoteStatus,
|
|
36
37
|
lastError: record.lastError,
|
|
38
|
+
healthHistory: record.healthHistory?.map((sample) => ({ ...sample })),
|
|
39
|
+
effectiveAccess: {
|
|
40
|
+
scopes: [...record.scopes],
|
|
41
|
+
allowedAgents: [...record.allowedAgents],
|
|
42
|
+
allowedWorkspaceRoots: [...record.allowedWorkspaceRoots],
|
|
43
|
+
workspaceAliases: { ...record.workspaceAliases },
|
|
44
|
+
},
|
|
37
45
|
};
|
|
38
46
|
}
|
|
39
47
|
export function publicInvitation(record) {
|
|
40
48
|
return {
|
|
41
49
|
id: record.id,
|
|
42
50
|
name: record.name,
|
|
51
|
+
group: record.group,
|
|
43
52
|
expiresAt: record.expiresAt,
|
|
44
53
|
createdAt: record.createdAt,
|
|
45
54
|
scopes: [...record.scopes],
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { WEB_API_ROUTE_DEFINITIONS } from "../web/web-api-contract.js";
|
|
2
|
+
const LOCAL_ONLY_ROUTE_PATHS = new Set([
|
|
3
|
+
"/api/auth/me",
|
|
4
|
+
"/api/dashboard/logout",
|
|
5
|
+
"/api/permissions",
|
|
6
|
+
"/api/settings",
|
|
7
|
+
"/api/settings/wizard/test",
|
|
8
|
+
"/api/peers",
|
|
9
|
+
"/api/peers/invite",
|
|
10
|
+
"/api/peers/pair",
|
|
11
|
+
"/api/peers/probe",
|
|
12
|
+
"/api/peers/discover",
|
|
13
|
+
"/api/peers/discovery-jobs",
|
|
14
|
+
"/api/peers/discovery-jobs/:id",
|
|
15
|
+
"/api/peers/discovery-jobs/:id/cancel",
|
|
16
|
+
"/api/peers/discovery-jobs/:id/log",
|
|
17
|
+
"/api/peers/identity/backup",
|
|
18
|
+
"/api/peers/identity/restore",
|
|
19
|
+
"/api/peers/invitations/:id",
|
|
20
|
+
"/api/peers/:id",
|
|
21
|
+
"/api/peers/:id/repin",
|
|
22
|
+
"/api/peers/:id/health",
|
|
23
|
+
"/api/peers/:id/proxy",
|
|
24
|
+
"/api/peers/:id/events",
|
|
25
|
+
"/api/peers/global-sessions",
|
|
26
|
+
"/api/users",
|
|
27
|
+
"/api/users/:id",
|
|
28
|
+
"/api/users/:id/password",
|
|
29
|
+
"/api/users/:id/sessions",
|
|
30
|
+
"/api/users/:id/sessions/:sessionId",
|
|
31
|
+
"/api/users/:id/telegram",
|
|
32
|
+
"/api/users/:id/telegram/:identityId",
|
|
33
|
+
"/api/users/:id/discord",
|
|
34
|
+
"/api/users/:id/discord/:identityId",
|
|
35
|
+
"/api/users/:id/slack",
|
|
36
|
+
"/api/users/:id/slack/:identityId",
|
|
37
|
+
"/api/groups",
|
|
38
|
+
"/api/groups/:id",
|
|
39
|
+
"/api/telegram-chats",
|
|
40
|
+
"/api/telegram-chats/:id",
|
|
41
|
+
"/api/discord-channels",
|
|
42
|
+
"/api/discord-channels/:id",
|
|
43
|
+
"/api/slack-channels",
|
|
44
|
+
"/api/slack-channels/:id",
|
|
45
|
+
"/api/audit",
|
|
46
|
+
]);
|
|
47
|
+
const IMPLEMENTED_ROUTE_PATHS = new Set([
|
|
48
|
+
"/api/bootstrap",
|
|
49
|
+
"/api/health",
|
|
50
|
+
"/api/snapshot",
|
|
51
|
+
"/api/tasks",
|
|
52
|
+
"/api/progress",
|
|
53
|
+
"/api/metrics",
|
|
54
|
+
"/api/jobs",
|
|
55
|
+
"/api/jobs/:id/log",
|
|
56
|
+
"/api/jobs/:id/action",
|
|
57
|
+
"/api/active-sessions",
|
|
58
|
+
"/api/version",
|
|
59
|
+
"/api/update",
|
|
60
|
+
"/api/agent-updates",
|
|
61
|
+
"/api/agent-update",
|
|
62
|
+
"/api/agent-update/:id/log",
|
|
63
|
+
"/api/agent-update/:id/input",
|
|
64
|
+
"/api/agent-update/:id/cancel",
|
|
65
|
+
"/api/adapters/health",
|
|
66
|
+
"/api/adapters/conformance",
|
|
67
|
+
"/api/locks",
|
|
68
|
+
"/api/auth/status",
|
|
69
|
+
"/api/auth/login",
|
|
70
|
+
"/api/auth/logout",
|
|
71
|
+
"/api/control-options",
|
|
72
|
+
"/api/sessions",
|
|
73
|
+
"/api/sessions/new",
|
|
74
|
+
"/api/sessions/switch",
|
|
75
|
+
"/api/sessions/attach",
|
|
76
|
+
"/api/sessions/detail",
|
|
77
|
+
"/api/agent",
|
|
78
|
+
"/api/models",
|
|
79
|
+
"/api/session/model",
|
|
80
|
+
"/api/session/reasoning",
|
|
81
|
+
"/api/session/fast",
|
|
82
|
+
"/api/session/launch",
|
|
83
|
+
"/api/prompt",
|
|
84
|
+
"/api/prompt/upload",
|
|
85
|
+
"/api/abort",
|
|
86
|
+
"/api/stop",
|
|
87
|
+
"/api/handback",
|
|
88
|
+
"/api/retry",
|
|
89
|
+
"/api/sync",
|
|
90
|
+
"/api/queue",
|
|
91
|
+
"/api/chat/history",
|
|
92
|
+
"/api/chat/mirror",
|
|
93
|
+
"/api/activity",
|
|
94
|
+
"/api/artifacts",
|
|
95
|
+
"/api/artifacts/bulk",
|
|
96
|
+
"/api/artifacts/zip",
|
|
97
|
+
"/api/artifacts/file",
|
|
98
|
+
"/api/artifacts/preview",
|
|
99
|
+
"/api/logs",
|
|
100
|
+
"/api/logs/clear",
|
|
101
|
+
"/api/diagnostics",
|
|
102
|
+
"/api/diagnostics/bundle",
|
|
103
|
+
"/api/runtime/restart",
|
|
104
|
+
]);
|
|
105
|
+
export function peerProxyCoverage() {
|
|
106
|
+
const implemented = [];
|
|
107
|
+
const localOnly = [];
|
|
108
|
+
const missing = [];
|
|
109
|
+
for (const route of WEB_API_ROUTE_DEFINITIONS) {
|
|
110
|
+
for (const method of route.methods) {
|
|
111
|
+
const key = { method, path: route.path };
|
|
112
|
+
if (IMPLEMENTED_ROUTE_PATHS.has(route.path)) {
|
|
113
|
+
implemented.push(key);
|
|
114
|
+
}
|
|
115
|
+
else if (LOCAL_ONLY_ROUTE_PATHS.has(route.path)) {
|
|
116
|
+
localOnly.push(key);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
missing.push(key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { implemented, localOnly, missing };
|
|
124
|
+
}
|
|
125
|
+
export function isPeerProxyLocalOnlyPath(path) {
|
|
126
|
+
return LOCAL_ONLY_ROUTE_PATHS.has(path);
|
|
127
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
2
|
-
import { getDiscordRateLimitMetrics } from "
|
|
3
|
-
import { getSlackRateLimitMetrics } from "
|
|
4
|
-
import { getTelegramRateLimitMetrics } from "
|
|
2
|
+
import { getDiscordRateLimitMetrics } from "../channels/discord/discord-rate-limit.js";
|
|
3
|
+
import { getSlackRateLimitMetrics } from "../channels/slack/slack-rate-limit.js";
|
|
4
|
+
import { getTelegramRateLimitMetrics } from "../channels/telegram/telegram-rate-limit.js";
|
|
5
|
+
import { getWebApiPerformanceMetrics } from "../web/web-performance.js";
|
|
5
6
|
const startedAt = Date.now();
|
|
6
7
|
const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
|
|
7
8
|
eventLoopDelay.enable();
|
|
@@ -38,6 +39,7 @@ export function buildRuntimeMetrics(input) {
|
|
|
38
39
|
discord: getDiscordRateLimitMetrics(),
|
|
39
40
|
slack: getSlackRateLimitMetrics(),
|
|
40
41
|
},
|
|
42
|
+
web: getWebApiPerformanceMetrics(),
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
function processMetrics() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { collectRecentWorkspaceArtifacts, createArtifactZipBundle, getArtifactTurnReport, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "
|
|
3
|
+
import { collectRecentWorkspaceArtifacts, createArtifactZipBundle, getArtifactTurnReport, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "../artifacts/artifacts.js";
|
|
4
4
|
const MAX_TEXT_PREVIEW_BYTES = 256 * 1024;
|
|
5
5
|
export class RelayArtifactService {
|
|
6
6
|
config;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "../agents/claude-code/claude-code-auth.js";
|
|
2
|
+
import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "../agents/codex/codex-auth.js";
|
|
3
|
+
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "../agents/hermes/hermes-auth.js";
|
|
4
|
+
import { checkOpenClawAuthStatus } from "../agents/openclaw/openclaw-auth.js";
|
|
5
|
+
import { checkPiAuthStatus } from "../agents/pi/pi-auth.js";
|
|
6
|
+
export class RelayAuthService {
|
|
7
|
+
config;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
}
|
|
11
|
+
async check(info) {
|
|
12
|
+
if (info.agentId === "pi") {
|
|
13
|
+
return checkPiAuthStatus(info.model);
|
|
14
|
+
}
|
|
15
|
+
if (info.agentId === "hermes") {
|
|
16
|
+
return checkHermesAuthStatus({
|
|
17
|
+
baseUrl: this.config.hermesApiBaseUrl,
|
|
18
|
+
apiKey: this.config.hermesApiKey,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (info.agentId === "openclaw") {
|
|
22
|
+
return checkOpenClawAuthStatus({
|
|
23
|
+
gatewayUrl: this.config.openClawGatewayUrl,
|
|
24
|
+
token: this.config.openClawGatewayToken,
|
|
25
|
+
password: this.config.openClawGatewayPassword,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (info.agentId === "claude-code") {
|
|
29
|
+
return checkClaudeCodeAuthStatus(this.config.claudeCodeCliPath);
|
|
30
|
+
}
|
|
31
|
+
return checkAuthStatus(this.config.codexApiKey);
|
|
32
|
+
}
|
|
33
|
+
async startLogin(info) {
|
|
34
|
+
if (info.agentId === "hermes") {
|
|
35
|
+
return startHermesLogin(this.config.hermesCliPath);
|
|
36
|
+
}
|
|
37
|
+
if (info.agentId === "claude-code") {
|
|
38
|
+
return startClaudeCodeLogin(this.config.claudeCodeCliPath);
|
|
39
|
+
}
|
|
40
|
+
if (info.agentId === "codex") {
|
|
41
|
+
return startCodexLogin();
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
message: `${info.agentLabel} login is not managed by NordRelay. Run the agent login flow on the host.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async startLogout(info) {
|
|
49
|
+
if (info.agentId === "hermes") {
|
|
50
|
+
return startHermesLogout(this.config.hermesCliPath);
|
|
51
|
+
}
|
|
52
|
+
if (info.agentId === "claude-code") {
|
|
53
|
+
return startClaudeCodeLogout(this.config.claudeCodeCliPath);
|
|
54
|
+
}
|
|
55
|
+
if (info.agentId === "codex") {
|
|
56
|
+
return startCodexLogout();
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
message: `${info.agentLabel} logout is not managed by NordRelay. Run the agent logout flow on the host.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { enabledAgents } from "../agents/shared/agent-factory.js";
|
|
2
|
+
import { listAgentAdapterDescriptors } from "../agents/shared/agent-adapter.js";
|
|
3
|
+
import { friendlyErrorText } from "../core/error-messages.js";
|
|
4
|
+
import { getAgentDiagnostics } from "../agents/shared/agent-activity.js";
|
|
5
|
+
import { getConnectorHealth, getVersionChecks, readConnectorState } from "../support/operations.js";
|
|
6
|
+
import { collectSlackDiagnostics } from "../channels/slack/slack-diagnostics.js";
|
|
7
|
+
import { getSlackRateLimitMetrics } from "../channels/slack/slack-rate-limit.js";
|
|
8
|
+
import { cliHealthForAgent, versionCheckForAgent } from "./relay-runtime-helpers.js";
|
|
9
|
+
export class RelayDashboardService {
|
|
10
|
+
options;
|
|
11
|
+
keys = ["version", "adapterHealth", "diagnostics"];
|
|
12
|
+
warmTimer;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
options.cache.register("version", () => this.produceVersion());
|
|
16
|
+
options.cache.register("adapterHealth", () => this.produceAdapterHealth());
|
|
17
|
+
options.cache.register("diagnostics", () => this.produceDiagnostics());
|
|
18
|
+
}
|
|
19
|
+
startBackgroundRefresh() {
|
|
20
|
+
this.options.cache.warm(this.keys);
|
|
21
|
+
const ttlMs = this.options.config.dashboardCacheTtlMs;
|
|
22
|
+
if (ttlMs <= 0 || this.warmTimer) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const intervalMs = Math.max(5_000, ttlMs);
|
|
26
|
+
this.warmTimer = setInterval(() => this.options.cache.warm(this.keys), intervalMs);
|
|
27
|
+
this.warmTimer.unref?.();
|
|
28
|
+
}
|
|
29
|
+
stopBackgroundRefresh() {
|
|
30
|
+
if (!this.warmTimer) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
clearInterval(this.warmTimer);
|
|
34
|
+
this.warmTimer = undefined;
|
|
35
|
+
}
|
|
36
|
+
async version() {
|
|
37
|
+
return this.cached("version");
|
|
38
|
+
}
|
|
39
|
+
async diagnostics() {
|
|
40
|
+
return this.cached("diagnostics");
|
|
41
|
+
}
|
|
42
|
+
async adapterHealth() {
|
|
43
|
+
return this.cached("adapterHealth");
|
|
44
|
+
}
|
|
45
|
+
invalidate(key) {
|
|
46
|
+
this.options.cache.invalidate(key);
|
|
47
|
+
if (key) {
|
|
48
|
+
this.options.cache.warm([key]);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.options.cache.warm(this.keys);
|
|
52
|
+
}
|
|
53
|
+
async cached(key) {
|
|
54
|
+
return (await this.options.cache.get(key, this.options.config.dashboardCacheTtlMs)).value;
|
|
55
|
+
}
|
|
56
|
+
async produceVersion() {
|
|
57
|
+
const cliOptions = this.options.cliPathOptions();
|
|
58
|
+
const [health, state, versionChecks] = await Promise.all([
|
|
59
|
+
getConnectorHealth(cliOptions),
|
|
60
|
+
readConnectorState(),
|
|
61
|
+
getVersionChecks(cliOptions),
|
|
62
|
+
]);
|
|
63
|
+
return {
|
|
64
|
+
health,
|
|
65
|
+
state,
|
|
66
|
+
versionChecks,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async produceDiagnostics() {
|
|
70
|
+
const cliOptions = this.options.cliPathOptions();
|
|
71
|
+
const [health, versionChecks, snapshot, session] = await Promise.all([
|
|
72
|
+
getConnectorHealth(cliOptions),
|
|
73
|
+
getVersionChecks(cliOptions),
|
|
74
|
+
this.options.snapshot(),
|
|
75
|
+
this.options.getSession(),
|
|
76
|
+
]);
|
|
77
|
+
return {
|
|
78
|
+
health,
|
|
79
|
+
versionChecks,
|
|
80
|
+
snapshot,
|
|
81
|
+
runtime: {
|
|
82
|
+
stateBackend: this.options.config.stateBackend,
|
|
83
|
+
sourceWorkspace: this.options.config.workspace,
|
|
84
|
+
queuePaused: this.options.queuePaused(),
|
|
85
|
+
externalMirror: this.options.externalMirror(),
|
|
86
|
+
agentDiagnostics: getAgentDiagnostics(session, this.options.config),
|
|
87
|
+
slackDiagnostics: await collectSlackDiagnostics({
|
|
88
|
+
config: this.options.config,
|
|
89
|
+
timeoutMs: 2_500,
|
|
90
|
+
rateLimit: getSlackRateLimitMetrics(),
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async produceAdapterHealth() {
|
|
96
|
+
const cliOptions = this.options.cliPathOptions();
|
|
97
|
+
const [health, versions] = await Promise.all([
|
|
98
|
+
getConnectorHealth(cliOptions),
|
|
99
|
+
getVersionChecks(cliOptions),
|
|
100
|
+
]);
|
|
101
|
+
return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
|
|
102
|
+
const enabled = enabledAgents(this.options.config).includes(descriptor.id);
|
|
103
|
+
const auth = descriptor.capabilities.auth && enabled
|
|
104
|
+
? await this.options.authStatus(descriptor.id).catch((error) => ({
|
|
105
|
+
agentId: descriptor.id,
|
|
106
|
+
agentLabel: descriptor.label,
|
|
107
|
+
supported: descriptor.capabilities.auth,
|
|
108
|
+
authenticated: false,
|
|
109
|
+
detail: friendlyErrorText(error),
|
|
110
|
+
loginSupported: descriptor.capabilities.login,
|
|
111
|
+
logoutSupported: descriptor.capabilities.logout,
|
|
112
|
+
}))
|
|
113
|
+
: null;
|
|
114
|
+
const cli = cliHealthForAgent(descriptor.id, health);
|
|
115
|
+
const version = versionCheckForAgent(descriptor.id, versions);
|
|
116
|
+
return {
|
|
117
|
+
id: descriptor.id,
|
|
118
|
+
label: descriptor.label,
|
|
119
|
+
enabled,
|
|
120
|
+
status: descriptor.status === "available" ? (enabled ? "enabled" : "disabled") : "planned",
|
|
121
|
+
auth: {
|
|
122
|
+
supported: descriptor.capabilities.auth,
|
|
123
|
+
authenticated: auth ? auth.authenticated : null,
|
|
124
|
+
method: auth?.method,
|
|
125
|
+
detail: auth?.detail,
|
|
126
|
+
},
|
|
127
|
+
cli,
|
|
128
|
+
version: {
|
|
129
|
+
installed: version.installedLabel,
|
|
130
|
+
latest: version.latestVersion,
|
|
131
|
+
status: version.status,
|
|
132
|
+
detail: version.detail,
|
|
133
|
+
},
|
|
134
|
+
capabilities: descriptor.capabilities,
|
|
135
|
+
notes: descriptor.notes,
|
|
136
|
+
};
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
}
|