@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.
- package/.env.example +80 -11
- package/README.md +154 -22
- package/dist/access-control.js +7 -1
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +535 -11
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +40 -7
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +369 -0
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-service.js +237 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +93 -13
- package/dist/config.js +103 -8
- package/dist/context-key.js +87 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2073 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +57 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +36 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +87 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +897 -394
- package/dist/remote-prompt.js +98 -0
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/support-bundle.js +1 -0
- package/dist/telegram-access-commands.js +15 -2
- package/dist/telegram-access-middleware.js +16 -3
- package/dist/telegram-agent-commands.js +25 -0
- package/dist/telegram-artifact-commands.js +46 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +16 -6
- package/dist/telegram-operational-commands.js +14 -6
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/telegram-queue-commands.js +74 -4
- package/dist/telegram-support-command.js +7 -0
- package/dist/telegram-update-commands.js +27 -0
- package/dist/user-management.js +208 -0
- package/dist/web-api-contract.js +17 -0
- package/dist/web-dashboard-access-routes.js +74 -1
- package/dist/web-dashboard-artifact-routes.js +3 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-pages.js +109 -13
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +2 -0
- package/dist/web-dashboard.js +160 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +779 -55
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
|
@@ -10,12 +10,12 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
10
10
|
if (req.method === "POST" && url.pathname === "/api/locks") {
|
|
11
11
|
const body = await readJsonBody(req);
|
|
12
12
|
await options.assertCurrentSessionScope(authUser);
|
|
13
|
-
sendJson(res, 200, { lock: runtime.lockWebSession(optionalStringField(body, "ownerName")), locks: runtime.locks() });
|
|
13
|
+
sendJson(res, 200, { lock: runtime.lockWebSession(optionalStringField(body, "ownerName"), options.activityActor), locks: runtime.locks() });
|
|
14
14
|
return true;
|
|
15
15
|
}
|
|
16
16
|
if (req.method === "DELETE" && url.pathname === "/api/locks") {
|
|
17
17
|
await options.assertCurrentSessionScope(authUser);
|
|
18
|
-
sendJson(res, 200, runtime.unlockWebSession());
|
|
18
|
+
sendJson(res, 200, runtime.unlockWebSession(options.activityActor));
|
|
19
19
|
return true;
|
|
20
20
|
}
|
|
21
21
|
if (req.method === "GET" && url.pathname === "/api/auth/status") {
|
|
@@ -28,14 +28,14 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
28
28
|
const body = await readJsonBody(req);
|
|
29
29
|
const agentId = options.parseAgentId(optionalStringField(body, "agentId"));
|
|
30
30
|
options.assertScopedAgent(authUser, agentId);
|
|
31
|
-
sendJson(res, 200, await runtime.login(agentId));
|
|
31
|
+
sendJson(res, 200, await runtime.login(agentId, options.activityActor));
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
34
|
if (req.method === "POST" && url.pathname === "/api/auth/logout") {
|
|
35
35
|
const body = await readJsonBody(req);
|
|
36
36
|
const agentId = options.parseAgentId(optionalStringField(body, "agentId"));
|
|
37
37
|
options.assertScopedAgent(authUser, agentId);
|
|
38
|
-
sendJson(res, 200, await runtime.logout(agentId));
|
|
38
|
+
sendJson(res, 200, await runtime.logout(agentId, options.activityActor));
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
41
41
|
if (req.method === "GET" && url.pathname === "/api/snapshot") {
|
|
@@ -62,7 +62,7 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
62
62
|
throw new Error(`Invalid agent: ${agentId}`);
|
|
63
63
|
}
|
|
64
64
|
options.assertScopedAgent(authUser, agentId);
|
|
65
|
-
sendJson(res, 200, { session: await runtime.setAgent(agentId) });
|
|
65
|
+
sendJson(res, 200, { session: await runtime.setAgent(agentId, options.activityActor) });
|
|
66
66
|
return true;
|
|
67
67
|
}
|
|
68
68
|
if (req.method === "POST" && url.pathname === "/api/sessions/new") {
|
|
@@ -79,7 +79,7 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
79
79
|
reasoningEffort: optionalStringField(body, "reasoningEffort"),
|
|
80
80
|
launchProfileId: optionalStringField(body, "launchProfileId"),
|
|
81
81
|
fastMode: optionalBooleanField(body, "fastMode"),
|
|
82
|
-
}),
|
|
82
|
+
}, options.activityActor),
|
|
83
83
|
});
|
|
84
84
|
return true;
|
|
85
85
|
}
|
|
@@ -90,14 +90,14 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
90
90
|
if (detail.record && typeof detail.record === "object") {
|
|
91
91
|
options.assertSessionScope(authUser, detail.record);
|
|
92
92
|
}
|
|
93
|
-
const session = await runtime.switchSession(threadId);
|
|
93
|
+
const session = await runtime.switchSession(threadId, options.activityActor);
|
|
94
94
|
options.assertSessionScope(authUser, session);
|
|
95
95
|
sendJson(res, 200, { session });
|
|
96
96
|
return true;
|
|
97
97
|
}
|
|
98
98
|
if (req.method === "POST" && url.pathname === "/api/sessions/attach") {
|
|
99
99
|
const body = await readJsonBody(req);
|
|
100
|
-
const session = await runtime.attachSession(stringField(body, "threadId"));
|
|
100
|
+
const session = await runtime.attachSession(stringField(body, "threadId"), options.activityActor);
|
|
101
101
|
options.assertSessionScope(authUser, session);
|
|
102
102
|
sendJson(res, 200, { session });
|
|
103
103
|
return true;
|
|
@@ -117,31 +117,31 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
117
117
|
if (req.method === "POST" && url.pathname === "/api/session/model") {
|
|
118
118
|
const body = await readJsonBody(req);
|
|
119
119
|
await options.assertCurrentSessionScope(authUser);
|
|
120
|
-
sendJson(res, 200, { session: await runtime.setModel(stringField(body, "model")) });
|
|
120
|
+
sendJson(res, 200, { session: await runtime.setModel(stringField(body, "model"), options.activityActor) });
|
|
121
121
|
return true;
|
|
122
122
|
}
|
|
123
123
|
if (req.method === "POST" && url.pathname === "/api/session/reasoning") {
|
|
124
124
|
const body = await readJsonBody(req);
|
|
125
125
|
await options.assertCurrentSessionScope(authUser);
|
|
126
|
-
sendJson(res, 200, { session: await runtime.setReasoningEffort(stringField(body, "reasoning")) });
|
|
126
|
+
sendJson(res, 200, { session: await runtime.setReasoningEffort(stringField(body, "reasoning"), options.activityActor) });
|
|
127
127
|
return true;
|
|
128
128
|
}
|
|
129
129
|
if (req.method === "POST" && url.pathname === "/api/session/fast") {
|
|
130
130
|
const body = await readJsonBody(req);
|
|
131
131
|
await options.assertCurrentSessionScope(authUser);
|
|
132
|
-
sendJson(res, 200, { session: await runtime.setFastMode(Boolean(body?.enabled)) });
|
|
132
|
+
sendJson(res, 200, { session: await runtime.setFastMode(Boolean(body?.enabled), options.activityActor) });
|
|
133
133
|
return true;
|
|
134
134
|
}
|
|
135
135
|
if (req.method === "POST" && url.pathname === "/api/session/launch") {
|
|
136
136
|
const body = await readJsonBody(req);
|
|
137
137
|
await options.assertCurrentSessionScope(authUser);
|
|
138
|
-
sendJson(res, 200, { session: await runtime.setLaunchProfile(stringField(body, "profileId")) });
|
|
138
|
+
sendJson(res, 200, { session: await runtime.setLaunchProfile(stringField(body, "profileId"), options.activityActor) });
|
|
139
139
|
return true;
|
|
140
140
|
}
|
|
141
141
|
if (req.method === "POST" && url.pathname === "/api/prompt") {
|
|
142
142
|
const body = await readJsonBody(req);
|
|
143
143
|
await options.assertCurrentSessionScope(authUser);
|
|
144
|
-
sendJson(res, 202, await runtime.sendPrompt(stringField(body, "text")));
|
|
144
|
+
sendJson(res, 202, await runtime.sendPrompt(stringField(body, "text"), options.activityActor));
|
|
145
145
|
return true;
|
|
146
146
|
}
|
|
147
147
|
if (req.method === "POST" && url.pathname === "/api/prompt/upload") {
|
|
@@ -150,28 +150,28 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
150
150
|
sendJson(res, 202, await runtime.sendUploadPrompt({
|
|
151
151
|
text: optionalStringField(body, "text"),
|
|
152
152
|
files: parseUploadFiles(body.files),
|
|
153
|
-
}));
|
|
153
|
+
}, options.activityActor));
|
|
154
154
|
return true;
|
|
155
155
|
}
|
|
156
156
|
if (req.method === "POST" && (url.pathname === "/api/abort" || url.pathname === "/api/stop")) {
|
|
157
157
|
await options.assertCurrentSessionScope(authUser);
|
|
158
|
-
await runtime.abort();
|
|
158
|
+
await runtime.abort(options.activityActor);
|
|
159
159
|
sendJson(res, 200, { ok: true });
|
|
160
160
|
return true;
|
|
161
161
|
}
|
|
162
162
|
if (req.method === "POST" && url.pathname === "/api/handback") {
|
|
163
163
|
await options.assertCurrentSessionScope(authUser);
|
|
164
|
-
sendJson(res, 200, await runtime.handback());
|
|
164
|
+
sendJson(res, 200, await runtime.handback(options.activityActor));
|
|
165
165
|
return true;
|
|
166
166
|
}
|
|
167
167
|
if (req.method === "POST" && url.pathname === "/api/retry") {
|
|
168
168
|
await options.assertCurrentSessionScope(authUser);
|
|
169
|
-
sendJson(res, 202, await runtime.retry());
|
|
169
|
+
sendJson(res, 202, await runtime.retry(options.activityActor));
|
|
170
170
|
return true;
|
|
171
171
|
}
|
|
172
172
|
if (req.method === "POST" && url.pathname === "/api/sync") {
|
|
173
173
|
await options.assertCurrentSessionScope(authUser);
|
|
174
|
-
sendJson(res, 200, await runtime.sync());
|
|
174
|
+
sendJson(res, 200, await runtime.sync(options.activityActor));
|
|
175
175
|
return true;
|
|
176
176
|
}
|
|
177
177
|
if (req.method === "GET" && url.pathname === "/api/queue") {
|
|
@@ -182,7 +182,7 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
182
182
|
if (req.method === "POST" && url.pathname === "/api/queue") {
|
|
183
183
|
const body = await readJsonBody(req);
|
|
184
184
|
await options.assertCurrentSessionScope(authUser);
|
|
185
|
-
sendJson(res, 200, { queue: runtime.queueAction(stringField(body, "action"), optionalStringField(body, "id")), paused: runtime.queuePaused() });
|
|
185
|
+
sendJson(res, 200, { queue: runtime.queueAction(stringField(body, "action"), optionalStringField(body, "id"), options.activityActor), paused: runtime.queuePaused() });
|
|
186
186
|
return true;
|
|
187
187
|
}
|
|
188
188
|
if (req.method === "GET" && url.pathname === "/api/chat/history") {
|
|
@@ -192,7 +192,7 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
192
192
|
}
|
|
193
193
|
if (req.method === "DELETE" && url.pathname === "/api/chat/history") {
|
|
194
194
|
await options.assertCurrentSessionScope(authUser);
|
|
195
|
-
sendJson(res, 200, await runtime.clearChatHistory());
|
|
195
|
+
sendJson(res, 200, await runtime.clearChatHistory(options.activityActor));
|
|
196
196
|
return true;
|
|
197
197
|
}
|
|
198
198
|
if (req.method === "GET" && url.pathname === "/api/activity") {
|
|
@@ -201,6 +201,13 @@ export async function handleDashboardSessionRoute(req, res, url, options) {
|
|
|
201
201
|
limit: numberParam(url, "limit", 100),
|
|
202
202
|
source: (url.searchParams.get("source") || "all"),
|
|
203
203
|
status: (url.searchParams.get("status") || "all"),
|
|
204
|
+
category: (url.searchParams.get("category") || "all"),
|
|
205
|
+
actor: url.searchParams.get("actor") || undefined,
|
|
206
|
+
agentId: url.searchParams.get("agent") || "all",
|
|
207
|
+
threadId: url.searchParams.get("thread") || undefined,
|
|
208
|
+
workspace: url.searchParams.get("workspace") || undefined,
|
|
209
|
+
type: url.searchParams.get("type") || undefined,
|
|
210
|
+
since: url.searchParams.get("since") || undefined,
|
|
204
211
|
})),
|
|
205
212
|
});
|
|
206
213
|
return true;
|
package/dist/web-dashboard-ui.js
CHANGED
|
@@ -4,9 +4,11 @@ export const DASHBOARD_PAGES = [
|
|
|
4
4
|
{ id: "sessions", label: "Sessions", permission: "sessions.read" },
|
|
5
5
|
{ id: "queue", label: "Queue", permission: "queue.read" },
|
|
6
6
|
{ id: "tasks", label: "Tasks", permission: "inspect" },
|
|
7
|
+
{ id: "metrics", label: "Metrics", permission: "inspect" },
|
|
7
8
|
{ id: "activity", label: "Activity", permission: "sessions.read" },
|
|
8
9
|
{ id: "artifacts", label: "Artifacts", permission: "files.read" },
|
|
9
10
|
{ id: "adapters", label: "Adapters", permission: "inspect" },
|
|
11
|
+
{ id: "peers", label: "Peers", permission: "peers.read" },
|
|
10
12
|
{ id: "access", label: "Users", permission: "users.read" },
|
|
11
13
|
{ id: "version", label: "Version", permission: "inspect" },
|
|
12
14
|
{ id: "settings", label: "Settings", permission: "settings.read" },
|
package/dist/web-dashboard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { URL } from "node:url";
|
|
@@ -17,9 +18,10 @@ import { handleDashboardAccessRoute } from "./web-dashboard-access-routes.js";
|
|
|
17
18
|
import { handleDashboardArtifactRoute } from "./web-dashboard-artifact-routes.js";
|
|
18
19
|
import { dashboardCss, dashboardJs } from "./web-dashboard-assets.js";
|
|
19
20
|
import { objectRecord, optionalStringField, parseCookies, readJsonBody, sendJson, sendText, } from "./web-dashboard-http.js";
|
|
20
|
-
import { renderDashboardApp, renderLoginPage } from "./web-dashboard-pages.js";
|
|
21
|
+
import { renderDashboardApp, renderFirstRunSetupPage, renderLoginPage } from "./web-dashboard-pages.js";
|
|
21
22
|
import { handleDashboardRuntimeRoute } from "./web-dashboard-runtime-routes.js";
|
|
22
23
|
import { handleDashboardSessionRoute } from "./web-dashboard-session-routes.js";
|
|
24
|
+
import { handleDashboardPeerRoute } from "./web-dashboard-peer-routes.js";
|
|
23
25
|
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
24
26
|
const options = parseOptions(process.argv.slice(2));
|
|
25
27
|
const config = loadConfig();
|
|
@@ -28,6 +30,11 @@ const settings = new SettingsService(resolveDashboardEnvPath(options.home));
|
|
|
28
30
|
const users = new UserStore(options.home);
|
|
29
31
|
const auditLog = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
30
32
|
const loginAttempts = new Map();
|
|
33
|
+
const firstRunSetupToken = users.hasAdminUser() ? undefined : randomBytes(18).toString("base64url");
|
|
34
|
+
const firstRunSetupRequiresToken = !isLoopbackHost(options.host);
|
|
35
|
+
if (firstRunSetupToken) {
|
|
36
|
+
console.log(`NordRelay first-run setup token: ${firstRunSetupToken}`);
|
|
37
|
+
}
|
|
31
38
|
class AccessDeniedError extends Error {
|
|
32
39
|
}
|
|
33
40
|
const server = createServer((req, res) => {
|
|
@@ -45,6 +52,10 @@ async function handleRequest(req, res) {
|
|
|
45
52
|
await handleLogin(req, res);
|
|
46
53
|
return;
|
|
47
54
|
}
|
|
55
|
+
if (url.pathname === "/api/setup/admin" && req.method === "POST") {
|
|
56
|
+
await handleFirstRunSetup(req, res);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
48
59
|
if (url.pathname === "/api/dashboard/logout" && req.method === "POST") {
|
|
49
60
|
handleLogout(req, res);
|
|
50
61
|
return;
|
|
@@ -60,6 +71,10 @@ async function handleRequest(req, res) {
|
|
|
60
71
|
}
|
|
61
72
|
if (!authenticated) {
|
|
62
73
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
74
|
+
if (!users.hasAdminUser()) {
|
|
75
|
+
sendText(res, 200, renderFirstRunSetupPage({ tokenRequired: firstRunSetupRequiresToken || !isLoopbackRequest(req) }), "text/html; charset=utf-8");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
63
78
|
sendText(res, 200, renderLoginPage({ adminConfigured: users.hasAdminUser() }), "text/html; charset=utf-8");
|
|
64
79
|
return;
|
|
65
80
|
}
|
|
@@ -104,6 +119,7 @@ async function handleApi(req, res, url, authUser) {
|
|
|
104
119
|
status: "denied",
|
|
105
120
|
channelId: "web",
|
|
106
121
|
contextKey: "web",
|
|
122
|
+
actor: webActivityActor(authUser),
|
|
107
123
|
actorId: authUser.user.id,
|
|
108
124
|
actorRole: authUser.groups.map((group) => group.name).join(", "),
|
|
109
125
|
description: `Denied unknown endpoint ${req.method ?? "GET"} ${url.pathname}`,
|
|
@@ -117,6 +133,7 @@ async function handleApi(req, res, url, authUser) {
|
|
|
117
133
|
status: "denied",
|
|
118
134
|
channelId: "web",
|
|
119
135
|
contextKey: "web",
|
|
136
|
+
actor: webActivityActor(authUser),
|
|
120
137
|
actorId: authUser.user.id,
|
|
121
138
|
actorRole: authUser.groups.map((group) => group.name).join(", "),
|
|
122
139
|
description: `${permission} required for ${req.method ?? "GET"} ${url.pathname}`,
|
|
@@ -133,6 +150,8 @@ async function handleApi(req, res, url, authUser) {
|
|
|
133
150
|
assertAgentUpdateJobScope,
|
|
134
151
|
assertCurrentSessionScope,
|
|
135
152
|
scopedTasks,
|
|
153
|
+
scopedActiveSessions,
|
|
154
|
+
activityActor: webActivityActor(authUser),
|
|
136
155
|
})) {
|
|
137
156
|
return;
|
|
138
157
|
}
|
|
@@ -162,6 +181,15 @@ async function handleApi(req, res, url, authUser) {
|
|
|
162
181
|
})) {
|
|
163
182
|
return;
|
|
164
183
|
}
|
|
184
|
+
if (await handleDashboardPeerRoute(req, res, url, {
|
|
185
|
+
config,
|
|
186
|
+
home: options.home,
|
|
187
|
+
runtime,
|
|
188
|
+
activityActor: webActivityActor(authUser),
|
|
189
|
+
auditPeerAction: (action, description) => auditUserAction(authUser, action, description),
|
|
190
|
+
})) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
165
193
|
if (req.method === "GET" && url.pathname === "/api/settings") {
|
|
166
194
|
sendJson(res, 200, await settings.snapshot(process.env, activeSettingsValues(config)));
|
|
167
195
|
return;
|
|
@@ -182,6 +210,7 @@ async function handleApi(req, res, url, authUser) {
|
|
|
182
210
|
assertSessionDetailScope,
|
|
183
211
|
scopedSessionPage,
|
|
184
212
|
filterActivityByScope,
|
|
213
|
+
activityActor: webActivityActor(authUser),
|
|
185
214
|
})) {
|
|
186
215
|
return;
|
|
187
216
|
}
|
|
@@ -189,6 +218,7 @@ async function handleApi(req, res, url, authUser) {
|
|
|
189
218
|
runtime,
|
|
190
219
|
authUser,
|
|
191
220
|
assertCurrentSessionScope,
|
|
221
|
+
activityActor: webActivityActor(authUser),
|
|
192
222
|
})) {
|
|
193
223
|
return;
|
|
194
224
|
}
|
|
@@ -239,6 +269,58 @@ async function handleEvents(req, res) {
|
|
|
239
269
|
unsubscribe();
|
|
240
270
|
});
|
|
241
271
|
}
|
|
272
|
+
async function handleFirstRunSetup(req, res) {
|
|
273
|
+
if (users.hasAdminUser()) {
|
|
274
|
+
sendJson(res, 409, { error: "Admin user already exists." });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const body = await readJsonBody(req);
|
|
278
|
+
const email = optionalStringField(body, "email") ?? "";
|
|
279
|
+
const displayName = optionalStringField(body, "displayName") ?? email;
|
|
280
|
+
const password = optionalStringField(body, "password") ?? "";
|
|
281
|
+
const setupToken = optionalStringField(body, "setupToken") ?? "";
|
|
282
|
+
if ((firstRunSetupRequiresToken || !isLoopbackRequest(req)) && setupToken !== firstRunSetupToken) {
|
|
283
|
+
audit({
|
|
284
|
+
action: "auth_login_failed",
|
|
285
|
+
status: "denied",
|
|
286
|
+
channelId: "web",
|
|
287
|
+
contextKey: "web",
|
|
288
|
+
description: `Rejected remote first-run setup for ${email || "unknown"}`,
|
|
289
|
+
});
|
|
290
|
+
sendJson(res, 403, { error: "Setup token required." });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (setupToken && setupToken !== firstRunSetupToken) {
|
|
294
|
+
sendJson(res, 403, { error: "Invalid setup token." });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!email || !password || password.length < 12) {
|
|
298
|
+
sendJson(res, 400, { error: "Email and a password with at least 12 characters are required." });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const authUser = users.createAdmin({ email, displayName, password });
|
|
302
|
+
const session = users.createWebSession(authUser.user.id);
|
|
303
|
+
audit({
|
|
304
|
+
action: "user_created",
|
|
305
|
+
status: "ok",
|
|
306
|
+
channelId: "web",
|
|
307
|
+
contextKey: "web",
|
|
308
|
+
actor: webActivityActor(authUser),
|
|
309
|
+
actorId: authUser.user.id,
|
|
310
|
+
actorRole: authUser.groups.map((group) => group.name).join(", "),
|
|
311
|
+
description: `First admin created: ${authUser.user.email}`,
|
|
312
|
+
});
|
|
313
|
+
runtime.recordActivity({
|
|
314
|
+
source: "web",
|
|
315
|
+
status: "info",
|
|
316
|
+
type: "first_run_admin_created",
|
|
317
|
+
threadId: null,
|
|
318
|
+
actor: webActivityActor(authUser),
|
|
319
|
+
detail: authUser.user.email,
|
|
320
|
+
});
|
|
321
|
+
setSessionCookie(res, session.token);
|
|
322
|
+
sendJson(res, 201, currentUserDto(authUser));
|
|
323
|
+
}
|
|
242
324
|
async function handleLogin(req, res) {
|
|
243
325
|
const body = await readJsonBody(req);
|
|
244
326
|
const email = optionalStringField(body, "email");
|
|
@@ -280,13 +362,32 @@ async function handleLogin(req, res) {
|
|
|
280
362
|
status: "ok",
|
|
281
363
|
channelId: "web",
|
|
282
364
|
contextKey: "web",
|
|
365
|
+
actor: webActivityActor(authUser),
|
|
283
366
|
actorId: authUser.user.id,
|
|
284
367
|
actorRole: authUser.groups.map((group) => group.name).join(", "),
|
|
285
368
|
description: `Login ${authUser.user.email}`,
|
|
286
369
|
});
|
|
370
|
+
runtime.recordActivity({
|
|
371
|
+
source: "web",
|
|
372
|
+
status: "info",
|
|
373
|
+
type: "auth_login",
|
|
374
|
+
threadId: null,
|
|
375
|
+
actor: webActivityActor(authUser),
|
|
376
|
+
detail: authUser.user.email,
|
|
377
|
+
});
|
|
287
378
|
setSessionCookie(res, session.token);
|
|
288
379
|
sendJson(res, 200, currentUserDto(authUser));
|
|
289
380
|
}
|
|
381
|
+
function isLoopbackRequest(req) {
|
|
382
|
+
const address = req.socket.remoteAddress ?? "";
|
|
383
|
+
return address === "127.0.0.1" ||
|
|
384
|
+
address === "::1" ||
|
|
385
|
+
address === "::ffff:127.0.0.1" ||
|
|
386
|
+
address === "localhost";
|
|
387
|
+
}
|
|
388
|
+
function isLoopbackHost(host) {
|
|
389
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
390
|
+
}
|
|
290
391
|
function handleLogout(req, res) {
|
|
291
392
|
const authUser = authenticateRequest(req);
|
|
292
393
|
users.destroyWebSession(parseCookies(req.headers.cookie ?? "").nr_session);
|
|
@@ -345,10 +446,27 @@ function auditUserAction(authUser, action, description) {
|
|
|
345
446
|
status: "ok",
|
|
346
447
|
channelId: "web",
|
|
347
448
|
contextKey: "web",
|
|
449
|
+
actor: webActivityActor(authUser),
|
|
348
450
|
actorId: authUser.user.id,
|
|
349
451
|
actorRole: authUser.groups.map((group) => group.name).join(", "),
|
|
350
452
|
description,
|
|
351
453
|
});
|
|
454
|
+
runtime.recordActivity({
|
|
455
|
+
source: "web",
|
|
456
|
+
status: "info",
|
|
457
|
+
type: action,
|
|
458
|
+
threadId: null,
|
|
459
|
+
actor: webActivityActor(authUser),
|
|
460
|
+
detail: description,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function webActivityActor(authUser) {
|
|
464
|
+
return {
|
|
465
|
+
channel: "web",
|
|
466
|
+
id: authUser.user.id,
|
|
467
|
+
label: authUser.user.displayName || authUser.user.email,
|
|
468
|
+
username: authUser.user.email,
|
|
469
|
+
};
|
|
352
470
|
}
|
|
353
471
|
function scopedControlOptions(authUser, options) {
|
|
354
472
|
return {
|
|
@@ -372,6 +490,12 @@ async function scopedTasks(authUser, tasks) {
|
|
|
372
490
|
recent: filterActivityByScope(authUser, tasks.recent),
|
|
373
491
|
};
|
|
374
492
|
}
|
|
493
|
+
function scopedActiveSessions(authUser, active) {
|
|
494
|
+
return {
|
|
495
|
+
...active,
|
|
496
|
+
sessions: active.sessions.filter((session) => canUseSession(authUser, session)),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
375
499
|
async function scopeRelayEvent(authUser, event, canUseCurrentSession = () => canUseCurrentSessionScope(authUser)) {
|
|
376
500
|
switch (event.type) {
|
|
377
501
|
case "snapshot":
|
|
@@ -380,6 +504,8 @@ async function scopeRelayEvent(authUser, event, canUseCurrentSession = () => can
|
|
|
380
504
|
return canUseSession(authUser, event.session) ? event : null;
|
|
381
505
|
case "activity_update":
|
|
382
506
|
return { ...event, events: filterActivityByScope(authUser, event.events) };
|
|
507
|
+
case "active_sessions_update":
|
|
508
|
+
return { ...event, active: scopedActiveSessions(authUser, event.active) };
|
|
383
509
|
case "agent_update":
|
|
384
510
|
return users.canUseAgent(authUser, event.job.agentId) ? event : null;
|
|
385
511
|
case "status":
|
|
@@ -504,6 +630,7 @@ function optionalEnv(key) {
|
|
|
504
630
|
}
|
|
505
631
|
function activeSettingsValues(current) {
|
|
506
632
|
return {
|
|
633
|
+
TELEGRAM_ENABLED: boolValue(current.telegramEnabled),
|
|
507
634
|
TELEGRAM_BOT_TOKEN: current.telegramBotToken,
|
|
508
635
|
TELEGRAM_TRANSPORT: current.telegramTransport,
|
|
509
636
|
TELEGRAM_WEBHOOK_URL: current.telegramWebhookUrl,
|
|
@@ -511,6 +638,20 @@ function activeSettingsValues(current) {
|
|
|
511
638
|
TELEGRAM_WEBHOOK_PORT: String(current.telegramWebhookPort),
|
|
512
639
|
TELEGRAM_WEBHOOK_PATH: current.telegramWebhookPath,
|
|
513
640
|
TELEGRAM_WEBHOOK_SECRET: current.telegramWebhookSecret,
|
|
641
|
+
DISCORD_ENABLED: boolValue(current.discordEnabled),
|
|
642
|
+
DISCORD_BOT_TOKEN: current.discordBotToken,
|
|
643
|
+
DISCORD_CLIENT_ID: current.discordClientId,
|
|
644
|
+
DISCORD_GUILD_IDS: current.discordGuildIds.join(","),
|
|
645
|
+
DISCORD_ALLOWED_GUILD_IDS: current.discordAllowedGuildIds.join(","),
|
|
646
|
+
DISCORD_ALLOWED_CHANNEL_IDS: current.discordAllowedChannelIds.join(","),
|
|
647
|
+
DISCORD_MESSAGE_CONTENT_ENABLED: boolValue(current.discordMessageContentEnabled),
|
|
648
|
+
DISCORD_COMMAND_MODE: current.discordCommandMode,
|
|
649
|
+
DISCORD_AUTO_REGISTER_COMMANDS: boolValue(current.discordAutoRegisterCommands),
|
|
650
|
+
DISCORD_CLI_MIRROR_MODE: current.discordMirrorMode === current.mirrorMode ? "" : current.discordMirrorMode,
|
|
651
|
+
DISCORD_CLI_MIRROR_MIN_UPDATE_MS: current.discordMirrorMinUpdateMs === current.mirrorMinUpdateMs ? "" : String(current.discordMirrorMinUpdateMs),
|
|
652
|
+
DISCORD_NOTIFY_MODE: current.discordNotifyMode === current.notifyMode ? "" : current.discordNotifyMode,
|
|
653
|
+
DISCORD_QUIET_HOURS: quietOverrideValue(current.discordQuietHours, current.quietHours),
|
|
654
|
+
DISCORD_AUTO_SEND_ARTIFACTS: current.discordAutoSendArtifacts === current.autoSendArtifacts ? "" : boolValue(current.discordAutoSendArtifacts),
|
|
514
655
|
NORDRELAY_CODEX_ENABLED: boolValue(current.codexEnabled),
|
|
515
656
|
NORDRELAY_PI_ENABLED: boolValue(current.piEnabled),
|
|
516
657
|
NORDRELAY_HERMES_ENABLED: boolValue(current.hermesEnabled),
|
|
@@ -565,10 +706,15 @@ function activeSettingsValues(current) {
|
|
|
565
706
|
ENABLE_TELEGRAM_REACTIONS: boolValue(current.enableTelegramReactions),
|
|
566
707
|
TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS: String(current.telegramRateLimitMinIntervalMs),
|
|
567
708
|
TELEGRAM_EDIT_MIN_INTERVAL_MS: String(current.telegramEditMinIntervalMs),
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
709
|
+
NORDRELAY_CLI_MIRROR_MODE: current.mirrorMode,
|
|
710
|
+
NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS: String(current.mirrorMinUpdateMs),
|
|
711
|
+
NORDRELAY_NOTIFY_MODE: current.notifyMode,
|
|
712
|
+
NORDRELAY_QUIET_HOURS: quietValue(current.quietHours),
|
|
713
|
+
NORDRELAY_AUTO_SEND_ARTIFACTS: boolValue(current.autoSendArtifacts),
|
|
714
|
+
TELEGRAM_CLI_MIRROR_MODE: current.telegramMirrorMode === current.mirrorMode ? "" : current.telegramMirrorMode,
|
|
715
|
+
TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS: current.telegramMirrorMinUpdateMs === current.mirrorMinUpdateMs ? "" : String(current.telegramMirrorMinUpdateMs),
|
|
716
|
+
TELEGRAM_NOTIFY_MODE: current.telegramNotifyMode === current.notifyMode ? "" : current.telegramNotifyMode,
|
|
717
|
+
TELEGRAM_QUIET_HOURS: quietOverrideValue(current.telegramQuietHours, current.quietHours),
|
|
572
718
|
TELEGRAM_REDACT_PATTERNS: current.telegramRedactPatterns.join(","),
|
|
573
719
|
NORDRELAY_UPDATE_METHOD: process.env.NORDRELAY_UPDATE_METHOD || "auto",
|
|
574
720
|
MAX_FILE_SIZE: String(current.maxFileSize),
|
|
@@ -577,12 +723,14 @@ function activeSettingsValues(current) {
|
|
|
577
723
|
ARTIFACT_MAX_INBOX_DIRS: String(current.artifactMaxInboxDirs),
|
|
578
724
|
ARTIFACT_IGNORE_DIRS: current.artifactIgnoreDirs.join(","),
|
|
579
725
|
ARTIFACT_IGNORE_GLOBS: current.artifactIgnoreGlobs.join(","),
|
|
580
|
-
TELEGRAM_AUTO_SEND_ARTIFACTS: boolValue(current.telegramAutoSendArtifacts),
|
|
726
|
+
TELEGRAM_AUTO_SEND_ARTIFACTS: current.telegramAutoSendArtifacts === current.autoSendArtifacts ? "" : boolValue(current.telegramAutoSendArtifacts),
|
|
581
727
|
WORKSPACE_ALLOWED_ROOTS: current.workspaceAllowedRoots.join(","),
|
|
582
728
|
WORKSPACE_WARN_ROOTS: current.workspaceWarnRoots.join(","),
|
|
583
729
|
NORDRELAY_STATE_BACKEND: current.stateBackend,
|
|
584
730
|
NORDRELAY_AUDIT_MAX_EVENTS: String(current.auditMaxEvents),
|
|
585
731
|
NORDRELAY_SESSION_LOCK_TTL_MS: String(current.sessionLockTtlMs),
|
|
732
|
+
NORDRELAY_DASHBOARD_CACHE_TTL_MS: String(current.dashboardCacheTtlMs),
|
|
733
|
+
NORDRELAY_UNIFIED_JOB_MAX_ITEMS: String(current.unifiedJobMaxItems),
|
|
586
734
|
NORDRELAY_VERSION_CACHE_TTL_MS: process.env.NORDRELAY_VERSION_CACHE_TTL_MS,
|
|
587
735
|
NORDRELAY_CLI_VERSION_CACHE_TTL_MS: process.env.NORDRELAY_CLI_VERSION_CACHE_TTL_MS,
|
|
588
736
|
VOICE_PREFERRED_BACKEND: current.voicePreferredBackend,
|
|
@@ -601,6 +749,12 @@ function activeSettingsValues(current) {
|
|
|
601
749
|
function boolValue(value) {
|
|
602
750
|
return value ? "true" : "false";
|
|
603
751
|
}
|
|
752
|
+
function quietValue(value) {
|
|
753
|
+
return value ? `${value.startHour}-${value.endHour}` : "";
|
|
754
|
+
}
|
|
755
|
+
function quietOverrideValue(channelValue, defaultValue) {
|
|
756
|
+
return quietValue(channelValue) === quietValue(defaultValue) ? "" : quietValue(channelValue);
|
|
757
|
+
}
|
|
604
758
|
function requireArg(argv, index, flag) {
|
|
605
759
|
const value = argv[index];
|
|
606
760
|
if (!value || value.startsWith("--")) {
|
package/dist/web-state.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { activityActorLabel, activityCategoryForType, } from "./activity-events.js";
|
|
2
3
|
import { createDocumentStore } from "./state-backend.js";
|
|
3
4
|
const DEFAULT_CHAT_LIMIT = 300;
|
|
4
5
|
const DEFAULT_ACTIVITY_LIMIT = 1000;
|
|
@@ -76,6 +77,7 @@ export class WebActivityStore {
|
|
|
76
77
|
id: randomId(),
|
|
77
78
|
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
78
79
|
...input,
|
|
80
|
+
category: input.category ?? activityCategoryForType(input.type),
|
|
79
81
|
};
|
|
80
82
|
payload.events.push(event);
|
|
81
83
|
if (payload.events.length > this.maxEvents) {
|
|
@@ -86,9 +88,17 @@ export class WebActivityStore {
|
|
|
86
88
|
}
|
|
87
89
|
list(options = {}) {
|
|
88
90
|
const limit = Math.max(1, Math.min(500, options.limit ?? 100));
|
|
91
|
+
const since = normalizeSince(options.since);
|
|
89
92
|
return this.readPayload().events
|
|
90
93
|
.filter((event) => !options.source || options.source === "all" || event.source === options.source)
|
|
91
94
|
.filter((event) => !options.status || options.status === "all" || event.status === options.status)
|
|
95
|
+
.filter((event) => !options.category || options.category === "all" || (event.category ?? activityCategoryForType(event.type)) === options.category)
|
|
96
|
+
.filter((event) => !options.agentId || options.agentId === "all" || event.agentId === options.agentId)
|
|
97
|
+
.filter((event) => !options.threadId || event.threadId === options.threadId)
|
|
98
|
+
.filter((event) => !options.workspace || event.workspace === options.workspace)
|
|
99
|
+
.filter((event) => !options.type || event.type.toLowerCase().includes(options.type.toLowerCase()))
|
|
100
|
+
.filter((event) => !options.actor || activityActorMatches(event.actor, options.actor))
|
|
101
|
+
.filter((event) => !since || Date.parse(event.timestamp) >= since)
|
|
92
102
|
.slice(-limit)
|
|
93
103
|
.reverse();
|
|
94
104
|
}
|
|
@@ -113,7 +123,7 @@ function isWebChatMessage(value) {
|
|
|
113
123
|
typeof candidate.text === "string" &&
|
|
114
124
|
typeof candidate.timestamp === "string" &&
|
|
115
125
|
["user", "agent", "system", "tool"].includes(candidate.role) &&
|
|
116
|
-
["web", "cli"].includes(candidate.source);
|
|
126
|
+
["web", "telegram", "discord", "cli"].includes(candidate.source);
|
|
117
127
|
}
|
|
118
128
|
function isWebActivityEvent(value) {
|
|
119
129
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -123,9 +133,30 @@ function isWebActivityEvent(value) {
|
|
|
123
133
|
return typeof candidate.id === "string" &&
|
|
124
134
|
typeof candidate.timestamp === "string" &&
|
|
125
135
|
typeof candidate.type === "string" &&
|
|
126
|
-
["web", "cli"].includes(candidate.source) &&
|
|
136
|
+
["web", "telegram", "discord", "cli"].includes(candidate.source) &&
|
|
127
137
|
["queued", "running", "completed", "failed", "aborted", "info"].includes(candidate.status);
|
|
128
138
|
}
|
|
139
|
+
function normalizeSince(value) {
|
|
140
|
+
if (value === undefined || value === null || value === "") {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const time = typeof value === "number" ? value : Date.parse(value);
|
|
144
|
+
return Number.isFinite(time) ? time : null;
|
|
145
|
+
}
|
|
146
|
+
function activityActorMatches(actor, query) {
|
|
147
|
+
const needle = query.trim().toLowerCase();
|
|
148
|
+
if (!needle) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return [
|
|
152
|
+
activityActorLabel(actor),
|
|
153
|
+
actor?.id,
|
|
154
|
+
actor?.username,
|
|
155
|
+
actor?.channelUserId,
|
|
156
|
+
actor?.channel,
|
|
157
|
+
].some((value) => String(value ?? "").toLowerCase().includes(needle));
|
|
158
|
+
}
|
|
129
159
|
function randomId() {
|
|
130
160
|
return randomUUID().replace(/-/g, "").slice(0, 12);
|
|
131
161
|
}
|
|
162
|
+
export { activityActorLabel, activityCategoryForType, auditCategoryForAction, } from "./activity-events.js";
|