@naisys/supervisor 3.0.0-beta.6

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.
Files changed (57) hide show
  1. package/bin/naisys-supervisor +2 -0
  2. package/client-dist/android-chrome-192x192.png +0 -0
  3. package/client-dist/android-chrome-512x512.png +0 -0
  4. package/client-dist/apple-touch-icon.png +0 -0
  5. package/client-dist/assets/index-BBrK4ItN.js +177 -0
  6. package/client-dist/assets/index-CKg0vgt5.css +1 -0
  7. package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
  8. package/client-dist/favicon-16x16.png +0 -0
  9. package/client-dist/favicon-32x32.png +0 -0
  10. package/client-dist/favicon.ico +0 -0
  11. package/client-dist/index.html +49 -0
  12. package/client-dist/site.webmanifest +22 -0
  13. package/dist/api-reference.js +52 -0
  14. package/dist/auth-middleware.js +116 -0
  15. package/dist/database/hubDb.js +26 -0
  16. package/dist/database/supervisorDb.js +18 -0
  17. package/dist/error-helpers.js +13 -0
  18. package/dist/hateoas.js +61 -0
  19. package/dist/logger.js +11 -0
  20. package/dist/route-helpers.js +7 -0
  21. package/dist/routes/admin.js +209 -0
  22. package/dist/routes/agentChat.js +194 -0
  23. package/dist/routes/agentConfig.js +265 -0
  24. package/dist/routes/agentLifecycle.js +350 -0
  25. package/dist/routes/agentMail.js +171 -0
  26. package/dist/routes/agentRuns.js +90 -0
  27. package/dist/routes/agents.js +236 -0
  28. package/dist/routes/api.js +52 -0
  29. package/dist/routes/attachments.js +18 -0
  30. package/dist/routes/auth.js +103 -0
  31. package/dist/routes/costs.js +51 -0
  32. package/dist/routes/hosts.js +296 -0
  33. package/dist/routes/models.js +152 -0
  34. package/dist/routes/root.js +56 -0
  35. package/dist/routes/schemas.js +31 -0
  36. package/dist/routes/status.js +20 -0
  37. package/dist/routes/users.js +420 -0
  38. package/dist/routes/variables.js +103 -0
  39. package/dist/schema-registry.js +23 -0
  40. package/dist/services/agentConfigService.js +182 -0
  41. package/dist/services/agentHostStatusService.js +178 -0
  42. package/dist/services/agentService.js +291 -0
  43. package/dist/services/attachmentProxyService.js +130 -0
  44. package/dist/services/browserSocketService.js +78 -0
  45. package/dist/services/chatService.js +201 -0
  46. package/dist/services/configExportService.js +61 -0
  47. package/dist/services/costsService.js +127 -0
  48. package/dist/services/hostService.js +156 -0
  49. package/dist/services/hubConnectionService.js +333 -0
  50. package/dist/services/logFileService.js +11 -0
  51. package/dist/services/mailService.js +154 -0
  52. package/dist/services/modelService.js +92 -0
  53. package/dist/services/runsService.js +164 -0
  54. package/dist/services/userService.js +147 -0
  55. package/dist/services/variableService.js +23 -0
  56. package/dist/supervisorServer.js +221 -0
  57. package/package.json +79 -0
@@ -0,0 +1,116 @@
1
+ import { AuthCache } from "@naisys/common";
2
+ import { extractBearerToken, hashToken, SESSION_COOKIE_NAME, } from "@naisys/common-node";
3
+ import { findAgentByApiKey } from "@naisys/hub-database";
4
+ import { findSession, findUserByApiKey } from "@naisys/supervisor-database";
5
+ import { createUserForAgent, getUserByUuid, getUserPermissions, } from "./services/userService.js";
6
+ const PUBLIC_PREFIXES = ["/api/supervisor/auth/login"];
7
+ export const authCache = new AuthCache();
8
+ function isPublicRoute(url) {
9
+ if (url === "/api/supervisor/" || url === "/api/supervisor")
10
+ return true;
11
+ for (const prefix of PUBLIC_PREFIXES) {
12
+ if (url.startsWith(prefix))
13
+ return true;
14
+ }
15
+ // Non-supervisor-API paths (static files, ERP routes, etc.)
16
+ if (!url.startsWith("/api/supervisor"))
17
+ return true;
18
+ return false;
19
+ }
20
+ async function buildSupervisorUser(id, username, uuid) {
21
+ const permissions = await getUserPermissions(id);
22
+ return { id, username, uuid, permissions };
23
+ }
24
+ export async function resolveUserFromToken(token) {
25
+ const tokenHash = hashToken(token);
26
+ const cacheKey = `cookie:${tokenHash}`;
27
+ const cached = authCache.get(cacheKey);
28
+ if (cached !== undefined)
29
+ return cached;
30
+ const session = await findSession(tokenHash);
31
+ if (!session) {
32
+ authCache.set(cacheKey, null);
33
+ return null;
34
+ }
35
+ const user = await buildSupervisorUser(session.userId, session.username, session.uuid);
36
+ authCache.set(cacheKey, user);
37
+ return user;
38
+ }
39
+ export async function resolveUserFromApiKey(apiKey) {
40
+ const apiKeyHash = hashToken(apiKey);
41
+ const cacheKey = `apikey:${apiKeyHash}`;
42
+ const cached = authCache.get(cacheKey);
43
+ if (cached !== undefined)
44
+ return cached;
45
+ // Try supervisor DB first (human users), then hub DB (agents)
46
+ const match = (await findUserByApiKey(apiKey)) ?? (await findAgentByApiKey(apiKey));
47
+ if (!match) {
48
+ authCache.set(cacheKey, null);
49
+ return null;
50
+ }
51
+ let localUser = await getUserByUuid(match.uuid);
52
+ if (!localUser) {
53
+ localUser = await createUserForAgent(match.username, match.uuid);
54
+ }
55
+ const user = await buildSupervisorUser(localUser.id, localUser.username, localUser.uuid);
56
+ authCache.set(cacheKey, user);
57
+ return user;
58
+ }
59
+ export function registerAuthMiddleware(fastify) {
60
+ const publicRead = process.env.PUBLIC_READ === "true";
61
+ fastify.decorateRequest("supervisorUser", undefined);
62
+ fastify.addHook("onRequest", async (request, reply) => {
63
+ const token = request.cookies?.[SESSION_COOKIE_NAME];
64
+ if (token) {
65
+ const user = await resolveUserFromToken(token);
66
+ if (user)
67
+ request.supervisorUser = user;
68
+ }
69
+ // API key auth (for agents / machine-to-machine)
70
+ if (!request.supervisorUser) {
71
+ const apiKey = extractBearerToken(request.headers.authorization);
72
+ if (apiKey) {
73
+ const user = await resolveUserFromApiKey(apiKey);
74
+ if (user)
75
+ request.supervisorUser = user;
76
+ }
77
+ }
78
+ if (request.supervisorUser)
79
+ return; // Authenticated
80
+ if (isPublicRoute(request.url))
81
+ return; // Public route
82
+ if (publicRead && request.method === "GET")
83
+ return; // Public read mode
84
+ reply.status(401).send({
85
+ statusCode: 401,
86
+ error: "Unauthorized",
87
+ message: "Authentication required",
88
+ });
89
+ });
90
+ }
91
+ export function hasPermission(user, permission) {
92
+ return ((user?.permissions.includes(permission) ||
93
+ user?.permissions.includes("supervisor_admin")) ??
94
+ false);
95
+ }
96
+ export function requirePermission(permission) {
97
+ return async (request, reply) => {
98
+ if (!request.supervisorUser) {
99
+ reply.status(401).send({
100
+ statusCode: 401,
101
+ error: "Unauthorized",
102
+ message: "Authentication required",
103
+ });
104
+ return;
105
+ }
106
+ if (!hasPermission(request.supervisorUser, permission)) {
107
+ reply.status(403).send({
108
+ statusCode: 403,
109
+ error: "Forbidden",
110
+ message: `Permission '${permission}' required`,
111
+ });
112
+ return;
113
+ }
114
+ };
115
+ }
116
+ //# sourceMappingURL=auth-middleware.js.map
@@ -0,0 +1,26 @@
1
+ import { createPrismaClient } from "@naisys/hub-database";
2
+ import path from "path";
3
+ import { env } from "process";
4
+ export function getNaisysDatabasePath() {
5
+ if (!env.NAISYS_FOLDER) {
6
+ throw new Error("NAISYS_FOLDER environment variable is not set.");
7
+ }
8
+ const dbFilename = "naisys_hub.db";
9
+ return path.join(env.NAISYS_FOLDER, "database", dbFilename);
10
+ }
11
+ let _db;
12
+ /** Lazily initialized Prisma client. First access creates the connection. */
13
+ export const hubDb = new Proxy({}, {
14
+ get(_target, prop, receiver) {
15
+ if (!_db) {
16
+ throw new Error("hubDb accessed before initialization. Ensure migrations have completed first.");
17
+ }
18
+ return Reflect.get(_db, prop, receiver);
19
+ },
20
+ });
21
+ export async function initHubDb() {
22
+ if (!_db) {
23
+ _db = await createPrismaClient(getNaisysDatabasePath());
24
+ }
25
+ }
26
+ //# sourceMappingURL=hubDb.js.map
@@ -0,0 +1,18 @@
1
+ import { createPrismaClient, supervisorDbPath, } from "@naisys/supervisor-database";
2
+ let _db;
3
+ /** Lazily initialized Prisma client. First access creates the connection. */
4
+ const supervisorDb = new Proxy({}, {
5
+ get(_target, prop, receiver) {
6
+ if (!_db) {
7
+ throw new Error("supervisorDb accessed before initialization. Ensure migrations have completed first.");
8
+ }
9
+ return Reflect.get(_db, prop, receiver);
10
+ },
11
+ });
12
+ export async function initSupervisorDb() {
13
+ if (!_db) {
14
+ _db = await createPrismaClient(supervisorDbPath());
15
+ }
16
+ }
17
+ export default supervisorDb;
18
+ //# sourceMappingURL=supervisorDb.js.map
@@ -0,0 +1,13 @@
1
+ export function notFound(reply, message) {
2
+ reply.status(404);
3
+ return { success: false, message };
4
+ }
5
+ export function badRequest(reply, message) {
6
+ reply.status(400);
7
+ return { success: false, message };
8
+ }
9
+ export function conflict(reply, message) {
10
+ reply.status(409);
11
+ return { success: false, message };
12
+ }
13
+ //# sourceMappingURL=error-helpers.js.map
@@ -0,0 +1,61 @@
1
+ export const API_PREFIX = "/api/supervisor";
2
+ export function selfLink(path, title) {
3
+ return { rel: "self", href: `${API_PREFIX}${path}`, title };
4
+ }
5
+ export function collectionLink(resource) {
6
+ return {
7
+ rel: "collection",
8
+ href: `${API_PREFIX}/${resource}`,
9
+ title: resource,
10
+ };
11
+ }
12
+ export function schemaLink(schemaName) {
13
+ return {
14
+ rel: "schema",
15
+ href: `${API_PREFIX}/schemas/${schemaName}`,
16
+ };
17
+ }
18
+ function buildQuery(page, pageSize, filters) {
19
+ const params = new URLSearchParams();
20
+ params.set("page", String(page));
21
+ params.set("pageSize", String(pageSize));
22
+ if (filters) {
23
+ for (const [key, value] of Object.entries(filters)) {
24
+ if (value !== undefined)
25
+ params.set(key, value);
26
+ }
27
+ }
28
+ return params.toString();
29
+ }
30
+ export function paginationLinks(basePath, page, pageSize, total, filters) {
31
+ const fullPath = `${API_PREFIX}/${basePath}`;
32
+ const totalPages = Math.ceil(total / pageSize);
33
+ const links = [
34
+ {
35
+ rel: "self",
36
+ href: `${fullPath}?${buildQuery(page, pageSize, filters)}`,
37
+ },
38
+ {
39
+ rel: "first",
40
+ href: `${fullPath}?${buildQuery(1, pageSize, filters)}`,
41
+ },
42
+ {
43
+ rel: "last",
44
+ href: `${fullPath}?${buildQuery(Math.max(1, totalPages), pageSize, filters)}`,
45
+ },
46
+ ];
47
+ if (page > 1) {
48
+ links.push({
49
+ rel: "prev",
50
+ href: `${fullPath}?${buildQuery(page - 1, pageSize, filters)}`,
51
+ });
52
+ }
53
+ if (page < totalPages) {
54
+ links.push({
55
+ rel: "next",
56
+ href: `${fullPath}?${buildQuery(page + 1, pageSize, filters)}`,
57
+ });
58
+ }
59
+ return links;
60
+ }
61
+ //# sourceMappingURL=hateoas.js.map
package/dist/logger.js ADDED
@@ -0,0 +1,11 @@
1
+ let _logger;
2
+ export function initLogger(logger) {
3
+ _logger = logger;
4
+ }
5
+ export function getLogger() {
6
+ if (!_logger) {
7
+ throw new Error("Logger not initialized. Call initLogger() first.");
8
+ }
9
+ return _logger;
10
+ }
11
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,7 @@
1
+ import { permGate, resolveActions as resolveActionsBase, } from "@naisys/common";
2
+ import { hasPermission } from "./auth-middleware.js";
3
+ export { permGate };
4
+ export function resolveActions(defs, baseHref, ctx) {
5
+ return resolveActionsBase(defs, baseHref, ctx, (perm) => hasPermission(ctx.user, perm));
6
+ }
7
+ //# sourceMappingURL=route-helpers.js.map
@@ -0,0 +1,209 @@
1
+ import fs from "node:fs/promises";
2
+ import { supervisorDbPath } from "@naisys/supervisor-database";
3
+ import { AdminAttachmentListRequestSchema, AdminAttachmentListResponseSchema, AdminInfoResponseSchema, ErrorResponseSchema, RotateAccessKeyResultSchema, ServerLogRequestSchema, ServerLogResponseSchema, } from "@naisys/supervisor-shared";
4
+ import archiver from "archiver";
5
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
6
+ import { getNaisysDatabasePath, hubDb } from "../database/hubDb.js";
7
+ import { API_PREFIX, paginationLinks } from "../hateoas.js";
8
+ import { buildExportFiles, } from "../services/configExportService.js";
9
+ import { getHubAccessKey, isHubConnected, sendRotateAccessKey, } from "../services/hubConnectionService.js";
10
+ import { getLogFilePath, tailLogFile } from "../services/logFileService.js";
11
+ function adminActions(hasAdminPermission) {
12
+ const actions = [];
13
+ if (hasAdminPermission) {
14
+ actions.push({
15
+ rel: "export-config",
16
+ href: `${API_PREFIX}/admin/export-config`,
17
+ method: "GET",
18
+ title: "Export Config",
19
+ }, {
20
+ rel: "view-logs",
21
+ href: `${API_PREFIX}/admin/logs`,
22
+ method: "GET",
23
+ title: "View Logs",
24
+ }, {
25
+ rel: "view-attachments",
26
+ href: `${API_PREFIX}/admin/attachments`,
27
+ method: "GET",
28
+ title: "View Attachments",
29
+ });
30
+ if (getHubAccessKey()) {
31
+ actions.push({
32
+ rel: "rotate-access-key",
33
+ href: `${API_PREFIX}/admin/rotate-access-key`,
34
+ method: "POST",
35
+ title: "Rotate Hub Access Key",
36
+ });
37
+ }
38
+ }
39
+ return actions;
40
+ }
41
+ export default function adminRoutes(fastify, _options) {
42
+ // GET / — Admin info
43
+ fastify.get("/", {
44
+ preHandler: [requirePermission("supervisor_admin")],
45
+ schema: {
46
+ description: "Get admin system info",
47
+ tags: ["Admin"],
48
+ response: {
49
+ 200: AdminInfoResponseSchema,
50
+ 500: ErrorResponseSchema,
51
+ },
52
+ security: [{ cookieAuth: [] }],
53
+ },
54
+ }, async (request, _reply) => {
55
+ const hasAdminPermission = hasPermission(request.supervisorUser, "supervisor_admin");
56
+ const actions = adminActions(hasAdminPermission);
57
+ const [supervisorDbSize, hubDbSize] = await Promise.all([
58
+ fs
59
+ .stat(supervisorDbPath())
60
+ .then((s) => s.size)
61
+ .catch(() => undefined),
62
+ fs
63
+ .stat(getNaisysDatabasePath())
64
+ .then((s) => s.size)
65
+ .catch(() => undefined),
66
+ ]);
67
+ return {
68
+ supervisorDbPath: supervisorDbPath(),
69
+ supervisorDbSize,
70
+ hubDbPath: getNaisysDatabasePath(),
71
+ hubDbSize,
72
+ hubConnected: isHubConnected(),
73
+ hubAccessKey: getHubAccessKey(),
74
+ _actions: actions.length > 0 ? actions : undefined,
75
+ };
76
+ });
77
+ // GET /export-config — Download config zip
78
+ fastify.get("/export-config", {
79
+ preHandler: [requirePermission("supervisor_admin")],
80
+ schema: {
81
+ description: "Export configuration as a zip file",
82
+ tags: ["Admin"],
83
+ security: [{ cookieAuth: [] }],
84
+ },
85
+ }, async (_request, reply) => {
86
+ const users = (await hubDb.users.findMany({
87
+ select: {
88
+ id: true,
89
+ username: true,
90
+ title: true,
91
+ config: true,
92
+ lead_user_id: true,
93
+ archived: true,
94
+ },
95
+ }));
96
+ const variables = await hubDb.variables.findMany({
97
+ select: { key: true, value: true },
98
+ orderBy: { key: "asc" },
99
+ });
100
+ const modelRows = (await hubDb.models.findMany());
101
+ const exportFiles = buildExportFiles(users, variables, modelRows);
102
+ reply.header("Content-Disposition", 'attachment; filename="naisys-config.zip"');
103
+ reply.type("application/zip");
104
+ const archive = archiver("zip", { zlib: { level: 9 } });
105
+ archive.on("error", (err) => {
106
+ reply.log.error(err, "Archiver error");
107
+ });
108
+ for (const file of exportFiles) {
109
+ archive.append(file.content, { name: file.path });
110
+ }
111
+ await archive.finalize();
112
+ return reply.send(archive);
113
+ });
114
+ // POST /rotate-access-key — Rotate hub access key
115
+ fastify.post("/rotate-access-key", {
116
+ preHandler: [requirePermission("supervisor_admin")],
117
+ schema: {
118
+ description: "Rotate the hub access key",
119
+ tags: ["Admin"],
120
+ response: {
121
+ 200: RotateAccessKeyResultSchema,
122
+ 500: ErrorResponseSchema,
123
+ },
124
+ security: [{ cookieAuth: [] }],
125
+ },
126
+ }, async (_request, reply) => {
127
+ try {
128
+ const result = await sendRotateAccessKey();
129
+ return result;
130
+ }
131
+ catch (error) {
132
+ reply.log.error(error, "Error in POST /admin/rotate-access-key route");
133
+ return reply.status(500).send({
134
+ success: false,
135
+ message: error instanceof Error
136
+ ? error.message
137
+ : "Failed to rotate access key",
138
+ });
139
+ }
140
+ });
141
+ // GET /logs — Tail server log files
142
+ fastify.get("/logs", {
143
+ preHandler: [requirePermission("supervisor_admin")],
144
+ schema: {
145
+ description: "Get tail of a server log file",
146
+ tags: ["Admin"],
147
+ querystring: ServerLogRequestSchema,
148
+ response: {
149
+ 200: ServerLogResponseSchema,
150
+ 500: ErrorResponseSchema,
151
+ },
152
+ security: [{ cookieAuth: [] }],
153
+ },
154
+ }, async (request, _reply) => {
155
+ const { file, lines, minLevel } = request.query;
156
+ const cappedLines = Math.min(lines, 1000);
157
+ const filePath = getLogFilePath(file);
158
+ const { entries, fileSize } = await tailLogFile(filePath, cappedLines, minLevel);
159
+ return {
160
+ entries,
161
+ fileName: `${file}.log`,
162
+ fileSize,
163
+ };
164
+ });
165
+ // GET /attachments — List all attachments
166
+ fastify.get("/attachments", {
167
+ preHandler: [requirePermission("supervisor_admin")],
168
+ schema: {
169
+ description: "List all uploaded attachments",
170
+ tags: ["Admin"],
171
+ querystring: AdminAttachmentListRequestSchema,
172
+ response: {
173
+ 200: AdminAttachmentListResponseSchema,
174
+ 500: ErrorResponseSchema,
175
+ },
176
+ security: [{ cookieAuth: [] }],
177
+ },
178
+ }, async (request, _reply) => {
179
+ const { page, pageSize } = request.query;
180
+ const skip = (page - 1) * pageSize;
181
+ const [rows, total] = await Promise.all([
182
+ hubDb.attachments.findMany({
183
+ orderBy: { created_at: "desc" },
184
+ include: {
185
+ uploader: { select: { username: true } },
186
+ },
187
+ skip,
188
+ take: pageSize,
189
+ }),
190
+ hubDb.attachments.count(),
191
+ ]);
192
+ return {
193
+ attachments: rows.map((r) => ({
194
+ id: r.public_id,
195
+ filename: r.filename,
196
+ fileSize: r.file_size,
197
+ fileHash: r.file_hash,
198
+ purpose: r.purpose,
199
+ uploadedBy: r.uploader.username,
200
+ createdAt: r.created_at.toISOString(),
201
+ })),
202
+ total,
203
+ page,
204
+ pageSize,
205
+ _links: paginationLinks("admin/attachments", page, pageSize, total),
206
+ };
207
+ });
208
+ }
209
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1,194 @@
1
+ import { AgentUsernameParamsSchema, ArchiveChatResponseSchema, ChatConversationsRequestSchema, ChatConversationsResponseSchema, ChatMessagesRequestSchema, ChatMessagesResponseSchema, ErrorResponseSchema, SendChatRequestSchema, SendChatResponseSchema, } from "@naisys/supervisor-shared";
2
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
3
+ import { badRequest, notFound } from "../error-helpers.js";
4
+ import { API_PREFIX } from "../hateoas.js";
5
+ import { resolveAgentId } from "../services/agentService.js";
6
+ import { uploadToHub } from "../services/attachmentProxyService.js";
7
+ import { archiveAllChatMessages, getConversations, getMessages, sendChatMessage, } from "../services/chatService.js";
8
+ function sendChatAction(username) {
9
+ return {
10
+ rel: "send",
11
+ href: `${API_PREFIX}/agents/${username}/chat`,
12
+ method: "POST",
13
+ title: "Send Chat Message",
14
+ schema: `${API_PREFIX}/schemas/SendChat`,
15
+ body: { fromId: 0, toIds: [0], message: "" },
16
+ alternateEncoding: {
17
+ contentType: "multipart/form-data",
18
+ description: "Send as multipart to include file attachments",
19
+ fileFields: ["attachments"],
20
+ },
21
+ };
22
+ }
23
+ function archiveChatAction(username) {
24
+ return {
25
+ rel: "archive",
26
+ href: `${API_PREFIX}/agents/${username}/chat/archive`,
27
+ method: "POST",
28
+ title: "Archive All Chat Messages",
29
+ };
30
+ }
31
+ export default function agentChatRoutes(fastify, _options) {
32
+ // GET /:username/chat — List conversations for agent
33
+ fastify.get("/:username/chat", {
34
+ schema: {
35
+ description: "Get chat conversations for a specific agent",
36
+ tags: ["Chat"],
37
+ params: AgentUsernameParamsSchema,
38
+ querystring: ChatConversationsRequestSchema,
39
+ response: {
40
+ 200: ChatConversationsResponseSchema,
41
+ 500: ErrorResponseSchema,
42
+ },
43
+ },
44
+ }, async (request, reply) => {
45
+ const { username } = request.params;
46
+ const { page, count } = request.query;
47
+ const id = resolveAgentId(username);
48
+ if (!id) {
49
+ return notFound(reply, `Agent '${username}' not found`);
50
+ }
51
+ const { conversations, total } = await getConversations(id, page, count);
52
+ const canSend = hasPermission(request.supervisorUser, "agent_communication");
53
+ return {
54
+ success: true,
55
+ conversations,
56
+ total,
57
+ _actions: canSend
58
+ ? [sendChatAction(username), archiveChatAction(username)]
59
+ : undefined,
60
+ };
61
+ });
62
+ // GET /:username/chat/:participants — Messages in a conversation
63
+ fastify.get("/:username/chat/:participants", {
64
+ schema: {
65
+ description: "Get chat messages for a specific conversation",
66
+ tags: ["Chat"],
67
+ querystring: ChatMessagesRequestSchema,
68
+ response: {
69
+ 200: ChatMessagesResponseSchema,
70
+ 500: ErrorResponseSchema,
71
+ },
72
+ },
73
+ }, async (request, _reply) => {
74
+ const { username, participants } = request.params;
75
+ const { updatedSince, page, count } = request.query;
76
+ const data = await getMessages(participants, updatedSince, page, count);
77
+ const canSend = hasPermission(request.supervisorUser, "agent_communication");
78
+ return {
79
+ success: true,
80
+ messages: data.messages,
81
+ total: data.total,
82
+ timestamp: data.timestamp,
83
+ _actions: canSend ? [sendChatAction(username)] : undefined,
84
+ };
85
+ });
86
+ // POST /:username/chat/archive — Archive all chat messages
87
+ fastify.post("/:username/chat/archive", {
88
+ preHandler: [requirePermission("agent_communication")],
89
+ schema: {
90
+ description: "Archive all chat messages for an agent",
91
+ tags: ["Chat"],
92
+ params: AgentUsernameParamsSchema,
93
+ response: {
94
+ 200: ArchiveChatResponseSchema,
95
+ 500: ErrorResponseSchema,
96
+ },
97
+ security: [{ cookieAuth: [] }],
98
+ },
99
+ }, async (request, reply) => {
100
+ const { username } = request.params;
101
+ const id = resolveAgentId(username);
102
+ if (!id) {
103
+ return notFound(reply, `Agent '${username}' not found`);
104
+ }
105
+ const archivedCount = await archiveAllChatMessages(id);
106
+ return { success: true, archivedCount };
107
+ });
108
+ // POST /:username/chat — Send chat message
109
+ fastify.post("/:username/chat", {
110
+ preHandler: [requirePermission("agent_communication")],
111
+ schema: {
112
+ description: "Send a chat message as an agent with optional attachments. Supports JSON and multipart/form-data",
113
+ tags: ["Chat"],
114
+ params: AgentUsernameParamsSchema,
115
+ // No body schema — multipart requests are parsed manually via request.parts()
116
+ response: {
117
+ 200: SendChatResponseSchema,
118
+ 400: ErrorResponseSchema,
119
+ 500: ErrorResponseSchema,
120
+ },
121
+ security: [{ cookieAuth: [] }],
122
+ },
123
+ }, async (request, reply) => {
124
+ const contentType = request.headers["content-type"];
125
+ let fromId = 0, toIds = [], message = "";
126
+ let attachmentBuffers = [];
127
+ if (contentType?.includes("multipart/form-data")) {
128
+ const parts = request.parts();
129
+ for await (const part of parts) {
130
+ if (part.type === "field") {
131
+ const field = part;
132
+ switch (field.fieldname) {
133
+ case "fromId":
134
+ fromId = Number(field.value);
135
+ break;
136
+ case "toIds":
137
+ try {
138
+ toIds = JSON.parse(field.value);
139
+ }
140
+ catch {
141
+ return badRequest(reply, "toIds must be valid JSON array");
142
+ }
143
+ break;
144
+ case "message":
145
+ message = field.value;
146
+ break;
147
+ }
148
+ }
149
+ else if (part.type === "file") {
150
+ const file = part;
151
+ if (file.fieldname === "attachments") {
152
+ const buffer = await file.toBuffer();
153
+ attachmentBuffers.push({
154
+ filename: file.filename || "unnamed_file",
155
+ data: buffer,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+ else {
162
+ const body = request.body;
163
+ fromId = body.fromId;
164
+ toIds = body.toIds;
165
+ message = body.message;
166
+ }
167
+ const parsed = SendChatRequestSchema.safeParse({
168
+ fromId,
169
+ toIds,
170
+ message,
171
+ });
172
+ if (!parsed.success) {
173
+ return badRequest(reply, parsed.error.message);
174
+ }
175
+ ({ fromId, toIds, message } = parsed.data);
176
+ // Upload attachments to hub and collect IDs
177
+ let attachmentIds;
178
+ if (attachmentBuffers.length > 0) {
179
+ attachmentIds = [];
180
+ for (const att of attachmentBuffers) {
181
+ const id = await uploadToHub(att.data, att.filename, fromId, "mail");
182
+ attachmentIds.push(id);
183
+ }
184
+ }
185
+ const result = await sendChatMessage(fromId, toIds, message, attachmentIds);
186
+ if (result.success) {
187
+ return reply.code(200).send(result);
188
+ }
189
+ else {
190
+ return reply.code(500).send(result);
191
+ }
192
+ });
193
+ }
194
+ //# sourceMappingURL=agentChat.js.map