@nordbyte/nordrelay 0.4.1 → 0.5.1
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 +155 -64
- package/README.md +81 -65
- package/dist/access-control.js +126 -115
- package/dist/agent-updates.js +62 -9
- package/dist/bot-rendering.js +838 -0
- package/dist/bot-ui.js +1 -0
- package/dist/bot.js +342 -2498
- package/dist/channel-actions.js +8 -8
- package/dist/channel-runtime.js +89 -0
- package/dist/config-metadata.js +238 -0
- package/dist/config.js +0 -58
- package/dist/index.js +8 -0
- package/dist/operations.js +63 -9
- package/dist/relay-artifact-service.js +126 -0
- package/dist/relay-external-activity-monitor.js +216 -0
- package/dist/relay-queue-service.js +66 -0
- package/dist/relay-runtime-types.js +1 -0
- package/dist/relay-runtime.js +96 -354
- package/dist/settings-service.js +2 -117
- package/dist/support-bundle.js +205 -0
- package/dist/telegram-access-commands.js +123 -0
- package/dist/telegram-access-middleware.js +129 -0
- package/dist/telegram-agent-commands.js +212 -0
- package/dist/telegram-artifact-commands.js +139 -0
- package/dist/telegram-channel-runtime.js +132 -0
- package/dist/telegram-command-menu.js +55 -0
- package/dist/telegram-command-types.js +1 -0
- package/dist/telegram-diagnostics-command.js +102 -0
- package/dist/telegram-general-commands.js +52 -0
- package/dist/telegram-operational-commands.js +153 -0
- package/dist/telegram-output.js +216 -0
- package/dist/telegram-preference-commands.js +198 -0
- package/dist/telegram-queue-commands.js +278 -0
- package/dist/telegram-support-command.js +53 -0
- package/dist/telegram-update-commands.js +93 -0
- package/dist/user-management.js +708 -0
- package/dist/web-api-contract.js +104 -0
- package/dist/web-api-types.js +1 -0
- package/dist/web-dashboard-access-routes.js +163 -0
- package/dist/web-dashboard-artifact-routes.js +65 -0
- package/dist/web-dashboard-assets.js +35 -2
- package/dist/web-dashboard-http.js +143 -0
- package/dist/web-dashboard-pages.js +257 -0
- package/dist/web-dashboard-runtime-routes.js +92 -0
- package/dist/web-dashboard-session-routes.js +209 -0
- package/dist/web-dashboard-ui.js +14 -14
- package/dist/web-dashboard.js +330 -707
- package/dist/webui-assets/dashboard.css +989 -0
- package/dist/webui-assets/dashboard.js +1750 -0
- package/dist/zip-writer.js +83 -0
- package/package.json +13 -4
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/commands/remote.md +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +227 -78
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +1 -1
- package/dist/web-dashboard-client.js +0 -275
- package/dist/web-dashboard-style.js +0 -9
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const stringToken = "${string}";
|
|
2
|
+
export const WEB_API_ROUTE_DEFINITIONS = [
|
|
3
|
+
exact("/api/auth/me", ["GET"], "inspect"),
|
|
4
|
+
exact("/api/dashboard/logout", ["POST"], "inspect"),
|
|
5
|
+
exact("/api/bootstrap", ["GET"], "inspect"),
|
|
6
|
+
exact("/api/health", ["GET"], "inspect"),
|
|
7
|
+
exact("/api/snapshot", ["GET"], "inspect"),
|
|
8
|
+
exact("/api/tasks", ["GET"], "inspect"),
|
|
9
|
+
exact("/api/progress", ["GET"], "inspect"),
|
|
10
|
+
exact("/api/version", ["GET"], "inspect"),
|
|
11
|
+
exact("/api/update", ["POST"], "updates.run"),
|
|
12
|
+
exact("/api/agent-updates", ["GET"], "updates.run"),
|
|
13
|
+
exact("/api/agent-update", ["POST"], "updates.run"),
|
|
14
|
+
dynamic("/api/agent-update/:id/log", "^/api/agent-update/[^/]+/log$", ["GET", "DELETE"], "updates.run", `/api/agent-update/${stringToken}/log`),
|
|
15
|
+
dynamic("/api/agent-update/:id/input", "^/api/agent-update/[^/]+/input$", ["POST"], "updates.run", `/api/agent-update/${stringToken}/input`),
|
|
16
|
+
dynamic("/api/agent-update/:id/cancel", "^/api/agent-update/[^/]+/cancel$", ["POST"], "updates.run", `/api/agent-update/${stringToken}/cancel`),
|
|
17
|
+
exact("/api/adapters/health", ["GET"], "inspect"),
|
|
18
|
+
exact("/api/permissions", ["GET"], "users.read"),
|
|
19
|
+
exact("/api/users", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
20
|
+
dynamic("/api/users/:id", "^/api/users/[^/]+$", ["PATCH"], "users.write", `/api/users/${stringToken}`),
|
|
21
|
+
dynamic("/api/users/:id/password", "^/api/users/[^/]+/password$", ["POST"], "users.write", `/api/users/${stringToken}/password`),
|
|
22
|
+
dynamic("/api/users/:id/sessions", "^/api/users/[^/]+/sessions$", ["GET", "DELETE"], readWrite("users.read", "users.write"), `/api/users/${stringToken}/sessions`),
|
|
23
|
+
dynamic("/api/users/:id/sessions/:sessionId", "^/api/users/[^/]+/sessions/[^/]+$", ["DELETE"], "users.write", `/api/users/${stringToken}/sessions/${stringToken}`),
|
|
24
|
+
dynamic("/api/users/:id/telegram", "^/api/users/[^/]+/telegram$", ["POST"], "users.write", `/api/users/${stringToken}/telegram`),
|
|
25
|
+
dynamic("/api/users/:id/telegram/:identityId", "^/api/users/[^/]+/telegram/[^/]+$", ["DELETE"], "users.write", `/api/users/${stringToken}/telegram/${stringToken}`),
|
|
26
|
+
exact("/api/groups", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
27
|
+
dynamic("/api/groups/:id", "^/api/groups/[^/]+$", ["PATCH"], "users.write", `/api/groups/${stringToken}`),
|
|
28
|
+
exact("/api/telegram-chats", ["GET", "POST"], readWrite("users.read", "users.write")),
|
|
29
|
+
dynamic("/api/telegram-chats/:id", "^/api/telegram-chats/[^/]+$", ["PATCH"], "users.write", `/api/telegram-chats/${stringToken}`),
|
|
30
|
+
exact("/api/audit", ["GET"], "audit.read"),
|
|
31
|
+
exact("/api/locks", ["GET", "POST", "DELETE"], readWrite("sessions.read", "sessions.write")),
|
|
32
|
+
exact("/api/auth/status", ["GET"], "inspect"),
|
|
33
|
+
exact("/api/auth/login", ["POST"], "auth.manage"),
|
|
34
|
+
exact("/api/auth/logout", ["POST"], "auth.manage"),
|
|
35
|
+
exact("/api/settings", ["GET", "PATCH"], readWrite("settings.read", "settings.write")),
|
|
36
|
+
exact("/api/control-options", ["GET"], "settings.read"),
|
|
37
|
+
exact("/api/sessions", ["GET"], "sessions.read"),
|
|
38
|
+
exact("/api/sessions/new", ["POST"], "sessions.write"),
|
|
39
|
+
exact("/api/sessions/switch", ["POST"], "sessions.write"),
|
|
40
|
+
exact("/api/sessions/attach", ["POST"], "sessions.write"),
|
|
41
|
+
exact("/api/sessions/detail", ["GET"], "sessions.read"),
|
|
42
|
+
exact("/api/agent", ["POST"], "sessions.write"),
|
|
43
|
+
exact("/api/models", ["GET"], "settings.read"),
|
|
44
|
+
exact("/api/session/model", ["POST"], "settings.write"),
|
|
45
|
+
exact("/api/session/reasoning", ["POST"], "settings.write"),
|
|
46
|
+
exact("/api/session/fast", ["POST"], "settings.write"),
|
|
47
|
+
exact("/api/session/launch", ["POST"], "settings.write"),
|
|
48
|
+
exact("/api/prompt", ["POST"], "prompt.send"),
|
|
49
|
+
exact("/api/prompt/upload", ["POST"], "prompt.send"),
|
|
50
|
+
exact("/api/abort", ["POST"], "prompt.abort"),
|
|
51
|
+
exact("/api/stop", ["POST"], "prompt.abort"),
|
|
52
|
+
exact("/api/handback", ["POST"], "sessions.write"),
|
|
53
|
+
exact("/api/retry", ["POST"], "prompt.send"),
|
|
54
|
+
exact("/api/sync", ["POST"], "sessions.write"),
|
|
55
|
+
exact("/api/queue", ["GET", "POST"], readWrite("queue.read", "queue.write")),
|
|
56
|
+
exact("/api/chat/history", ["GET", "DELETE"], readWrite("sessions.read", "sessions.write")),
|
|
57
|
+
exact("/api/activity", ["GET"], "sessions.read"),
|
|
58
|
+
exact("/api/artifacts", ["GET", "DELETE"], readWrite("files.read", "files.write")),
|
|
59
|
+
exact("/api/artifacts/bulk", ["POST"], "files.write"),
|
|
60
|
+
exact("/api/artifacts/zip", ["GET"], "files.read"),
|
|
61
|
+
exact("/api/artifacts/file", ["GET"], "files.read"),
|
|
62
|
+
exact("/api/artifacts/preview", ["GET"], "files.read"),
|
|
63
|
+
exact("/api/logs", ["GET"], "logs.read"),
|
|
64
|
+
exact("/api/logs/clear", ["POST"], "logs.clear"),
|
|
65
|
+
exact("/api/diagnostics", ["GET"], "diagnostics.read"),
|
|
66
|
+
exact("/api/diagnostics/bundle", ["GET"], "diagnostics.read"),
|
|
67
|
+
exact("/api/runtime/restart", ["POST"], "system.restart"),
|
|
68
|
+
];
|
|
69
|
+
export const WEB_API_STATIC_PATHS = WEB_API_ROUTE_DEFINITIONS
|
|
70
|
+
.filter((route) => !route.pattern)
|
|
71
|
+
.map((route) => route.path);
|
|
72
|
+
export const WEB_API_DYNAMIC_TYPE_PATHS = WEB_API_ROUTE_DEFINITIONS
|
|
73
|
+
.flatMap((route) => route.dynamicType ? [route.dynamicType] : []);
|
|
74
|
+
export function permissionForWebRequestFromContract(method, pathname) {
|
|
75
|
+
const verb = normalizeMethod(method);
|
|
76
|
+
const rule = WEB_API_ROUTE_DEFINITIONS.find((candidate) => (candidate.pattern ? new RegExp(candidate.pattern).test(pathname) : candidate.path === pathname));
|
|
77
|
+
const methods = rule?.methods ?? [];
|
|
78
|
+
if (!rule || !methods.includes(verb)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return resolvePermission(rule.permissions, verb);
|
|
82
|
+
}
|
|
83
|
+
function exact(path, methods, permissions) {
|
|
84
|
+
return { path, methods, permissions };
|
|
85
|
+
}
|
|
86
|
+
function dynamic(path, pattern, methods, permissions, dynamicType) {
|
|
87
|
+
return { path, pattern, methods, permissions, dynamicType };
|
|
88
|
+
}
|
|
89
|
+
function readWrite(read, write) {
|
|
90
|
+
return { read, write };
|
|
91
|
+
}
|
|
92
|
+
function resolvePermission(rule, verb) {
|
|
93
|
+
if (typeof rule === "string") {
|
|
94
|
+
return rule;
|
|
95
|
+
}
|
|
96
|
+
return verb === "GET" ? rule.read : rule.write;
|
|
97
|
+
}
|
|
98
|
+
function normalizeMethod(method) {
|
|
99
|
+
const upper = (method ?? "GET").toUpperCase();
|
|
100
|
+
if (upper === "GET" || upper === "POST" || upper === "PATCH" || upper === "PUT" || upper === "DELETE") {
|
|
101
|
+
return upper;
|
|
102
|
+
}
|
|
103
|
+
return "GET";
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { ALL_PERMISSIONS } from "./access-control.js";
|
|
2
|
+
import { publicUser, publicUserSnapshot, } from "./user-management.js";
|
|
3
|
+
import { arrayNumberField, arrayStringField, numberField, numberParam, optionalBooleanField, optionalNumberField, optionalStringField, readJsonBody, sendJson, stringField, } from "./web-dashboard-http.js";
|
|
4
|
+
export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
5
|
+
const { users, runtime, authUser } = options;
|
|
6
|
+
if (req.method === "GET" && url.pathname === "/api/permissions") {
|
|
7
|
+
sendJson(res, 200, { ...publicUserSnapshot(users.snapshot()), permissions: ALL_PERMISSIONS });
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (req.method === "GET" && url.pathname === "/api/users") {
|
|
11
|
+
sendJson(res, 200, { ...publicUserSnapshot(users.snapshot()), permissions: ALL_PERMISSIONS });
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (req.method === "POST" && url.pathname === "/api/users") {
|
|
15
|
+
const body = await readJsonBody(req);
|
|
16
|
+
const user = users.createUser({
|
|
17
|
+
email: stringField(body, "email"),
|
|
18
|
+
displayName: optionalStringField(body, "displayName") ?? stringField(body, "email"),
|
|
19
|
+
password: stringField(body, "password"),
|
|
20
|
+
groupIds: arrayStringField(body, "groupIds"),
|
|
21
|
+
active: optionalBooleanField(body, "active") ?? true,
|
|
22
|
+
telegramUserId: optionalNumberField(body, "telegramUserId"),
|
|
23
|
+
});
|
|
24
|
+
options.auditUserAction(authUser, "user_created", user.user.email);
|
|
25
|
+
sendJson(res, 201, { user: publicUser(user.user), groups: user.groups });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const userMatch = url.pathname.match(/^\/api\/users\/([^/]+)$/);
|
|
29
|
+
if (userMatch?.[1] && req.method === "PATCH") {
|
|
30
|
+
const body = await readJsonBody(req);
|
|
31
|
+
const user = users.updateUser(decodeURIComponent(userMatch[1]), {
|
|
32
|
+
email: optionalStringField(body, "email"),
|
|
33
|
+
displayName: optionalStringField(body, "displayName"),
|
|
34
|
+
active: optionalBooleanField(body, "active"),
|
|
35
|
+
groupIds: body.groupIds === undefined ? undefined : arrayStringField(body, "groupIds"),
|
|
36
|
+
});
|
|
37
|
+
options.auditUserAction(authUser, "user_updated", user.user.email);
|
|
38
|
+
sendJson(res, 200, { user: publicUser(user.user), groups: user.groups });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const passwordMatch = url.pathname.match(/^\/api\/users\/([^/]+)\/password$/);
|
|
42
|
+
if (passwordMatch?.[1] && req.method === "POST") {
|
|
43
|
+
const body = await readJsonBody(req);
|
|
44
|
+
const userId = decodeURIComponent(passwordMatch[1]);
|
|
45
|
+
users.setPassword(userId, stringField(body, "password"));
|
|
46
|
+
options.auditUserAction(authUser, "user_password_changed", userId);
|
|
47
|
+
sendJson(res, 200, { ok: true });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const userSessionsMatch = url.pathname.match(/^\/api\/users\/([^/]+)\/sessions$/);
|
|
51
|
+
if (userSessionsMatch?.[1] && req.method === "GET") {
|
|
52
|
+
sendJson(res, 200, { sessions: users.listWebSessions(decodeURIComponent(userSessionsMatch[1])) });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (userSessionsMatch?.[1] && req.method === "DELETE") {
|
|
56
|
+
const userId = decodeURIComponent(userSessionsMatch[1]);
|
|
57
|
+
const revoked = users.revokeUserSessions(userId);
|
|
58
|
+
options.auditUserAction(authUser, "user_session_revoked", `${userId}: ${revoked} sessions`);
|
|
59
|
+
sendJson(res, 200, { revoked });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const userSessionMatch = url.pathname.match(/^\/api\/users\/[^/]+\/sessions\/([^/]+)$/);
|
|
63
|
+
if (userSessionMatch?.[1] && req.method === "DELETE") {
|
|
64
|
+
const sessionId = decodeURIComponent(userSessionMatch[1]);
|
|
65
|
+
const revoked = users.revokeWebSession(sessionId);
|
|
66
|
+
options.auditUserAction(authUser, "user_session_revoked", sessionId);
|
|
67
|
+
sendJson(res, 200, { revoked });
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const telegramLinkMatch = url.pathname.match(/^\/api\/users\/([^/]+)\/telegram$/);
|
|
71
|
+
if (telegramLinkMatch?.[1] && req.method === "POST") {
|
|
72
|
+
const body = await readJsonBody(req);
|
|
73
|
+
if (body.createCode === true) {
|
|
74
|
+
const userId = decodeURIComponent(telegramLinkMatch[1]);
|
|
75
|
+
const linkCode = users.createTelegramLinkCode(userId);
|
|
76
|
+
options.auditUserAction(authUser, "telegram_link_created", userId);
|
|
77
|
+
sendJson(res, 201, { linkCode });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
const identity = users.linkTelegramUser(decodeURIComponent(telegramLinkMatch[1]), {
|
|
81
|
+
telegramUserId: numberField(body, "telegramUserId"),
|
|
82
|
+
username: optionalStringField(body, "username"),
|
|
83
|
+
});
|
|
84
|
+
options.auditUserAction(authUser, "telegram_linked", String(identity.telegramUserId));
|
|
85
|
+
sendJson(res, 201, { identity });
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const telegramUnlinkMatch = url.pathname.match(/^\/api\/users\/[^/]+\/telegram\/([^/]+)$/);
|
|
89
|
+
if (telegramUnlinkMatch?.[1] && req.method === "DELETE") {
|
|
90
|
+
const identityId = decodeURIComponent(telegramUnlinkMatch[1]);
|
|
91
|
+
const removed = users.unlinkTelegramIdentity(identityId);
|
|
92
|
+
options.auditUserAction(authUser, "telegram_unlinked", identityId);
|
|
93
|
+
sendJson(res, 200, { removed });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (req.method === "GET" && url.pathname === "/api/groups") {
|
|
97
|
+
sendJson(res, 200, { groups: users.listGroups() });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (req.method === "POST" && url.pathname === "/api/groups") {
|
|
101
|
+
const body = await readJsonBody(req);
|
|
102
|
+
const group = users.createGroup({
|
|
103
|
+
name: stringField(body, "name"),
|
|
104
|
+
description: optionalStringField(body, "description"),
|
|
105
|
+
permissions: arrayStringField(body, "permissions"),
|
|
106
|
+
agentIds: arrayStringField(body, "agentIds"),
|
|
107
|
+
workspaceRoots: arrayStringField(body, "workspaceRoots"),
|
|
108
|
+
telegramChatIds: arrayNumberField(body, "telegramChatIds"),
|
|
109
|
+
});
|
|
110
|
+
options.auditUserAction(authUser, "group_created", group.id);
|
|
111
|
+
sendJson(res, 201, { group });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const groupMatch = url.pathname.match(/^\/api\/groups\/([^/]+)$/);
|
|
115
|
+
if (groupMatch?.[1] && req.method === "PATCH") {
|
|
116
|
+
const body = await readJsonBody(req);
|
|
117
|
+
const group = users.updateGroup(decodeURIComponent(groupMatch[1]), {
|
|
118
|
+
name: optionalStringField(body, "name"),
|
|
119
|
+
description: optionalStringField(body, "description"),
|
|
120
|
+
permissions: body.permissions === undefined ? undefined : arrayStringField(body, "permissions"),
|
|
121
|
+
agentIds: body.agentIds === undefined ? undefined : arrayStringField(body, "agentIds"),
|
|
122
|
+
workspaceRoots: body.workspaceRoots === undefined ? undefined : arrayStringField(body, "workspaceRoots"),
|
|
123
|
+
telegramChatIds: body.telegramChatIds === undefined ? undefined : arrayNumberField(body, "telegramChatIds"),
|
|
124
|
+
});
|
|
125
|
+
options.auditUserAction(authUser, "group_updated", group.id);
|
|
126
|
+
sendJson(res, 200, { group });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (req.method === "GET" && url.pathname === "/api/telegram-chats") {
|
|
130
|
+
sendJson(res, 200, { chats: users.snapshot().telegramChats });
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (req.method === "POST" && url.pathname === "/api/telegram-chats") {
|
|
134
|
+
const body = await readJsonBody(req);
|
|
135
|
+
const chat = users.registerTelegramChat({
|
|
136
|
+
chatId: numberField(body, "chatId"),
|
|
137
|
+
title: optionalStringField(body, "title"),
|
|
138
|
+
type: optionalStringField(body, "type"),
|
|
139
|
+
enabled: optionalBooleanField(body, "enabled") ?? true,
|
|
140
|
+
allowedGroupIds: arrayStringField(body, "allowedGroupIds"),
|
|
141
|
+
});
|
|
142
|
+
options.auditUserAction(authUser, "telegram_chat_updated", String(chat.chatId));
|
|
143
|
+
sendJson(res, 201, { chat });
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
const chatMatch = url.pathname.match(/^\/api\/telegram-chats\/([^/]+)$/);
|
|
147
|
+
if (chatMatch?.[1] && req.method === "PATCH") {
|
|
148
|
+
const body = await readJsonBody(req);
|
|
149
|
+
const chat = users.updateTelegramChat(decodeURIComponent(chatMatch[1]), {
|
|
150
|
+
enabled: optionalBooleanField(body, "enabled"),
|
|
151
|
+
title: optionalStringField(body, "title"),
|
|
152
|
+
allowedGroupIds: body.allowedGroupIds === undefined ? undefined : arrayStringField(body, "allowedGroupIds"),
|
|
153
|
+
});
|
|
154
|
+
options.auditUserAction(authUser, "telegram_chat_updated", String(chat.chatId));
|
|
155
|
+
sendJson(res, 200, { chat });
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (req.method === "GET" && url.pathname === "/api/audit") {
|
|
159
|
+
sendJson(res, 200, { events: runtime.audit(numberParam(url, "limit", 50)) });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readJsonBody, requiredSearch, sendFile, sendJson, stringField, } from "./web-dashboard-http.js";
|
|
2
|
+
export async function handleDashboardArtifactRoute(req, res, url, options) {
|
|
3
|
+
const { runtime, authUser } = options;
|
|
4
|
+
if (req.method === "GET" && url.pathname === "/api/artifacts") {
|
|
5
|
+
await options.assertCurrentSessionScope(authUser);
|
|
6
|
+
sendJson(res, 200, { reports: await runtime.artifacts() });
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (req.method === "DELETE" && url.pathname === "/api/artifacts") {
|
|
10
|
+
await options.assertCurrentSessionScope(authUser);
|
|
11
|
+
sendJson(res, 200, { removed: await runtime.deleteArtifact(requiredSearch(url, "turnId")) });
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (req.method === "POST" && url.pathname === "/api/artifacts/bulk") {
|
|
15
|
+
const body = await readJsonBody(req);
|
|
16
|
+
await options.assertCurrentSessionScope(authUser);
|
|
17
|
+
const action = stringField(body, "action");
|
|
18
|
+
const turnIds = Array.isArray(body.turnIds) ? body.turnIds.filter((item) => typeof item === "string") : [];
|
|
19
|
+
if (action !== "delete") {
|
|
20
|
+
throw new Error("Unsupported artifact bulk action.");
|
|
21
|
+
}
|
|
22
|
+
const removed = [];
|
|
23
|
+
for (const turnId of turnIds) {
|
|
24
|
+
if (await runtime.deleteArtifact(turnId)) {
|
|
25
|
+
removed.push(turnId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
sendJson(res, 200, { removed });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (req.method === "GET" && url.pathname === "/api/artifacts/zip") {
|
|
32
|
+
await options.assertCurrentSessionScope(authUser);
|
|
33
|
+
const bundle = await runtime.createArtifactZip(requiredSearch(url, "turnId"));
|
|
34
|
+
if (!bundle) {
|
|
35
|
+
sendJson(res, 404, { error: "Artifact turn not found or ZIP could not be created" });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
sendFile(res, bundle.path, bundle.name);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (req.method === "GET" && url.pathname === "/api/artifacts/file") {
|
|
42
|
+
await options.assertCurrentSessionScope(authUser);
|
|
43
|
+
const turnId = requiredSearch(url, "turnId");
|
|
44
|
+
const relativePath = requiredSearch(url, "path");
|
|
45
|
+
const report = await runtime.artifact(turnId);
|
|
46
|
+
const artifact = report?.artifacts.find((candidate) => candidate.relativePath === relativePath);
|
|
47
|
+
if (!artifact) {
|
|
48
|
+
sendJson(res, 404, { error: "Artifact not found" });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
sendFile(res, artifact.localPath, artifact.name);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (req.method === "GET" && url.pathname === "/api/artifacts/preview") {
|
|
55
|
+
await options.assertCurrentSessionScope(authUser);
|
|
56
|
+
const preview = await runtime.artifactPreview(requiredSearch(url, "turnId"), requiredSearch(url, "path"));
|
|
57
|
+
if (!preview) {
|
|
58
|
+
sendJson(res, 404, { error: "Artifact not found" });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
sendJson(res, 200, preview);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
@@ -1,2 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const clientSources = [
|
|
6
|
+
"client/core/api-routes.generated.js",
|
|
7
|
+
"client/core/api-client.js",
|
|
8
|
+
"client/core/runtime.js",
|
|
9
|
+
"client/overview.js",
|
|
10
|
+
"client/events.js",
|
|
11
|
+
"client/workflows.js",
|
|
12
|
+
"client/admin.js",
|
|
13
|
+
];
|
|
14
|
+
const styleSources = [
|
|
15
|
+
"styles/theme.css",
|
|
16
|
+
"styles/components.css",
|
|
17
|
+
"styles/layout.css",
|
|
18
|
+
"styles/responsive.css",
|
|
19
|
+
];
|
|
20
|
+
export function dashboardJs() {
|
|
21
|
+
return readDashboardAsset("dashboard.js", clientSources);
|
|
22
|
+
}
|
|
23
|
+
export function dashboardCss() {
|
|
24
|
+
return readDashboardAsset("dashboard.css", styleSources);
|
|
25
|
+
}
|
|
26
|
+
function readDashboardAsset(assetName, sourceFiles) {
|
|
27
|
+
const builtAsset = path.join(moduleDir, "webui-assets", assetName);
|
|
28
|
+
if (existsSync(builtAsset)) {
|
|
29
|
+
return readFileSync(builtAsset, "utf8");
|
|
30
|
+
}
|
|
31
|
+
const sourceDir = path.join(moduleDir, "webui");
|
|
32
|
+
return sourceFiles
|
|
33
|
+
.map((file) => readFileSync(path.join(sourceDir, file), "utf8"))
|
|
34
|
+
.join("\n");
|
|
35
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
|
|
3
|
+
export function parseCookies(cookieHeader) {
|
|
4
|
+
const cookies = {};
|
|
5
|
+
for (const part of cookieHeader.split(";")) {
|
|
6
|
+
const [key, ...valueParts] = part.trim().split("=");
|
|
7
|
+
if (key)
|
|
8
|
+
cookies[key] = decodeURIComponent(valueParts.join("=") ?? "");
|
|
9
|
+
}
|
|
10
|
+
return cookies;
|
|
11
|
+
}
|
|
12
|
+
export async function readJsonBody(req) {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of req) {
|
|
15
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
16
|
+
}
|
|
17
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
18
|
+
if (!text) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
export function sendJson(res, status, value) {
|
|
24
|
+
res.writeHead(status, JSON_HEADERS);
|
|
25
|
+
res.end(`${JSON.stringify(value)}\n`);
|
|
26
|
+
}
|
|
27
|
+
export function sendText(res, status, text, contentType) {
|
|
28
|
+
res.writeHead(status, { "content-type": contentType, "cache-control": "no-store" });
|
|
29
|
+
res.end(text);
|
|
30
|
+
}
|
|
31
|
+
export function sendFile(res, filePath, filename) {
|
|
32
|
+
res.writeHead(200, {
|
|
33
|
+
"content-type": "application/octet-stream",
|
|
34
|
+
"content-disposition": `attachment; filename="${filename.replace(/"/g, "")}"`,
|
|
35
|
+
});
|
|
36
|
+
createReadStream(filePath).pipe(res);
|
|
37
|
+
}
|
|
38
|
+
export function stringField(value, key) {
|
|
39
|
+
const field = value[key];
|
|
40
|
+
if (typeof field !== "string" || !field.trim()) {
|
|
41
|
+
throw new Error(`${key} is required`);
|
|
42
|
+
}
|
|
43
|
+
return field.trim();
|
|
44
|
+
}
|
|
45
|
+
export function optionalStringField(value, key) {
|
|
46
|
+
const field = value[key];
|
|
47
|
+
return typeof field === "string" && field.trim() ? field.trim() : undefined;
|
|
48
|
+
}
|
|
49
|
+
export function optionalBooleanField(value, key) {
|
|
50
|
+
const field = value[key];
|
|
51
|
+
return typeof field === "boolean" ? field : undefined;
|
|
52
|
+
}
|
|
53
|
+
export function numberField(value, key) {
|
|
54
|
+
const field = value[key];
|
|
55
|
+
const parsed = typeof field === "number" ? field : typeof field === "string" ? Number(field) : Number.NaN;
|
|
56
|
+
if (!Number.isInteger(parsed)) {
|
|
57
|
+
throw new Error(`${key} must be an integer`);
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
export function optionalNumberField(value, key) {
|
|
62
|
+
if (value[key] === undefined || value[key] === "") {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return numberField(value, key);
|
|
66
|
+
}
|
|
67
|
+
export function arrayStringField(value, key) {
|
|
68
|
+
const field = value[key];
|
|
69
|
+
if (field === undefined || field === null || field === "") {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(field)) {
|
|
73
|
+
return field.filter((item) => typeof item === "string");
|
|
74
|
+
}
|
|
75
|
+
if (typeof field === "string") {
|
|
76
|
+
return field.split(",").map((item) => item.trim()).filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`${key} must be a string list`);
|
|
79
|
+
}
|
|
80
|
+
export function arrayNumberField(value, key) {
|
|
81
|
+
const field = value[key];
|
|
82
|
+
if (field === undefined || field === null || field === "") {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(field)) {
|
|
86
|
+
return field.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
|
|
87
|
+
}
|
|
88
|
+
if (typeof field === "string") {
|
|
89
|
+
return field.split(",").map((item) => Number(item.trim())).filter((item) => Number.isInteger(item));
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`${key} must be a number list`);
|
|
92
|
+
}
|
|
93
|
+
export function parseAgentUpdateOperation(value) {
|
|
94
|
+
if (!value || value === "update") {
|
|
95
|
+
return "update";
|
|
96
|
+
}
|
|
97
|
+
if (value === "install") {
|
|
98
|
+
return "install";
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Invalid agent update operation: ${value}`);
|
|
101
|
+
}
|
|
102
|
+
export function parseLogTarget(value) {
|
|
103
|
+
return value === "update" || value === "agent-updates" ? value : "connector";
|
|
104
|
+
}
|
|
105
|
+
export function objectRecord(value) {
|
|
106
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
export function parseUploadFiles(value) {
|
|
112
|
+
if (!Array.isArray(value)) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return value.map((item, index) => {
|
|
116
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
117
|
+
throw new Error(`files[${index}] must be an object`);
|
|
118
|
+
}
|
|
119
|
+
const record = item;
|
|
120
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : `upload-${index + 1}`;
|
|
121
|
+
const mimeType = typeof record.mimeType === "string" ? record.mimeType.trim() : undefined;
|
|
122
|
+
const dataBase64 = typeof record.dataBase64 === "string" ? record.dataBase64 : "";
|
|
123
|
+
if (!dataBase64) {
|
|
124
|
+
throw new Error(`files[${index}].dataBase64 is required`);
|
|
125
|
+
}
|
|
126
|
+
return { name, mimeType, data: Buffer.from(stripDataUrlPrefix(dataBase64), "base64") };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export function numberParam(url, key, fallback) {
|
|
130
|
+
const value = Number(url.searchParams.get(key));
|
|
131
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
132
|
+
}
|
|
133
|
+
export function requiredSearch(url, key) {
|
|
134
|
+
const value = url.searchParams.get(key);
|
|
135
|
+
if (!value) {
|
|
136
|
+
throw new Error(`${key} is required`);
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function stripDataUrlPrefix(value) {
|
|
141
|
+
const comma = value.indexOf(",");
|
|
142
|
+
return value.startsWith("data:") && comma !== -1 ? value.slice(comma + 1) : value;
|
|
143
|
+
}
|