@nordbyte/nordrelay 0.8.1 → 0.8.3
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 +84 -1205
- package/dist/{access-control.js → access/access-control.js} +1 -1
- package/dist/{audit-log.js → access/audit-log.js} +32 -15
- 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} +176 -451
- 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/channels/shared/channel-prompt-queue.js +37 -0
- package/dist/{channel-turn-service.js → channels/shared/channel-turn-service.js} +25 -11
- 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} +171 -309
- 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} +195 -430
- 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/core/pagination.js +22 -0
- package/dist/index.js +27 -23
- 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} +3 -2
- package/dist/{peer-store.js → peers/peer-store.js} +96 -9
- package/dist/{peer-types.js → peers/peer-types.js} +28 -0
- package/dist/peers/peer-web-proxy-contract.js +129 -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} +155 -53
- package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +1 -0
- package/dist/runtime/relay-runtime-active-sessions.js +387 -0
- package/dist/runtime/relay-runtime-dashboard.js +204 -0
- package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +3 -0
- package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +311 -0
- package/dist/runtime/relay-runtime-sessions.js +631 -0
- package/dist/runtime/relay-runtime-trace.js +92 -0
- package/dist/runtime/relay-runtime-types.js +1 -0
- package/dist/runtime/relay-runtime-updates-jobs.js +366 -0
- package/dist/runtime/relay-runtime.js +461 -0
- package/dist/runtime/runtime-cache.js +117 -0
- package/dist/{prompt-store.js → state/prompt-store.js} +13 -1
- 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} +19 -3
- package/dist/web/web-api-types.js +1 -0
- package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +17 -14
- package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +6 -2
- package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +25 -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} +95 -30
- package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +121 -7
- package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +8 -1
- package/dist/web/web-dashboard-security.js +14 -0
- package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +29 -13
- package/dist/web/web-dashboard-ui.js +56 -0
- package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
- package/dist/web/web-performance.js +62 -0
- package/dist/web/web-rate-limit.js +19 -0
- package/dist/{web-state.js → web/web-state.js} +107 -9
- package/dist/webui-assets/dashboard.css +398 -49
- package/dist/webui-assets/dashboard.js +1239 -103
- 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 +6 -3
- package/plugins/nordrelay/scripts/nordrelay.mjs +346 -12
- package/plugins/nordrelay/scripts/service-installer.mjs +183 -0
- package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
- package/scripts/postinstall.mjs +122 -0
- package/dist/relay-runtime.js +0 -1916
- package/dist/runtime-cache.js +0 -57
- package/dist/web-dashboard-ui.js +0 -20
- /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-client.js → peers/peer-client.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/{web-api-types.js → runtime/relay-runtime-delegate.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/{state-backend.js → state/state-backend.js} +0 -0
- /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
|
@@ -9,6 +9,7 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
9
9
|
exact("/api/progress", ["GET"], "inspect"),
|
|
10
10
|
exact("/api/metrics", ["GET"], "inspect"),
|
|
11
11
|
exact("/api/jobs", ["GET"], "inspect"),
|
|
12
|
+
exact("/api/trace", ["GET"], "sessions.read"),
|
|
12
13
|
dynamic("/api/jobs/:id/log", "^/api/jobs/[^/]+/log$", ["GET"], "inspect", `/api/jobs/${stringToken}/log`),
|
|
13
14
|
dynamic("/api/jobs/:id/action", "^/api/jobs/[^/]+/action$", ["POST"], "inspect", `/api/jobs/${stringToken}/action`),
|
|
14
15
|
exact("/api/active-sessions", ["GET"], "sessions.read"),
|
|
@@ -25,8 +26,17 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
25
26
|
exact("/api/peers/invite", ["POST"], "peers.write"),
|
|
26
27
|
exact("/api/peers/pair", ["POST"], "peers.write"),
|
|
27
28
|
exact("/api/peers/probe", ["POST"], "peers.connect"),
|
|
29
|
+
exact("/api/peers/discover", ["GET"], "peers.connect"),
|
|
30
|
+
exact("/api/peers/discovery-jobs", ["GET", "POST"], readWrite("peers.connect", "peers.connect")),
|
|
31
|
+
dynamic("/api/peers/discovery-jobs/:id", "^/api/peers/discovery-jobs/[^/]+$", ["GET"], "peers.connect", `/api/peers/discovery-jobs/${stringToken}`),
|
|
32
|
+
dynamic("/api/peers/discovery-jobs/:id/cancel", "^/api/peers/discovery-jobs/[^/]+/cancel$", ["POST"], "peers.connect", `/api/peers/discovery-jobs/${stringToken}/cancel`),
|
|
33
|
+
dynamic("/api/peers/discovery-jobs/:id/log", "^/api/peers/discovery-jobs/[^/]+/log$", ["GET"], "peers.connect", `/api/peers/discovery-jobs/${stringToken}/log`),
|
|
34
|
+
exact("/api/peers/identity/backup", ["GET"], "peers.write"),
|
|
35
|
+
exact("/api/peers/identity/restore", ["POST"], "peers.write"),
|
|
28
36
|
exact("/api/peers/global-sessions", ["GET"], "sessions.read"),
|
|
29
37
|
dynamic("/api/peers/invitations/:id", "^/api/peers/invitations/[^/]+$", ["DELETE"], "peers.write", `/api/peers/invitations/${stringToken}`),
|
|
38
|
+
dynamic("/api/peers/:id/repin", "^/api/peers/[^/]+/repin$", ["POST"], "peers.write", `/api/peers/${stringToken}/repin`),
|
|
39
|
+
dynamic("/api/peers/:id/rotate", "^/api/peers/[^/]+/rotate$", ["POST"], "peers.write", `/api/peers/${stringToken}/rotate`),
|
|
30
40
|
dynamic("/api/peers/:id/health", "^/api/peers/[^/]+/health$", ["GET"], "peers.connect", `/api/peers/${stringToken}/health`),
|
|
31
41
|
dynamic("/api/peers/:id", "^/api/peers/[^/]+$", ["PATCH", "DELETE"], "peers.write", `/api/peers/${stringToken}`),
|
|
32
42
|
dynamic("/api/peers/:id/proxy", "^/api/peers/[^/]+/proxy$", ["POST"], "peers.connect", `/api/peers/${stringToken}/proxy`),
|
|
@@ -79,6 +89,7 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
79
89
|
exact("/api/sync", ["POST"], "sessions.write"),
|
|
80
90
|
exact("/api/queue", ["GET", "POST"], readWrite("queue.read", "queue.write")),
|
|
81
91
|
exact("/api/chat/history", ["GET", "DELETE"], readWrite("sessions.read", "sessions.write")),
|
|
92
|
+
exact("/api/chat/mirror", ["GET", "POST"], readWrite("sessions.read", "settings.write")),
|
|
82
93
|
exact("/api/activity", ["GET"], "sessions.read"),
|
|
83
94
|
exact("/api/artifacts", ["GET", "DELETE"], readWrite("files.read", "files.write")),
|
|
84
95
|
exact("/api/artifacts/bulk", ["POST"], "files.write"),
|
|
@@ -96,11 +107,16 @@ export const WEB_API_STATIC_PATHS = WEB_API_ROUTE_DEFINITIONS
|
|
|
96
107
|
.map((route) => route.path);
|
|
97
108
|
export const WEB_API_DYNAMIC_TYPE_PATHS = WEB_API_ROUTE_DEFINITIONS
|
|
98
109
|
.flatMap((route) => route.dynamicType ? [route.dynamicType] : []);
|
|
110
|
+
export function routeForWebRequest(method, pathname) {
|
|
111
|
+
const verb = normalizeMethod(method);
|
|
112
|
+
const route = WEB_API_ROUTE_DEFINITIONS.find((candidate) => (candidate.pattern ? new RegExp(candidate.pattern).test(pathname) : candidate.path === pathname));
|
|
113
|
+
const methods = route?.methods ?? [];
|
|
114
|
+
return route && methods.includes(verb) ? route : null;
|
|
115
|
+
}
|
|
99
116
|
export function permissionForWebRequestFromContract(method, pathname) {
|
|
100
117
|
const verb = normalizeMethod(method);
|
|
101
|
-
const rule =
|
|
102
|
-
|
|
103
|
-
if (!rule || !methods.includes(verb)) {
|
|
118
|
+
const rule = routeForWebRequest(verb, pathname);
|
|
119
|
+
if (!rule) {
|
|
104
120
|
return null;
|
|
105
121
|
}
|
|
106
122
|
return resolvePermission(rule.permissions, verb);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ALL_PERMISSIONS } from "
|
|
2
|
-
import { publicUser, publicUserSnapshot, } from "
|
|
1
|
+
import { ALL_PERMISSIONS } from "../access/access-control.js";
|
|
2
|
+
import { publicUser, publicUserSnapshot, } from "../access/user-management.js";
|
|
3
3
|
import { arrayNumberField, arrayStringField, numberField, numberParam, optionalBooleanField, optionalNumberField, optionalStringField, readJsonBody, sendJson, stringField, } from "./web-dashboard-http.js";
|
|
4
4
|
export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
5
5
|
const { users, runtime, authUser } = options;
|
|
@@ -278,19 +278,22 @@ export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
|
278
278
|
return true;
|
|
279
279
|
}
|
|
280
280
|
if (req.method === "GET" && url.pathname === "/api/audit") {
|
|
281
|
+
const page = runtime.auditPage({
|
|
282
|
+
limit: numberParam(url, "limit", 50),
|
|
283
|
+
cursor: url.searchParams.get("cursor") || undefined,
|
|
284
|
+
channelId: (url.searchParams.get("channel") || "all"),
|
|
285
|
+
category: (url.searchParams.get("category") || "all"),
|
|
286
|
+
status: (url.searchParams.get("status") || "all"),
|
|
287
|
+
action: url.searchParams.get("action") || "all",
|
|
288
|
+
actor: url.searchParams.get("actor") || undefined,
|
|
289
|
+
agentId: url.searchParams.get("agent") || "all",
|
|
290
|
+
threadId: url.searchParams.get("thread") || undefined,
|
|
291
|
+
workspace: url.searchParams.get("workspace") || undefined,
|
|
292
|
+
since: url.searchParams.get("since") || undefined,
|
|
293
|
+
});
|
|
281
294
|
sendJson(res, 200, {
|
|
282
|
-
events:
|
|
283
|
-
|
|
284
|
-
channelId: (url.searchParams.get("channel") || "all"),
|
|
285
|
-
category: (url.searchParams.get("category") || "all"),
|
|
286
|
-
status: (url.searchParams.get("status") || "all"),
|
|
287
|
-
action: url.searchParams.get("action") || "all",
|
|
288
|
-
actor: url.searchParams.get("actor") || undefined,
|
|
289
|
-
agentId: url.searchParams.get("agent") || "all",
|
|
290
|
-
threadId: url.searchParams.get("thread") || undefined,
|
|
291
|
-
workspace: url.searchParams.get("workspace") || undefined,
|
|
292
|
-
since: url.searchParams.get("since") || undefined,
|
|
293
|
-
}),
|
|
295
|
+
events: page.items,
|
|
296
|
+
pagination: page.pagination,
|
|
294
297
|
});
|
|
295
298
|
return true;
|
|
296
299
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cursorPage, normalizeCursorLimit } from "../core/pagination.js";
|
|
2
|
+
import { numberParam, readJsonBody, requiredSearch, sendFile, sendJson, stringField, } from "./web-dashboard-http.js";
|
|
2
3
|
export async function handleDashboardArtifactRoute(req, res, url, options) {
|
|
3
4
|
const { runtime, authUser } = options;
|
|
4
5
|
if (req.method === "GET" && url.pathname === "/api/artifacts") {
|
|
5
6
|
await options.assertCurrentSessionScope(authUser);
|
|
6
|
-
|
|
7
|
+
const limit = normalizeCursorLimit(numberParam(url, "limit", 50), 50, 200);
|
|
8
|
+
const reports = await runtime.artifacts(500);
|
|
9
|
+
const page = cursorPage(reports, url.searchParams.get("cursor") || undefined, limit, (report) => report.turnId);
|
|
10
|
+
sendJson(res, 200, { reports: page.items, pagination: page.pagination });
|
|
7
11
|
return true;
|
|
8
12
|
}
|
|
9
13
|
if (req.method === "DELETE" && url.pathname === "/api/artifacts") {
|
|
@@ -6,12 +6,14 @@ const clientSources = [
|
|
|
6
6
|
"client/core/api-routes.generated.js",
|
|
7
7
|
"client/core/api-client.js",
|
|
8
8
|
"client/core/runtime.js",
|
|
9
|
+
"client/core/components.js",
|
|
9
10
|
"client/overview.js",
|
|
10
11
|
"client/events.js",
|
|
11
12
|
"client/workflows.js",
|
|
12
13
|
"client/jobs.js",
|
|
13
14
|
"client/metrics.js",
|
|
14
15
|
"client/admin.js",
|
|
16
|
+
"client/users.js",
|
|
15
17
|
"client/settings-wizard.js",
|
|
16
18
|
];
|
|
17
19
|
const styleSources = [
|
|
@@ -26,13 +28,34 @@ export function dashboardJs() {
|
|
|
26
28
|
export function dashboardCss() {
|
|
27
29
|
return readDashboardAsset("dashboard.css", styleSources);
|
|
28
30
|
}
|
|
31
|
+
const staticAssetTypes = {
|
|
32
|
+
"favicon.ico": "image/x-icon",
|
|
33
|
+
"favicon.png": "image/png",
|
|
34
|
+
"logo.png": "image/png",
|
|
35
|
+
};
|
|
36
|
+
export function dashboardStaticAsset(assetName) {
|
|
37
|
+
const contentType = staticAssetTypes[assetName];
|
|
38
|
+
if (!contentType) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const filePath = dashboardStaticAssetPath(assetName);
|
|
42
|
+
return filePath ? { filePath, contentType } : null;
|
|
43
|
+
}
|
|
29
44
|
function readDashboardAsset(assetName, sourceFiles) {
|
|
30
|
-
const builtAsset = path.
|
|
45
|
+
const builtAsset = path.resolve(moduleDir, "..", "webui-assets", assetName);
|
|
31
46
|
if (existsSync(builtAsset)) {
|
|
32
47
|
return readFileSync(builtAsset, "utf8");
|
|
33
48
|
}
|
|
34
|
-
const sourceDir = path.join(moduleDir, "
|
|
49
|
+
const sourceDir = path.join(moduleDir, "ui");
|
|
35
50
|
return sourceFiles
|
|
36
51
|
.map((file) => readFileSync(path.join(sourceDir, file), "utf8"))
|
|
37
52
|
.join("\n");
|
|
38
53
|
}
|
|
54
|
+
function dashboardStaticAssetPath(assetName) {
|
|
55
|
+
const builtAsset = path.resolve(moduleDir, "..", "webui-assets", assetName);
|
|
56
|
+
if (existsSync(builtAsset)) {
|
|
57
|
+
return builtAsset;
|
|
58
|
+
}
|
|
59
|
+
const sourceAsset = path.join(moduleDir, "ui", "assets", assetName);
|
|
60
|
+
return existsSync(sourceAsset) ? sourceAsset : null;
|
|
61
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
|
-
const
|
|
2
|
+
const DEFAULT_JSON_BODY_LIMIT = 64 * 1024 * 1024;
|
|
3
|
+
const BASE_SECURITY_HEADERS = {
|
|
4
|
+
"x-content-type-options": "nosniff",
|
|
5
|
+
"x-frame-options": "DENY",
|
|
6
|
+
"referrer-policy": "no-referrer",
|
|
7
|
+
"permissions-policy": "camera=(), microphone=(), geolocation=()",
|
|
8
|
+
};
|
|
9
|
+
const JSON_HEADERS = { ...webSecurityHeaders(), "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
|
|
3
10
|
export function parseCookies(cookieHeader) {
|
|
4
11
|
const cookies = {};
|
|
5
12
|
for (const part of cookieHeader.split(";")) {
|
|
@@ -9,10 +16,22 @@ export function parseCookies(cookieHeader) {
|
|
|
9
16
|
}
|
|
10
17
|
return cookies;
|
|
11
18
|
}
|
|
12
|
-
export
|
|
19
|
+
export class RequestBodyTooLargeError extends Error {
|
|
20
|
+
statusCode = 413;
|
|
21
|
+
}
|
|
22
|
+
export function isRequestBodyTooLargeError(error) {
|
|
23
|
+
return error instanceof RequestBodyTooLargeError;
|
|
24
|
+
}
|
|
25
|
+
export async function readJsonBody(req, maxBytes = DEFAULT_JSON_BODY_LIMIT) {
|
|
13
26
|
const chunks = [];
|
|
27
|
+
let size = 0;
|
|
14
28
|
for await (const chunk of req) {
|
|
15
|
-
|
|
29
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
30
|
+
size += buffer.length;
|
|
31
|
+
if (size > maxBytes) {
|
|
32
|
+
throw new RequestBodyTooLargeError(`Request body is too large. Max ${Math.round(maxBytes / 1024 / 1024)} MB.`);
|
|
33
|
+
}
|
|
34
|
+
chunks.push(buffer);
|
|
16
35
|
}
|
|
17
36
|
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
18
37
|
if (!text) {
|
|
@@ -24,17 +43,26 @@ export function sendJson(res, status, value) {
|
|
|
24
43
|
res.writeHead(status, JSON_HEADERS);
|
|
25
44
|
res.end(`${JSON.stringify(value)}\n`);
|
|
26
45
|
}
|
|
27
|
-
export function sendText(res, status, text, contentType) {
|
|
28
|
-
res.writeHead(status, { "content-type": contentType, "cache-control": "no-store" });
|
|
46
|
+
export function sendText(res, status, text, contentType, options = {}) {
|
|
47
|
+
res.writeHead(status, { ...webSecurityHeaders(options.cspNonce), "content-type": contentType, "cache-control": "no-store" });
|
|
29
48
|
res.end(text);
|
|
30
49
|
}
|
|
31
50
|
export function sendFile(res, filePath, filename) {
|
|
32
51
|
res.writeHead(200, {
|
|
52
|
+
...webSecurityHeaders(),
|
|
33
53
|
"content-type": "application/octet-stream",
|
|
34
54
|
"content-disposition": `attachment; filename="${filename.replace(/"/g, "")}"`,
|
|
35
55
|
});
|
|
36
56
|
createReadStream(filePath).pipe(res);
|
|
37
57
|
}
|
|
58
|
+
export function sendStaticFile(res, filePath, contentType) {
|
|
59
|
+
res.writeHead(200, {
|
|
60
|
+
...webSecurityHeaders(),
|
|
61
|
+
"content-type": contentType,
|
|
62
|
+
"cache-control": "public, max-age=86400",
|
|
63
|
+
});
|
|
64
|
+
createReadStream(filePath).pipe(res);
|
|
65
|
+
}
|
|
38
66
|
export function stringField(value, key) {
|
|
39
67
|
const field = value[key];
|
|
40
68
|
if (typeof field !== "string" || !field.trim()) {
|
|
@@ -141,3 +169,11 @@ function stripDataUrlPrefix(value) {
|
|
|
141
169
|
const comma = value.indexOf(",");
|
|
142
170
|
return value.startsWith("data:") && comma !== -1 ? value.slice(comma + 1) : value;
|
|
143
171
|
}
|
|
172
|
+
export function webSecurityHeaders(cspNonce) {
|
|
173
|
+
const scriptSrc = cspNonce ? `'self' 'nonce-${cspNonce}'` : "'self'";
|
|
174
|
+
const styleSrc = cspNonce ? `'self' 'nonce-${cspNonce}'` : "'self'";
|
|
175
|
+
return {
|
|
176
|
+
...BASE_SECURITY_HEADERS,
|
|
177
|
+
"content-security-policy": `default-src 'self'; script-src ${scriptSrc}; style-src ${styleSrc}; connect-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { renderDashboardNav } from "./web-dashboard-ui.js";
|
|
2
|
+
const faviconLinks = `
|
|
3
|
+
<link rel="icon" href="/favicon.ico" sizes="any">
|
|
4
|
+
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
|
5
|
+
<link rel="apple-touch-icon" href="/assets/logo.png">`;
|
|
2
6
|
export function renderLoginPage(options) {
|
|
7
|
+
const nonce = nonceAttr(options.cspNonce);
|
|
3
8
|
return `<!doctype html>
|
|
4
9
|
<html lang="en">
|
|
5
10
|
<head>
|
|
6
11
|
<meta charset="utf-8">
|
|
7
12
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
13
|
<title>NordRelay Login</title>
|
|
9
|
-
|
|
14
|
+
${faviconLinks}
|
|
15
|
+
<style${nonce}>
|
|
10
16
|
body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f4f5f2;color:#181c19;font-family:Inter,system-ui,-apple-system,Segoe UI,sans-serif}
|
|
11
17
|
form{width:min(420px,calc(100vw - 32px));background:white;border:1px solid #dfe3dc;border-radius:8px;padding:24px;box-shadow:0 20px 60px rgba(20,30,24,.08)}
|
|
12
18
|
h1{font-size:24px;margin:0 0 8px}
|
|
@@ -26,7 +32,7 @@ export function renderLoginPage(options) {
|
|
|
26
32
|
<button ${options.adminConfigured ? "" : "disabled"}>Sign in</button>
|
|
27
33
|
<div class="error" id="error"></div>
|
|
28
34
|
</form>
|
|
29
|
-
<script>
|
|
35
|
+
<script${nonce}>
|
|
30
36
|
document.getElementById('login').addEventListener('submit', async (event) => {
|
|
31
37
|
event.preventDefault();
|
|
32
38
|
const payload = {
|
|
@@ -45,13 +51,15 @@ export function renderLoginPage(options) {
|
|
|
45
51
|
</html>`;
|
|
46
52
|
}
|
|
47
53
|
export function renderFirstRunSetupPage(options) {
|
|
54
|
+
const nonce = nonceAttr(options.cspNonce);
|
|
48
55
|
return `<!doctype html>
|
|
49
56
|
<html lang="en">
|
|
50
57
|
<head>
|
|
51
58
|
<meta charset="utf-8">
|
|
52
59
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
53
60
|
<title>NordRelay First Run</title>
|
|
54
|
-
|
|
61
|
+
${faviconLinks}
|
|
62
|
+
<style${nonce}>
|
|
55
63
|
body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f4f5f2;color:#181c19;font-family:Inter,system-ui,-apple-system,Segoe UI,sans-serif}
|
|
56
64
|
form{width:min(460px,calc(100vw - 32px));background:white;border:1px solid #dfe3dc;border-radius:8px;padding:24px;box-shadow:0 20px 60px rgba(20,30,24,.08)}
|
|
57
65
|
h1{font-size:24px;margin:0 0 8px}
|
|
@@ -75,7 +83,7 @@ export function renderFirstRunSetupPage(options) {
|
|
|
75
83
|
<button>Create admin</button>
|
|
76
84
|
<div class="error" id="error"></div>
|
|
77
85
|
</form>
|
|
78
|
-
<script>
|
|
86
|
+
<script${nonce}>
|
|
79
87
|
document.getElementById('setup').addEventListener('submit', async (event) => {
|
|
80
88
|
event.preventDefault();
|
|
81
89
|
const payload = {
|
|
@@ -96,20 +104,23 @@ export function renderFirstRunSetupPage(options) {
|
|
|
96
104
|
</body>
|
|
97
105
|
</html>`;
|
|
98
106
|
}
|
|
99
|
-
export function renderDashboardApp() {
|
|
107
|
+
export function renderDashboardApp(options = {}) {
|
|
108
|
+
const nonce = nonceAttr(options.cspNonce);
|
|
100
109
|
return `<!doctype html>
|
|
101
110
|
<html lang="en">
|
|
102
111
|
<head>
|
|
103
112
|
<meta charset="utf-8">
|
|
104
113
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
105
114
|
<title>NordRelay Dashboard</title>
|
|
106
|
-
|
|
115
|
+
${faviconLinks}
|
|
116
|
+
<script${nonce}>document.documentElement.dataset.theme = localStorage.getItem('nordrelayTheme') || 'light';</script>
|
|
107
117
|
<link rel="stylesheet" href="/assets/dashboard.css">
|
|
108
118
|
</head>
|
|
109
119
|
<body>
|
|
110
120
|
<div class="app">
|
|
111
121
|
<aside class="sidebar" id="sidebar">
|
|
112
|
-
<div class="brand"><
|
|
122
|
+
<div class="brand"><img class="brand-mark" src="/assets/logo.png" alt="" width="44" height="44" aria-hidden="true"><div><strong>NordRelay</strong><small>Remote control</small></div></div>
|
|
123
|
+
<div class="brand-separator" aria-hidden="true"></div>
|
|
113
124
|
<nav>
|
|
114
125
|
${renderDashboardNav()}
|
|
115
126
|
</nav>
|
|
@@ -126,7 +137,6 @@ export function renderDashboardApp() {
|
|
|
126
137
|
<select id="peerSelect" title="NordRelay target"></select>
|
|
127
138
|
<select id="agentSelect"></select>
|
|
128
139
|
<button id="themeBtn" class="secondary" title="Toggle dark theme">Dark</button>
|
|
129
|
-
<button id="refreshBtn">Refresh</button>
|
|
130
140
|
<button id="logoutBtn" class="secondary">Logout</button>
|
|
131
141
|
</div>
|
|
132
142
|
</header>
|
|
@@ -143,7 +153,7 @@ export function renderDashboardApp() {
|
|
|
143
153
|
</section>
|
|
144
154
|
|
|
145
155
|
<section class="page" id="page-chat">
|
|
146
|
-
<div class="chat-layout">
|
|
156
|
+
<div class="chat-layout tools-hidden" id="chatLayout">
|
|
147
157
|
<div class="panel chat-panel">
|
|
148
158
|
<div class="chat-toolbar">
|
|
149
159
|
<button id="newSessionBtn">New session</button>
|
|
@@ -151,9 +161,19 @@ export function renderDashboardApp() {
|
|
|
151
161
|
<button id="editLastBtn" class="secondary">Edit last</button>
|
|
152
162
|
<button id="syncBtn" class="secondary">Sync</button>
|
|
153
163
|
<button id="notifyBtn" class="secondary">Notify</button>
|
|
164
|
+
<label class="mirror-control" title="Mirror local CLI activity into this WebUI chat">
|
|
165
|
+
Mirror
|
|
166
|
+
<select id="mirrorModeSelect">
|
|
167
|
+
<option value="off">Off</option>
|
|
168
|
+
<option value="status">Status</option>
|
|
169
|
+
<option value="final">Final</option>
|
|
170
|
+
<option value="full">Full</option>
|
|
171
|
+
</select>
|
|
172
|
+
</label>
|
|
154
173
|
<button id="clearChatBtn" class="secondary">Clear history</button>
|
|
155
174
|
<button id="abortBtn">Abort</button>
|
|
156
175
|
<button id="handbackBtn">Handback</button>
|
|
176
|
+
<button id="toggleToolsBtn" class="secondary" type="button" aria-controls="toolPanel" aria-expanded="false">Show Tools</button>
|
|
157
177
|
</div>
|
|
158
178
|
<div class="control-grid" id="sessionControls"></div>
|
|
159
179
|
<div id="messages" class="messages"></div>
|
|
@@ -171,7 +191,7 @@ export function renderDashboardApp() {
|
|
|
171
191
|
<button>Send</button>
|
|
172
192
|
</form>
|
|
173
193
|
</div>
|
|
174
|
-
<div class="panel side-panel"><h2>Tools / Plan</h2><div id="toolStream" class="tool-stream"></div></div>
|
|
194
|
+
<div class="panel side-panel" id="toolPanel" hidden><h2>Tools / Plan</h2><div id="toolStream" class="tool-stream"></div></div>
|
|
175
195
|
</div>
|
|
176
196
|
</section>
|
|
177
197
|
|
|
@@ -179,6 +199,7 @@ export function renderDashboardApp() {
|
|
|
179
199
|
<div class="panel">
|
|
180
200
|
<div class="row"><button id="reloadTasksBtn">Reload tasks</button></div>
|
|
181
201
|
<div id="tasksList" class="list"></div>
|
|
202
|
+
<div id="jobsPager" class="pager"></div>
|
|
182
203
|
</div>
|
|
183
204
|
</section>
|
|
184
205
|
|
|
@@ -211,6 +232,14 @@ export function renderDashboardApp() {
|
|
|
211
232
|
<div class="panel">
|
|
212
233
|
<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>
|
|
213
234
|
<div id="activityList" class="list"></div>
|
|
235
|
+
<div id="activityPager" class="pager"></div>
|
|
236
|
+
</div>
|
|
237
|
+
</section>
|
|
238
|
+
|
|
239
|
+
<section class="page" id="page-trace">
|
|
240
|
+
<div class="panel">
|
|
241
|
+
<div class="row"><input id="traceCorrelationId" placeholder="Correlation ID"><button id="loadTraceBtn">Load trace</button></div>
|
|
242
|
+
<div id="traceDetail" class="list"></div>
|
|
214
243
|
</div>
|
|
215
244
|
</section>
|
|
216
245
|
|
|
@@ -219,6 +248,7 @@ export function renderDashboardApp() {
|
|
|
219
248
|
<div class="row"><button id="reloadArtifactsBtn">Reload artifacts</button><input id="artifactSearch" placeholder="Search artifacts"><select id="artifactKind"><option value="all">All files</option><option value="images">Images</option><option value="docs">Docs/code</option></select><button id="zipSelectedArtifactsBtn" class="secondary">ZIP selected</button><button id="deleteSelectedArtifactsBtn" class="danger">Delete selected</button></div>
|
|
220
249
|
<div id="artifactPreview" class="preview"></div>
|
|
221
250
|
<div id="artifactList" class="list"></div>
|
|
251
|
+
<div id="artifactPager" class="pager"></div>
|
|
222
252
|
</div>
|
|
223
253
|
</section>
|
|
224
254
|
|
|
@@ -233,10 +263,13 @@ export function renderDashboardApp() {
|
|
|
233
263
|
|
|
234
264
|
<section class="page" id="page-peers">
|
|
235
265
|
<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>
|
|
266
|
+
<div class="row"><button id="loadPeersBtn">Reload peers</button><button id="createPeerInviteBtn">Create invite</button><button id="addPeerBtn" class="secondary">Add peer</button><button id="discoverPeersBtn" class="secondary">Discover LAN peers</button><button id="cancelPeerDiscoveryBtn" class="secondary">Cancel discovery</button><button id="exportPeerIdentityBtn" class="secondary">Export identity</button><button id="restorePeerIdentityBtn" class="secondary">Restore identity</button></div>
|
|
267
|
+
<div class="row"><input id="peerDiscoveryTargets" placeholder="Optional targets: 192.168.178.0/24, 192.168.178.10-50, host.local, https://host:31979"><input id="peerDiscoveryMaxHosts" type="number" min="1" max="65536" value="512" title="Max hosts"><input id="peerDiscoveryConcurrency" type="number" min="1" max="128" value="32" title="Concurrency"></div>
|
|
237
268
|
<div id="peerStatus" class="list"></div>
|
|
238
269
|
<h2>Configured peers</h2>
|
|
239
270
|
<div id="peersList" class="list"></div>
|
|
271
|
+
<h2>LAN discovery</h2>
|
|
272
|
+
<div id="peerDiscovery" class="list"></div>
|
|
240
273
|
<h2>Open invitations</h2>
|
|
241
274
|
<div id="peerInvites" class="list"></div>
|
|
242
275
|
</div>
|
|
@@ -244,49 +277,71 @@ export function renderDashboardApp() {
|
|
|
244
277
|
|
|
245
278
|
<section class="page" id="page-access">
|
|
246
279
|
<div class="panel">
|
|
247
|
-
<div class="
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
280
|
+
<div class="section-header access-section-header">
|
|
281
|
+
<div id="accessTabs" class="section-tabs access-tabs" role="tablist" aria-label="Users sections">
|
|
282
|
+
<button type="button" role="tab" aria-selected="true" tabindex="0" data-access-tab="users" class="active">Users</button>
|
|
283
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="groups">Groups</button>
|
|
284
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="telegram">Telegram</button>
|
|
285
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="discord">Discord</button>
|
|
286
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="slack">Slack</button>
|
|
287
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="locks">Locks</button>
|
|
288
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="audit">Audit</button>
|
|
289
|
+
</div>
|
|
256
290
|
</div>
|
|
257
291
|
<div class="access-tab active" data-access-tab-panel="users">
|
|
258
|
-
<div
|
|
292
|
+
<div class="access-tab-heading">
|
|
293
|
+
<div class="row access-heading-actions"><button id="loadAccessBtn" class="secondary">Reload</button><button id="createUserBtn">Create user</button></div>
|
|
294
|
+
<div class="access-filter-row">
|
|
295
|
+
<input id="userSearch" placeholder="Search users">
|
|
296
|
+
<select id="userStatusFilter"><option value="all">All statuses</option><option value="active">Active</option><option value="disabled">Disabled</option></select>
|
|
297
|
+
<select id="userGroupFilter"><option value="all">All groups</option></select>
|
|
298
|
+
<select id="userIdentityFilter"><option value="all">All identities</option><option value="telegram">Telegram linked</option><option value="discord">Discord linked</option><option value="slack">Slack linked</option><option value="web">Web sessions</option><option value="unlinked">No chat identity</option></select>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div id="accessPanel" class="list user-list"></div>
|
|
302
|
+
<div id="usersPager" class="pager"></div>
|
|
259
303
|
</div>
|
|
260
304
|
<div class="access-tab" data-access-tab-panel="groups">
|
|
261
|
-
<
|
|
305
|
+
<div class="access-tab-heading">
|
|
306
|
+
<div class="row access-heading-actions"><button id="createGroupBtn" class="secondary">Create group</button></div>
|
|
307
|
+
<div class="access-filter-row"><input id="groupSearch" placeholder="Search groups"></div>
|
|
308
|
+
</div>
|
|
262
309
|
<div id="groupsList" class="list"></div>
|
|
263
310
|
</div>
|
|
264
311
|
<div class="access-tab" data-access-tab-panel="telegram">
|
|
265
|
-
<
|
|
312
|
+
<div class="access-tab-heading">
|
|
313
|
+
<div class="row access-heading-actions"><button id="createChatBtn" class="secondary">Add Telegram chat</button></div>
|
|
314
|
+
<input id="telegramChatSearch" placeholder="Search Telegram chats">
|
|
315
|
+
</div>
|
|
266
316
|
<div id="telegramChatsList" class="list"></div>
|
|
267
317
|
</div>
|
|
268
318
|
<div class="access-tab" data-access-tab-panel="discord">
|
|
269
319
|
<div class="access-tab-heading">
|
|
270
|
-
<
|
|
320
|
+
<div class="row access-heading-actions"><button id="createDiscordChannelBtn" class="secondary">Add Discord channel</button></div>
|
|
271
321
|
<input id="discordChannelSearch" placeholder="Search Discord channels">
|
|
272
322
|
</div>
|
|
273
323
|
<div id="discordChannelsList" class="list"></div>
|
|
274
324
|
</div>
|
|
275
325
|
<div class="access-tab" data-access-tab-panel="slack">
|
|
276
326
|
<div class="access-tab-heading">
|
|
277
|
-
<
|
|
327
|
+
<div class="row access-heading-actions"><button id="createSlackChannelBtn" class="secondary">Add Slack channel</button></div>
|
|
278
328
|
<input id="slackChannelSearch" placeholder="Search Slack channels">
|
|
279
329
|
</div>
|
|
280
330
|
<div id="slackChannelsList" class="list"></div>
|
|
281
331
|
</div>
|
|
282
332
|
<div class="access-tab" data-access-tab-panel="locks">
|
|
283
|
-
<
|
|
333
|
+
<div class="access-tab-heading">
|
|
334
|
+
<div class="row access-heading-actions"><button id="lockSessionBtn" class="secondary">Lock web session</button><button id="unlockSessionBtn" class="secondary">Unlock web session</button></div>
|
|
335
|
+
</div>
|
|
284
336
|
<div id="locksList" class="list"></div>
|
|
285
337
|
</div>
|
|
286
338
|
<div class="access-tab" data-access-tab-panel="audit">
|
|
287
|
-
<
|
|
288
|
-
|
|
339
|
+
<div class="access-tab-heading">
|
|
340
|
+
<div class="row access-heading-actions"><button id="loadAuditBtn">Load audit</button><button id="exportAuditBtn" class="secondary">Export</button></div>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="row audit-filter-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"></div>
|
|
289
343
|
<div id="auditList" class="list"></div>
|
|
344
|
+
<div id="auditPager" class="pager"></div>
|
|
290
345
|
</div>
|
|
291
346
|
</div>
|
|
292
347
|
</section>
|
|
@@ -302,8 +357,11 @@ export function renderDashboardApp() {
|
|
|
302
357
|
|
|
303
358
|
<section class="page" id="page-settings">
|
|
304
359
|
<div class="panel">
|
|
305
|
-
<div
|
|
306
|
-
|
|
360
|
+
<div id="settingsTabHeader" class="section-header settings-section-header">
|
|
361
|
+
<div id="settingsTabs" class="section-tabs settings-tabs" role="tablist" aria-label="Settings sections"></div>
|
|
362
|
+
</div>
|
|
363
|
+
<div id="settingsSubnav" class="settings-subnav" hidden></div>
|
|
364
|
+
<div id="settingsActions" class="row settings-actions"><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>
|
|
307
365
|
<div id="settingsForm" class="settings-grid"></div>
|
|
308
366
|
</div>
|
|
309
367
|
</section>
|
|
@@ -348,6 +406,10 @@ export function renderDashboardApp() {
|
|
|
348
406
|
<div id="sessionDetail"></div>
|
|
349
407
|
<div class="row dialog-actions"><button id="closeSessionDetailBtn" class="secondary">Close</button></div>
|
|
350
408
|
</dialog>
|
|
409
|
+
<dialog id="userDetailDialog">
|
|
410
|
+
<div id="userDetail"></div>
|
|
411
|
+
<div class="row dialog-actions"><button id="closeUserDetailBtn" class="secondary">Close</button></div>
|
|
412
|
+
</dialog>
|
|
351
413
|
<dialog id="adminDialog">
|
|
352
414
|
<form method="dialog" id="adminDialogForm">
|
|
353
415
|
<h2 id="adminDialogTitle">Edit</h2>
|
|
@@ -361,3 +423,6 @@ export function renderDashboardApp() {
|
|
|
361
423
|
</body>
|
|
362
424
|
</html>`;
|
|
363
425
|
}
|
|
426
|
+
function nonceAttr(cspNonce) {
|
|
427
|
+
return cspNonce ? ` nonce="${cspNonce.replace(/"/g, "")}"` : "";
|
|
428
|
+
}
|