@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.
- package/.env.example +52 -0
- package/README.md +171 -50
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/adapter-conformance.js +61 -0
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +95 -37
- package/dist/channel-adapter.js +44 -11
- package/dist/channel-command-catalog.js +94 -0
- package/dist/channel-command-core.js +60 -0
- package/dist/channel-command-service.js +230 -1
- package/dist/channel-mirror-registry.js +84 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-prompt-engine.js +177 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-lifecycle.js +73 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +82 -8
- package/dist/config.js +79 -7
- package/dist/context-key.js +42 -0
- package/dist/discord-bot.js +173 -342
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +29 -0
- package/dist/metrics.js +48 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +288 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-readiness.js +77 -0
- package/dist/peer-runtime-service.js +658 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +307 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +210 -0
- package/dist/relay-runtime.js +79 -274
- package/dist/remote-prompt.js +98 -0
- package/dist/settings-wizard-test.js +216 -0
- package/dist/slack-artifacts.js +165 -0
- package/dist/slack-bot.js +1461 -0
- package/dist/slack-channel-runtime.js +147 -0
- package/dist/slack-command-surface.js +46 -0
- package/dist/slack-diagnostics.js +116 -0
- package/dist/slack-rate-limit.js +139 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-general-commands.js +14 -0
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/user-management-crypto.js +38 -0
- package/dist/user-management-normalize.js +188 -0
- package/dist/user-management-types.js +1 -0
- package/dist/user-management.js +193 -196
- package/dist/web-api-contract.js +16 -0
- package/dist/web-dashboard-access-routes.js +62 -0
- package/dist/web-dashboard-assets.js +1 -0
- package/dist/web-dashboard-pages.js +26 -4
- package/dist/web-dashboard-peer-routes.js +225 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +46 -0
- package/dist/web-state.js +2 -2
- package/dist/webui-assets/dashboard.css +193 -0
- package/dist/webui-assets/dashboard.js +870 -57
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +468 -11
|
@@ -123,6 +123,7 @@ export function renderDashboardApp() {
|
|
|
123
123
|
</div>
|
|
124
124
|
<div class="header-actions">
|
|
125
125
|
<span id="connectionStatus" class="badge">Connecting</span>
|
|
126
|
+
<select id="peerSelect" title="NordRelay target"></select>
|
|
126
127
|
<select id="agentSelect"></select>
|
|
127
128
|
<button id="themeBtn" class="secondary" title="Toggle dark theme">Dark</button>
|
|
128
129
|
<button id="refreshBtn">Refresh</button>
|
|
@@ -208,7 +209,7 @@ export function renderDashboardApp() {
|
|
|
208
209
|
|
|
209
210
|
<section class="page" id="page-activity">
|
|
210
211
|
<div class="panel">
|
|
211
|
-
<div class="row"><select id="activitySource"><option value="all">All sources</option><option value="web">Web</option><option value="telegram">Telegram</option><option value="discord">Discord</option><option value="cli">CLI</option></select><select id="activityCategory"><option value="all">All categories</option><option value="prompt">Prompt</option><option value="session">Session</option><option value="queue">Queue</option><option value="agent-update">Agent update</option><option value="artifact">Artifact</option><option value="system">System</option><option value="auth">Auth</option><option value="security">Security</option><option value="tool">Tool</option></select><select id="activityStatus"><option value="all">All statuses</option><option value="queued">Queued</option><option value="running">Running</option><option value="completed">Completed</option><option value="failed">Failed</option><option value="aborted">Aborted</option><option value="info">Info</option></select><input id="activityActor" placeholder="Actor"><input id="activityAgent" placeholder="Agent"><input id="activityThread" placeholder="Thread ID"><input id="activityWorkspace" placeholder="Workspace"><input id="activityType" placeholder="Type"><input id="activitySince" type="datetime-local"><input id="activityLimit" type="number" value="100" min="1" max="500"><button id="loadActivityBtn">Load activity</button><button id="exportActivityBtn" class="secondary">Export</button></div>
|
|
212
|
+
<div class="row"><select id="activitySource"><option value="all">All sources</option><option value="web">Web</option><option value="telegram">Telegram</option><option value="discord">Discord</option><option value="slack">Slack</option><option value="cli">CLI</option></select><select id="activityCategory"><option value="all">All categories</option><option value="prompt">Prompt</option><option value="session">Session</option><option value="queue">Queue</option><option value="agent-update">Agent update</option><option value="artifact">Artifact</option><option value="system">System</option><option value="auth">Auth</option><option value="security">Security</option><option value="tool">Tool</option></select><select id="activityStatus"><option value="all">All statuses</option><option value="queued">Queued</option><option value="running">Running</option><option value="completed">Completed</option><option value="failed">Failed</option><option value="aborted">Aborted</option><option value="info">Info</option></select><input id="activityActor" placeholder="Actor"><input id="activityAgent" placeholder="Agent"><input id="activityThread" placeholder="Thread ID"><input id="activityWorkspace" placeholder="Workspace"><input id="activityType" placeholder="Type"><input id="activitySince" type="datetime-local"><input id="activityLimit" type="number" value="100" min="1" max="500"><button id="loadActivityBtn">Load activity</button><button id="exportActivityBtn" class="secondary">Export</button></div>
|
|
212
213
|
<div id="activityList" class="list"></div>
|
|
213
214
|
</div>
|
|
214
215
|
</section>
|
|
@@ -225,17 +226,31 @@ export function renderDashboardApp() {
|
|
|
225
226
|
<div class="panel">
|
|
226
227
|
<div class="row"><button id="reloadAdaptersBtn">Reload adapters</button></div>
|
|
227
228
|
<div id="adapterHealth" class="list"></div>
|
|
229
|
+
<h2 class="task-section-title">Adapter Conformance</h2>
|
|
230
|
+
<div id="adapterConformance" class="list"></div>
|
|
231
|
+
</div>
|
|
232
|
+
</section>
|
|
233
|
+
|
|
234
|
+
<section class="page" id="page-peers">
|
|
235
|
+
<div class="panel">
|
|
236
|
+
<div class="row"><button id="loadPeersBtn">Reload peers</button><button id="createPeerInviteBtn">Create invite</button><button id="addPeerBtn" class="secondary">Add peer</button></div>
|
|
237
|
+
<div id="peerStatus" class="list"></div>
|
|
238
|
+
<h2>Configured peers</h2>
|
|
239
|
+
<div id="peersList" class="list"></div>
|
|
240
|
+
<h2>Open invitations</h2>
|
|
241
|
+
<div id="peerInvites" class="list"></div>
|
|
228
242
|
</div>
|
|
229
243
|
</section>
|
|
230
244
|
|
|
231
245
|
<section class="page" id="page-access">
|
|
232
246
|
<div class="panel">
|
|
233
|
-
<div class="row"><button id="loadAccessBtn">Reload users</button><button id="createUserBtn">Create user</button><button id="createGroupBtn" class="secondary">Create group</button><button id="createChatBtn" class="secondary">Add Telegram chat</button><button id="createDiscordChannelBtn" class="secondary">Add Discord channel</button><button id="lockSessionBtn" class="secondary">Lock web session</button><button id="unlockSessionBtn" class="secondary">Unlock web session</button></div>
|
|
247
|
+
<div class="row"><button id="loadAccessBtn">Reload users</button><button id="createUserBtn">Create user</button><button id="createGroupBtn" class="secondary">Create group</button><button id="createChatBtn" class="secondary">Add Telegram chat</button><button id="createDiscordChannelBtn" class="secondary">Add Discord channel</button><button id="createSlackChannelBtn" class="secondary">Add Slack channel</button><button id="lockSessionBtn" class="secondary">Lock web session</button><button id="unlockSessionBtn" class="secondary">Unlock web session</button></div>
|
|
234
248
|
<div id="accessTabs" class="tabs access-tabs">
|
|
235
249
|
<button type="button" data-access-tab="users" class="active">Users</button>
|
|
236
250
|
<button type="button" data-access-tab="groups">Groups</button>
|
|
237
251
|
<button type="button" data-access-tab="telegram">Telegram</button>
|
|
238
252
|
<button type="button" data-access-tab="discord">Discord</button>
|
|
253
|
+
<button type="button" data-access-tab="slack">Slack</button>
|
|
239
254
|
<button type="button" data-access-tab="locks">Locks</button>
|
|
240
255
|
<button type="button" data-access-tab="audit">Audit</button>
|
|
241
256
|
</div>
|
|
@@ -257,13 +272,20 @@ export function renderDashboardApp() {
|
|
|
257
272
|
</div>
|
|
258
273
|
<div id="discordChannelsList" class="list"></div>
|
|
259
274
|
</div>
|
|
275
|
+
<div class="access-tab" data-access-tab-panel="slack">
|
|
276
|
+
<div class="access-tab-heading">
|
|
277
|
+
<h2>Slack channels</h2>
|
|
278
|
+
<input id="slackChannelSearch" placeholder="Search Slack channels">
|
|
279
|
+
</div>
|
|
280
|
+
<div id="slackChannelsList" class="list"></div>
|
|
281
|
+
</div>
|
|
260
282
|
<div class="access-tab" data-access-tab-panel="locks">
|
|
261
283
|
<h2>Locks</h2>
|
|
262
284
|
<div id="locksList" class="list"></div>
|
|
263
285
|
</div>
|
|
264
286
|
<div class="access-tab" data-access-tab-panel="audit">
|
|
265
287
|
<h2>Audit</h2>
|
|
266
|
-
<div class="row"><select id="auditChannel"><option value="all">All channels</option><option value="web">Web</option><option value="telegram">Telegram</option><option value="discord">Discord</option></select><select id="auditCategory"><option value="all">All categories</option><option value="prompt">Prompt</option><option value="session">Session</option><option value="queue">Queue</option><option value="agent-update">Agent update</option><option value="artifact">Artifact</option><option value="system">System</option><option value="auth">Auth</option><option value="security">Security</option><option value="tool">Tool</option></select><select id="auditStatus"><option value="all">All statuses</option><option value="ok">OK</option><option value="failed">Failed</option><option value="denied">Denied</option></select><input id="auditActor" placeholder="Actor"><input id="auditAgent" placeholder="Agent"><input id="auditThread" placeholder="Thread ID"><input id="auditWorkspace" placeholder="Workspace"><input id="auditSince" type="datetime-local"><input id="auditLimit" type="number" value="50" min="1" max="500"><button id="loadAuditBtn">Load audit</button><button id="exportAuditBtn" class="secondary">Export</button></div>
|
|
288
|
+
<div class="row"><select id="auditChannel"><option value="all">All channels</option><option value="web">Web</option><option value="telegram">Telegram</option><option value="discord">Discord</option><option value="slack">Slack</option></select><select id="auditCategory"><option value="all">All categories</option><option value="prompt">Prompt</option><option value="session">Session</option><option value="queue">Queue</option><option value="agent-update">Agent update</option><option value="artifact">Artifact</option><option value="system">System</option><option value="auth">Auth</option><option value="security">Security</option><option value="tool">Tool</option></select><select id="auditStatus"><option value="all">All statuses</option><option value="ok">OK</option><option value="failed">Failed</option><option value="denied">Denied</option></select><input id="auditActor" placeholder="Actor"><input id="auditAgent" placeholder="Agent"><input id="auditThread" placeholder="Thread ID"><input id="auditWorkspace" placeholder="Workspace"><input id="auditSince" type="datetime-local"><input id="auditLimit" type="number" value="50" min="1" max="500"><button id="loadAuditBtn">Load audit</button><button id="exportAuditBtn" class="secondary">Export</button></div>
|
|
267
289
|
<div id="auditList" class="list"></div>
|
|
268
290
|
</div>
|
|
269
291
|
</div>
|
|
@@ -280,7 +302,7 @@ export function renderDashboardApp() {
|
|
|
280
302
|
|
|
281
303
|
<section class="page" id="page-settings">
|
|
282
304
|
<div class="panel">
|
|
283
|
-
<div class="row"><button id="saveSettingsBtn">Save settings</button><button id="restartBtn" class="secondary">Restart NordRelay</button><span id="settingsStatus"></span></div>
|
|
305
|
+
<div class="row"><button id="saveSettingsBtn">Save settings</button><button id="settingsWizardBtn" class="secondary">Setup wizard</button><button id="restartBtn" class="secondary">Restart NordRelay</button><span id="settingsStatus"></span></div>
|
|
284
306
|
<div id="settingsTabs" class="tabs"></div>
|
|
285
307
|
<div id="settingsForm" class="settings-grid"></div>
|
|
286
308
|
</div>
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { isPermission } from "./access-control.js";
|
|
2
|
+
import { AGENT_IDS, isAgentId } from "./agent.js";
|
|
3
|
+
import { ensurePeerTlsFiles, loadOrCreatePeerIdentity, } from "./peer-identity.js";
|
|
4
|
+
import { checkPeerEndpoint, pairPeer, RemoteRelayClient } from "./peer-client.js";
|
|
5
|
+
import { buildPeerReadiness, peerListenUrl } from "./peer-readiness.js";
|
|
6
|
+
import { PeerStore } from "./peer-store.js";
|
|
7
|
+
import { publicPeer } from "./peer-types.js";
|
|
8
|
+
import { arrayStringField, objectRecord, optionalBooleanField, optionalNumberField, optionalStringField, readJsonBody, sendJson, } from "./web-dashboard-http.js";
|
|
9
|
+
export async function handleDashboardPeerRoute(req, res, url, options) {
|
|
10
|
+
const store = new PeerStore(options.home);
|
|
11
|
+
const identity = loadOrCreatePeerIdentity(options.home, options.config.peerName);
|
|
12
|
+
const tls = options.config.peerTlsEnabled ? ensurePeerTlsFiles(options.home, identity.public) : null;
|
|
13
|
+
if (req.method === "GET" && url.pathname === "/api/peers") {
|
|
14
|
+
const readiness = await buildPeerReadiness(options.config);
|
|
15
|
+
sendJson(res, 200, store.snapshot(identity.public, {
|
|
16
|
+
enabled: options.config.peerEnabled,
|
|
17
|
+
listenUrl: readiness.listenUrl,
|
|
18
|
+
requireTls: options.config.peerRequireTls,
|
|
19
|
+
readiness,
|
|
20
|
+
}));
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (req.method === "POST" && url.pathname === "/api/peers/invite") {
|
|
24
|
+
const body = await readJsonBody(req);
|
|
25
|
+
const readiness = await buildPeerReadiness(options.config);
|
|
26
|
+
const created = store.createInvitation({
|
|
27
|
+
name: optionalStringField(body, "name"),
|
|
28
|
+
expiresInMs: (optionalNumberField(body, "expiresMinutes") ?? 10) * 60 * 1000,
|
|
29
|
+
scopes: parseScopes(arrayStringField(body, "scopes")),
|
|
30
|
+
allowedAgents: parseAgents(arrayStringField(body, "allowedAgents")),
|
|
31
|
+
allowedWorkspaceRoots: arrayStringField(body, "allowedWorkspaceRoots"),
|
|
32
|
+
workspaceAliases: parseWorkspaceAliases(body.workspaceAliases),
|
|
33
|
+
});
|
|
34
|
+
const listenUrl = readiness.listenUrl;
|
|
35
|
+
const command = `nordrelay peer add ${listenUrl} --code ${created.code}`;
|
|
36
|
+
sendJson(res, 201, {
|
|
37
|
+
invitation: created.invitation,
|
|
38
|
+
code: created.code,
|
|
39
|
+
url: listenUrl,
|
|
40
|
+
fingerprint: identity.public.fingerprint,
|
|
41
|
+
tlsFingerprint: tls?.fingerprint,
|
|
42
|
+
command,
|
|
43
|
+
readiness,
|
|
44
|
+
warnings: readiness.warnings,
|
|
45
|
+
});
|
|
46
|
+
options.auditPeerAction?.("peer_invite_created", created.invitation.name);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (req.method === "POST" && url.pathname === "/api/peers/probe") {
|
|
50
|
+
const body = await readJsonBody(req);
|
|
51
|
+
const readiness = await buildPeerReadiness(options.config);
|
|
52
|
+
const peerId = optionalStringField(body, "peerId");
|
|
53
|
+
if (peerId) {
|
|
54
|
+
const probe = await new RemoteRelayClient(store).rpc(peerId, "peer.probe", {}, options.activityActor);
|
|
55
|
+
sendJson(res, 200, { type: "remote", peerId, readiness, probe });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const expectedTlsFingerprint = options.config.peerPublicUrl ? undefined : tls?.fingerprint;
|
|
59
|
+
const probe = await checkPeerEndpoint(readiness.listenUrl, { expectedTlsFingerprint });
|
|
60
|
+
sendJson(res, 200, { type: "local", readiness, probe });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const invitationMatch = url.pathname.match(/^\/api\/peers\/invitations\/([^/]+)$/);
|
|
64
|
+
if (invitationMatch?.[1] && req.method === "DELETE") {
|
|
65
|
+
const invitation = store.deleteInvitation(decodeURIComponent(invitationMatch[1]));
|
|
66
|
+
sendJson(res, 200, { removed: Boolean(invitation), invitation });
|
|
67
|
+
if (invitation) {
|
|
68
|
+
options.auditPeerAction?.("peer_invite_deleted", `${invitation.name} (${invitation.id})`);
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (req.method === "POST" && (url.pathname === "/api/peers" || url.pathname === "/api/peers/pair")) {
|
|
73
|
+
const body = await readJsonBody(req);
|
|
74
|
+
const result = await pairPeer({
|
|
75
|
+
url: requiredString(body, "url"),
|
|
76
|
+
code: requiredString(body, "code"),
|
|
77
|
+
name: optionalStringField(body, "name"),
|
|
78
|
+
publicUrl: optionalStringField(body, "publicUrl"),
|
|
79
|
+
}, identity, store);
|
|
80
|
+
sendJson(res, 201, { peer: publicPeer(result.peer), tlsFingerprint: result.tlsFingerprint });
|
|
81
|
+
options.auditPeerAction?.("peer_paired", `${result.peer.name} (${result.peer.id})`);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
const peerMatch = url.pathname.match(/^\/api\/peers\/([^/]+)$/);
|
|
85
|
+
if (peerMatch?.[1] && req.method === "PATCH") {
|
|
86
|
+
const body = await readJsonBody(req);
|
|
87
|
+
const peer = store.updatePeer(decodeURIComponent(peerMatch[1]), {
|
|
88
|
+
name: optionalStringField(body, "name"),
|
|
89
|
+
url: optionalStringField(body, "url"),
|
|
90
|
+
enabled: optionalBooleanField(body, "enabled"),
|
|
91
|
+
scopes: body.scopes === undefined ? undefined : parseScopes(arrayStringField(body, "scopes")),
|
|
92
|
+
allowedAgents: body.allowedAgents === undefined ? undefined : parseAgents(arrayStringField(body, "allowedAgents")),
|
|
93
|
+
allowedWorkspaceRoots: body.allowedWorkspaceRoots === undefined ? undefined : arrayStringField(body, "allowedWorkspaceRoots"),
|
|
94
|
+
workspaceAliases: body.workspaceAliases === undefined ? undefined : parseWorkspaceAliases(body.workspaceAliases),
|
|
95
|
+
});
|
|
96
|
+
sendJson(res, 200, { peer: publicPeer(peer) });
|
|
97
|
+
options.auditPeerAction?.("peer_updated", `${peer.name} (${peer.id})`);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (req.method === "GET" && url.pathname === "/api/peers/global-sessions") {
|
|
101
|
+
const query = optionalStringField(Object.fromEntries(url.searchParams), "query") ?? "";
|
|
102
|
+
const agent = parseAgent(optionalStringField(Object.fromEntries(url.searchParams), "agent"));
|
|
103
|
+
const limit = Math.min(Math.max(Number(url.searchParams.get("limit") ?? 50), 1), 50);
|
|
104
|
+
const client = new RemoteRelayClient(store);
|
|
105
|
+
const targets = [];
|
|
106
|
+
if (options.runtime) {
|
|
107
|
+
targets.push({
|
|
108
|
+
peerId: "local",
|
|
109
|
+
peerName: "Local",
|
|
110
|
+
ok: true,
|
|
111
|
+
data: await options.runtime.listSessionsPage(1, limit, query, agent),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const peers = store.listPublic().filter((peer) => peer.enabled && peer.url);
|
|
115
|
+
const remoteTargets = await Promise.all(peers.map(async (peer) => {
|
|
116
|
+
try {
|
|
117
|
+
const data = await client.webProxy(peer.id, {
|
|
118
|
+
method: "GET",
|
|
119
|
+
path: "/api/sessions",
|
|
120
|
+
query: { query, page: 1, limit, agent },
|
|
121
|
+
body: {},
|
|
122
|
+
contextKey: "web:dashboard",
|
|
123
|
+
}, options.activityActor, "web:dashboard");
|
|
124
|
+
return { peerId: peer.id, peerName: peer.name, ok: true, data };
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return { peerId: peer.id, peerName: peer.name, ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
sendJson(res, 200, { targets: [...targets, ...remoteTargets] });
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (peerMatch?.[1] && req.method === "DELETE") {
|
|
134
|
+
const peerId = decodeURIComponent(peerMatch[1]);
|
|
135
|
+
const removed = store.revokePeer(peerId);
|
|
136
|
+
sendJson(res, 200, { removed });
|
|
137
|
+
if (removed) {
|
|
138
|
+
options.auditPeerAction?.("peer_revoked", peerId);
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
const proxyMatch = url.pathname.match(/^\/api\/peers\/([^/]+)\/proxy$/);
|
|
143
|
+
if (proxyMatch?.[1] && req.method === "POST") {
|
|
144
|
+
const body = await readJsonBody(req);
|
|
145
|
+
const payload = parseProxyPayload(body);
|
|
146
|
+
const data = await new RemoteRelayClient(store).webProxy(decodeURIComponent(proxyMatch[1]), payload, options.activityActor, payload.contextKey);
|
|
147
|
+
sendJson(res, 200, data);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
const healthMatch = url.pathname.match(/^\/api\/peers\/([^/]+)\/health$/);
|
|
151
|
+
if (healthMatch?.[1] && req.method === "GET") {
|
|
152
|
+
const peerId = decodeURIComponent(healthMatch[1]);
|
|
153
|
+
const data = await new RemoteRelayClient(store).rpc(peerId, "peer.ping", undefined, options.activityActor);
|
|
154
|
+
sendJson(res, 200, { data, peer: publicPeer(store.get(peerId)) });
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const eventsMatch = url.pathname.match(/^\/api\/peers\/([^/]+)\/events$/);
|
|
158
|
+
if (eventsMatch?.[1] && req.method === "GET") {
|
|
159
|
+
res.writeHead(200, {
|
|
160
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
161
|
+
"cache-control": "no-cache, no-transform",
|
|
162
|
+
connection: "keep-alive",
|
|
163
|
+
});
|
|
164
|
+
const sourceContextKey = url.searchParams.get("contextKey") || undefined;
|
|
165
|
+
const subscription = new RemoteRelayClient(store).subscribe(decodeURIComponent(eventsMatch[1]), (event) => {
|
|
166
|
+
if (res.destroyed || res.writableEnded)
|
|
167
|
+
return;
|
|
168
|
+
res.write(`event: ${event.type}\n`);
|
|
169
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
170
|
+
}, (error) => {
|
|
171
|
+
if (!res.destroyed && !res.writableEnded) {
|
|
172
|
+
res.write("event: status\n");
|
|
173
|
+
res.write(`data: ${JSON.stringify({ type: "status", level: "error", message: error.message, at: new Date().toISOString() })}\n\n`);
|
|
174
|
+
}
|
|
175
|
+
}, sourceContextKey);
|
|
176
|
+
const heartbeat = setInterval(() => {
|
|
177
|
+
if (!res.destroyed && !res.writableEnded)
|
|
178
|
+
res.write(": heartbeat\n\n");
|
|
179
|
+
}, 25_000);
|
|
180
|
+
heartbeat.unref?.();
|
|
181
|
+
req.on("close", () => {
|
|
182
|
+
clearInterval(heartbeat);
|
|
183
|
+
subscription.close();
|
|
184
|
+
});
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
function parseScopes(values) {
|
|
190
|
+
return values.filter(isPermission);
|
|
191
|
+
}
|
|
192
|
+
function parseAgents(values) {
|
|
193
|
+
const parsed = values.filter(isAgentId);
|
|
194
|
+
return parsed.length > 0 ? parsed : [...AGENT_IDS];
|
|
195
|
+
}
|
|
196
|
+
function parseAgent(value) {
|
|
197
|
+
return value && isAgentId(value) ? value : undefined;
|
|
198
|
+
}
|
|
199
|
+
function parseProxyPayload(body) {
|
|
200
|
+
return {
|
|
201
|
+
method: requiredString(body, "method"),
|
|
202
|
+
path: requiredString(body, "path"),
|
|
203
|
+
query: objectRecord(body.query),
|
|
204
|
+
body: objectRecord(body.body),
|
|
205
|
+
contextKey: optionalStringField(body, "contextKey"),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function parseWorkspaceAliases(value) {
|
|
209
|
+
if (typeof value === "string") {
|
|
210
|
+
return Object.fromEntries(value.split(",").map((item) => {
|
|
211
|
+
const [alias, workspace] = item.split("=", 2);
|
|
212
|
+
return [alias?.trim() ?? "", workspace?.trim() ?? ""];
|
|
213
|
+
}).filter(([alias, workspace]) => alias && workspace));
|
|
214
|
+
}
|
|
215
|
+
const record = objectRecord(value);
|
|
216
|
+
return Object.fromEntries(Object.entries(record).filter((entry) => typeof entry[1] === "string" && entry[0].trim().length > 0 && entry[1].trim().length > 0));
|
|
217
|
+
}
|
|
218
|
+
function requiredString(body, key) {
|
|
219
|
+
const value = body[key];
|
|
220
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
221
|
+
if (!text) {
|
|
222
|
+
throw new Error(`${key} is required.`);
|
|
223
|
+
}
|
|
224
|
+
return text;
|
|
225
|
+
}
|
package/dist/web-dashboard-ui.js
CHANGED
|
@@ -8,6 +8,7 @@ export const DASHBOARD_PAGES = [
|
|
|
8
8
|
{ id: "activity", label: "Activity", permission: "sessions.read" },
|
|
9
9
|
{ id: "artifacts", label: "Artifacts", permission: "files.read" },
|
|
10
10
|
{ id: "adapters", label: "Adapters", permission: "inspect" },
|
|
11
|
+
{ id: "peers", label: "Peers", permission: "peers.read" },
|
|
11
12
|
{ id: "access", label: "Users", permission: "users.read" },
|
|
12
13
|
{ id: "version", label: "Version", permission: "inspect" },
|
|
13
14
|
{ id: "settings", label: "Settings", permission: "settings.read" },
|
package/dist/web-dashboard.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { URL } from "node:url";
|
|
6
6
|
import { enabledAgents } from "./agent-factory.js";
|
|
7
|
+
import { buildAdapterConformanceMatrix } from "./adapter-conformance.js";
|
|
7
8
|
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
8
9
|
import { isAgentId } from "./agent.js";
|
|
9
10
|
import { AuditLogStore } from "./audit-log.js";
|
|
@@ -13,6 +14,7 @@ import { loadConfig } from "./config.js";
|
|
|
13
14
|
import { friendlyErrorText } from "./error-messages.js";
|
|
14
15
|
import { RelayRuntime } from "./relay-runtime.js";
|
|
15
16
|
import { resolveDashboardEnvPath, SettingsService } from "./settings-service.js";
|
|
17
|
+
import { mergeSettingsWizardTestSettings, runSettingsWizardTest } from "./settings-wizard-test.js";
|
|
16
18
|
import { UserStore, publicUser } from "./user-management.js";
|
|
17
19
|
import { handleDashboardAccessRoute } from "./web-dashboard-access-routes.js";
|
|
18
20
|
import { handleDashboardArtifactRoute } from "./web-dashboard-artifact-routes.js";
|
|
@@ -21,6 +23,7 @@ import { objectRecord, optionalStringField, parseCookies, readJsonBody, sendJson
|
|
|
21
23
|
import { renderDashboardApp, renderFirstRunSetupPage, renderLoginPage } from "./web-dashboard-pages.js";
|
|
22
24
|
import { handleDashboardRuntimeRoute } from "./web-dashboard-runtime-routes.js";
|
|
23
25
|
import { handleDashboardSessionRoute } from "./web-dashboard-session-routes.js";
|
|
26
|
+
import { handleDashboardPeerRoute } from "./web-dashboard-peer-routes.js";
|
|
24
27
|
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
25
28
|
const options = parseOptions(process.argv.slice(2));
|
|
26
29
|
const config = loadConfig();
|
|
@@ -160,6 +163,7 @@ async function handleApi(req, res, url, authUser) {
|
|
|
160
163
|
auth: currentUserDto(authUser),
|
|
161
164
|
channels: listChannelDescriptors(),
|
|
162
165
|
agentAdapters: listAgentAdapterDescriptors().filter((adapter) => users.canUseAgent(authUser, adapter.id)),
|
|
166
|
+
adapterConformance: scopedAdapterConformance(authUser),
|
|
163
167
|
enabledAgents: enabledAgents(config).filter((agentId) => users.canUseAgent(authUser, agentId)),
|
|
164
168
|
controls: scopedControlOptions(authUser, await runtime.controlOptions()),
|
|
165
169
|
status: await runtime.bootstrapStatus(),
|
|
@@ -172,6 +176,10 @@ async function handleApi(req, res, url, authUser) {
|
|
|
172
176
|
sendJson(res, 200, scopedControlOptions(authUser, await runtime.controlOptions(agentId)));
|
|
173
177
|
return;
|
|
174
178
|
}
|
|
179
|
+
if (req.method === "GET" && url.pathname === "/api/adapters/conformance") {
|
|
180
|
+
sendJson(res, 200, scopedAdapterConformance(authUser));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
175
183
|
if (await handleDashboardAccessRoute(req, res, url, {
|
|
176
184
|
users,
|
|
177
185
|
runtime,
|
|
@@ -180,6 +188,15 @@ async function handleApi(req, res, url, authUser) {
|
|
|
180
188
|
})) {
|
|
181
189
|
return;
|
|
182
190
|
}
|
|
191
|
+
if (await handleDashboardPeerRoute(req, res, url, {
|
|
192
|
+
config,
|
|
193
|
+
home: options.home,
|
|
194
|
+
runtime,
|
|
195
|
+
activityActor: webActivityActor(authUser),
|
|
196
|
+
auditPeerAction: (action, description) => auditUserAction(authUser, action, description),
|
|
197
|
+
})) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
183
200
|
if (req.method === "GET" && url.pathname === "/api/settings") {
|
|
184
201
|
sendJson(res, 200, await settings.snapshot(process.env, activeSettingsValues(config)));
|
|
185
202
|
return;
|
|
@@ -189,6 +206,11 @@ async function handleApi(req, res, url, authUser) {
|
|
|
189
206
|
sendJson(res, 200, await settings.update(objectRecord(body?.settings)));
|
|
190
207
|
return;
|
|
191
208
|
}
|
|
209
|
+
if (req.method === "POST" && url.pathname === "/api/settings/wizard/test") {
|
|
210
|
+
const body = await readJsonBody(req);
|
|
211
|
+
sendJson(res, 200, await runSettingsWizardTest(optionalStringField(body, "channel") ?? "", mergeSettingsWizardTestSettings(activeSettingsValues(config), objectRecord(body?.settings))));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
192
214
|
if (await handleDashboardSessionRoute(req, res, url, {
|
|
193
215
|
runtime,
|
|
194
216
|
authUser,
|
|
@@ -464,6 +486,13 @@ function scopedControlOptions(authUser, options) {
|
|
|
464
486
|
workspaces: options.workspaces.filter((workspace) => users.canUseWorkspace(authUser, workspace)),
|
|
465
487
|
};
|
|
466
488
|
}
|
|
489
|
+
function scopedAdapterConformance(authUser) {
|
|
490
|
+
const matrix = buildAdapterConformanceMatrix();
|
|
491
|
+
return {
|
|
492
|
+
...matrix,
|
|
493
|
+
agents: matrix.agents.filter((adapter) => users.canUseAgent(authUser, adapter.id)),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
467
496
|
function scopedSessionPage(authUser, page) {
|
|
468
497
|
return {
|
|
469
498
|
...page,
|
|
@@ -494,6 +523,8 @@ async function scopeRelayEvent(authUser, event, canUseCurrentSession = () => can
|
|
|
494
523
|
return canUseSession(authUser, event.session) ? event : null;
|
|
495
524
|
case "activity_update":
|
|
496
525
|
return { ...event, events: filterActivityByScope(authUser, event.events) };
|
|
526
|
+
case "active_sessions_update":
|
|
527
|
+
return { ...event, active: scopedActiveSessions(authUser, event.active) };
|
|
497
528
|
case "agent_update":
|
|
498
529
|
return users.canUseAgent(authUser, event.job.agentId) ? event : null;
|
|
499
530
|
case "status":
|
|
@@ -640,6 +671,21 @@ function activeSettingsValues(current) {
|
|
|
640
671
|
DISCORD_NOTIFY_MODE: current.discordNotifyMode === current.notifyMode ? "" : current.discordNotifyMode,
|
|
641
672
|
DISCORD_QUIET_HOURS: quietOverrideValue(current.discordQuietHours, current.quietHours),
|
|
642
673
|
DISCORD_AUTO_SEND_ARTIFACTS: current.discordAutoSendArtifacts === current.autoSendArtifacts ? "" : boolValue(current.discordAutoSendArtifacts),
|
|
674
|
+
SLACK_ENABLED: boolValue(current.slackEnabled),
|
|
675
|
+
SLACK_BOT_TOKEN: current.slackBotToken,
|
|
676
|
+
SLACK_APP_TOKEN: current.slackAppToken,
|
|
677
|
+
SLACK_SIGNING_SECRET: current.slackSigningSecret,
|
|
678
|
+
SLACK_SOCKET_MODE: boolValue(current.slackSocketMode),
|
|
679
|
+
SLACK_PORT: String(current.slackPort),
|
|
680
|
+
SLACK_ALLOWED_TEAM_IDS: current.slackAllowedTeamIds.join(","),
|
|
681
|
+
SLACK_ALLOWED_CHANNEL_IDS: current.slackAllowedChannelIds.join(","),
|
|
682
|
+
SLACK_MESSAGE_CONTENT_ENABLED: boolValue(current.slackMessageContentEnabled),
|
|
683
|
+
SLACK_COMMAND: current.slackCommand,
|
|
684
|
+
SLACK_CLI_MIRROR_MODE: current.slackMirrorMode === current.mirrorMode ? "" : current.slackMirrorMode,
|
|
685
|
+
SLACK_CLI_MIRROR_MIN_UPDATE_MS: current.slackMirrorMinUpdateMs === current.mirrorMinUpdateMs ? "" : String(current.slackMirrorMinUpdateMs),
|
|
686
|
+
SLACK_NOTIFY_MODE: current.slackNotifyMode === current.notifyMode ? "" : current.slackNotifyMode,
|
|
687
|
+
SLACK_QUIET_HOURS: quietOverrideValue(current.slackQuietHours, current.quietHours),
|
|
688
|
+
SLACK_AUTO_SEND_ARTIFACTS: current.slackAutoSendArtifacts === current.autoSendArtifacts ? "" : boolValue(current.slackAutoSendArtifacts),
|
|
643
689
|
NORDRELAY_CODEX_ENABLED: boolValue(current.codexEnabled),
|
|
644
690
|
NORDRELAY_PI_ENABLED: boolValue(current.piEnabled),
|
|
645
691
|
NORDRELAY_HERMES_ENABLED: boolValue(current.hermesEnabled),
|
package/dist/web-state.js
CHANGED
|
@@ -123,7 +123,7 @@ function isWebChatMessage(value) {
|
|
|
123
123
|
typeof candidate.text === "string" &&
|
|
124
124
|
typeof candidate.timestamp === "string" &&
|
|
125
125
|
["user", "agent", "system", "tool"].includes(candidate.role) &&
|
|
126
|
-
["web", "telegram", "discord", "cli"].includes(candidate.source);
|
|
126
|
+
["web", "telegram", "discord", "slack", "cli"].includes(candidate.source);
|
|
127
127
|
}
|
|
128
128
|
function isWebActivityEvent(value) {
|
|
129
129
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -133,7 +133,7 @@ function isWebActivityEvent(value) {
|
|
|
133
133
|
return typeof candidate.id === "string" &&
|
|
134
134
|
typeof candidate.timestamp === "string" &&
|
|
135
135
|
typeof candidate.type === "string" &&
|
|
136
|
-
["web", "telegram", "discord", "cli"].includes(candidate.source) &&
|
|
136
|
+
["web", "telegram", "discord", "slack", "cli"].includes(candidate.source) &&
|
|
137
137
|
["queued", "running", "completed", "failed", "aborted", "info"].includes(candidate.status);
|
|
138
138
|
}
|
|
139
139
|
function normalizeSince(value) {
|