@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,171 @@
1
+ import { AgentUsernameParamsSchema, ArchiveMailResponseSchema, ErrorResponseSchema, MailDataRequestSchema, MailDataResponseSchema, SendMailRequestSchema, SendMailResponseSchema, } 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 { archiveAllMailMessages, getMailDataByUserId, sendMessage, } from "../services/mailService.js";
7
+ export default function agentMailRoutes(fastify, _options) {
8
+ // GET /:username/mail — Mail for agent
9
+ fastify.get("/:username/mail", {
10
+ schema: {
11
+ description: "Get mail data for a specific agent",
12
+ tags: ["Mail"],
13
+ params: AgentUsernameParamsSchema,
14
+ querystring: MailDataRequestSchema,
15
+ response: {
16
+ 200: MailDataResponseSchema,
17
+ 500: ErrorResponseSchema,
18
+ },
19
+ },
20
+ }, async (request, reply) => {
21
+ const { username } = request.params;
22
+ const { updatedSince, page, count } = request.query;
23
+ const id = resolveAgentId(username);
24
+ if (!id) {
25
+ return notFound(reply, `Agent '${username}' not found`);
26
+ }
27
+ const data = await getMailDataByUserId(id, updatedSince, page, count, "mail");
28
+ const canSend = hasPermission(request.supervisorUser, "agent_communication");
29
+ return {
30
+ success: true,
31
+ message: "Mail data retrieved successfully",
32
+ data,
33
+ _links: data
34
+ ? [
35
+ {
36
+ rel: "next",
37
+ href: `${API_PREFIX}/agents/${username}/mail?updatedSince=${encodeURIComponent(data.timestamp)}`,
38
+ title: "Poll for newer mail",
39
+ },
40
+ ]
41
+ : undefined,
42
+ _actions: canSend
43
+ ? [
44
+ {
45
+ rel: "send",
46
+ href: `${API_PREFIX}/agents/${username}/mail`,
47
+ method: "POST",
48
+ title: "Send Mail",
49
+ schema: `${API_PREFIX}/schemas/SendMail`,
50
+ body: { fromId: 0, toIds: [0], subject: "", message: "" },
51
+ alternateEncoding: {
52
+ contentType: "multipart/form-data",
53
+ description: "Send as multipart to include file attachments",
54
+ fileFields: ["attachments"],
55
+ },
56
+ },
57
+ {
58
+ rel: "archive",
59
+ href: `${API_PREFIX}/agents/${username}/mail/archive`,
60
+ method: "POST",
61
+ title: "Archive All Mail Messages",
62
+ },
63
+ ]
64
+ : undefined,
65
+ };
66
+ });
67
+ // POST /:username/mail/archive — Archive all mail messages
68
+ fastify.post("/:username/mail/archive", {
69
+ preHandler: [requirePermission("agent_communication")],
70
+ schema: {
71
+ description: "Archive all mail messages for an agent",
72
+ tags: ["Mail"],
73
+ params: AgentUsernameParamsSchema,
74
+ response: {
75
+ 200: ArchiveMailResponseSchema,
76
+ 500: ErrorResponseSchema,
77
+ },
78
+ security: [{ cookieAuth: [] }],
79
+ },
80
+ }, async (request, reply) => {
81
+ const { username } = request.params;
82
+ const id = resolveAgentId(username);
83
+ if (!id) {
84
+ return notFound(reply, `Agent '${username}' not found`);
85
+ }
86
+ const archivedCount = await archiveAllMailMessages(id);
87
+ return { success: true, archivedCount };
88
+ });
89
+ // POST /:username/mail — Send mail as agent
90
+ fastify.post("/:username/mail", {
91
+ preHandler: [requirePermission("agent_communication")],
92
+ schema: {
93
+ description: "Send email as agent with optional attachments. Supports JSON and multipart/form-data",
94
+ tags: ["Mail"],
95
+ params: AgentUsernameParamsSchema,
96
+ // No body schema — multipart requests are parsed manually via request.parts()
97
+ response: {
98
+ 200: SendMailResponseSchema,
99
+ 400: ErrorResponseSchema,
100
+ 500: ErrorResponseSchema,
101
+ },
102
+ security: [{ cookieAuth: [] }],
103
+ },
104
+ }, async (request, reply) => {
105
+ const contentType = request.headers["content-type"];
106
+ let fromId = 0, toIds = [], subject = "", message = "";
107
+ let attachments = [];
108
+ if (contentType?.includes("multipart/form-data")) {
109
+ const parts = request.parts();
110
+ for await (const part of parts) {
111
+ if (part.type === "field") {
112
+ const field = part;
113
+ switch (field.fieldname) {
114
+ case "fromId":
115
+ fromId = Number(field.value);
116
+ break;
117
+ case "toIds":
118
+ try {
119
+ toIds = JSON.parse(field.value);
120
+ }
121
+ catch {
122
+ return badRequest(reply, "toIds must be valid JSON array");
123
+ }
124
+ break;
125
+ case "subject":
126
+ subject = field.value;
127
+ break;
128
+ case "message":
129
+ message = field.value;
130
+ break;
131
+ }
132
+ }
133
+ else if (part.type === "file") {
134
+ const file = part;
135
+ if (file.fieldname === "attachments") {
136
+ const buffer = await file.toBuffer();
137
+ attachments.push({
138
+ filename: file.filename || "unnamed_file",
139
+ data: buffer,
140
+ });
141
+ }
142
+ }
143
+ }
144
+ }
145
+ else {
146
+ const body = request.body;
147
+ fromId = body.fromId;
148
+ toIds = body.toIds;
149
+ subject = body.subject;
150
+ message = body.message;
151
+ }
152
+ const parsed = SendMailRequestSchema.safeParse({
153
+ fromId,
154
+ toIds,
155
+ subject,
156
+ message,
157
+ });
158
+ if (!parsed.success) {
159
+ return badRequest(reply, parsed.error.message);
160
+ }
161
+ ({ fromId, toIds, subject, message } = parsed.data);
162
+ const result = await sendMessage({ fromId, toIds, subject, message }, attachments.length > 0 ? attachments : undefined);
163
+ if (result.success) {
164
+ return reply.code(200).send(result);
165
+ }
166
+ else {
167
+ return reply.code(500).send(result);
168
+ }
169
+ });
170
+ }
171
+ //# sourceMappingURL=agentMail.js.map
@@ -0,0 +1,90 @@
1
+ import { AgentUsernameParamsSchema, ContextLogParamsSchema, ContextLogRequestSchema, ContextLogResponseSchema, RunsDataRequestSchema, RunsDataResponseSchema, } from "@naisys/supervisor-shared";
2
+ import { hasPermission } from "../auth-middleware.js";
3
+ import { notFound } from "../error-helpers.js";
4
+ import { API_PREFIX } from "../hateoas.js";
5
+ import { resolveAgentId } from "../services/agentService.js";
6
+ import { getContextLog, getRunsData, obfuscateLogs, } from "../services/runsService.js";
7
+ export default function agentRunsRoutes(fastify, _options) {
8
+ // GET /:username/runs — Runs for agent
9
+ fastify.get("/:username/runs", {
10
+ schema: {
11
+ description: "Get run sessions for a specific agent",
12
+ tags: ["Runs"],
13
+ params: AgentUsernameParamsSchema,
14
+ querystring: RunsDataRequestSchema,
15
+ response: {
16
+ 200: RunsDataResponseSchema,
17
+ 500: RunsDataResponseSchema,
18
+ },
19
+ },
20
+ }, async (request, reply) => {
21
+ const { username } = request.params;
22
+ const { updatedSince, page, count } = request.query;
23
+ const id = resolveAgentId(username);
24
+ if (!id) {
25
+ return notFound(reply, `Agent '${username}' not found`);
26
+ }
27
+ const data = await getRunsData(id, updatedSince, page, count);
28
+ return {
29
+ success: true,
30
+ message: "Runs data retrieved successfully",
31
+ data: data ?? undefined,
32
+ _linkTemplates: [
33
+ {
34
+ rel: "logs",
35
+ hrefTemplate: `${API_PREFIX}/agents/${username}/runs/{runId}/sessions/{sessionId}/logs`,
36
+ },
37
+ ],
38
+ _links: data
39
+ ? [
40
+ {
41
+ rel: "next",
42
+ href: `${API_PREFIX}/agents/${username}/runs?updatedSince=${encodeURIComponent(data.timestamp)}`,
43
+ title: "Poll for updated runs",
44
+ },
45
+ ]
46
+ : undefined,
47
+ };
48
+ });
49
+ // GET /:username/runs/:runId/sessions/:sessionId/logs — Context log
50
+ fastify.get("/:username/runs/:runId/sessions/:sessionId/logs", {
51
+ schema: {
52
+ description: "Get context log for a specific run session",
53
+ tags: ["Runs"],
54
+ params: ContextLogParamsSchema,
55
+ querystring: ContextLogRequestSchema,
56
+ response: {
57
+ 200: ContextLogResponseSchema,
58
+ 500: ContextLogResponseSchema,
59
+ },
60
+ },
61
+ }, async (request, reply) => {
62
+ const { username, runId, sessionId } = request.params;
63
+ const { logsAfter, logsBefore } = request.query;
64
+ const id = resolveAgentId(username);
65
+ if (!id) {
66
+ return notFound(reply, `Agent '${username}' not found`);
67
+ }
68
+ let data = await getContextLog(id, runId, sessionId, logsAfter, logsBefore);
69
+ // Obfuscate log text for users without view_run_logs permission
70
+ if (!hasPermission(request.supervisorUser, "view_run_logs")) {
71
+ data = obfuscateLogs(data);
72
+ }
73
+ const maxLogId = data?.logs.length
74
+ ? Math.max(...data.logs.map((l) => l.id))
75
+ : (logsAfter ?? 0);
76
+ return {
77
+ success: true,
78
+ message: "Context log retrieved successfully",
79
+ data,
80
+ _links: [
81
+ {
82
+ rel: "next",
83
+ href: `${API_PREFIX}/agents/${username}/runs/${runId}/sessions/${sessionId}/logs?logsAfter=${maxLogId}`,
84
+ title: "Poll for newer logs",
85
+ },
86
+ ],
87
+ };
88
+ });
89
+ }
90
+ //# sourceMappingURL=agentRuns.js.map
@@ -0,0 +1,236 @@
1
+ import { AgentDetailResponseSchema, AgentListRequestSchema, AgentListResponseSchema, AgentUsernameParamsSchema, CreateAgentConfigRequestSchema, CreateAgentConfigResponseSchema, ErrorResponseSchema, } from "@naisys/supervisor-shared";
2
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
3
+ import { badRequest, notFound } from "../error-helpers.js";
4
+ import { API_PREFIX, collectionLink, schemaLink, selfLink, } from "../hateoas.js";
5
+ import { resolveActions } from "../route-helpers.js";
6
+ import { createAgentConfig } from "../services/agentConfigService.js";
7
+ import { getAgentStatus, isAgentActive, } from "../services/agentHostStatusService.js";
8
+ import { getAgent, getAgents, resolveAgentId, } from "../services/agentService.js";
9
+ function agentActions(username, user, enabled, archived, agentId, hasSpendLimit) {
10
+ const active = agentId ? isAgentActive(agentId) : false;
11
+ const href = `${API_PREFIX}/agents/${username}`;
12
+ return resolveActions([
13
+ {
14
+ rel: "start",
15
+ path: "/start",
16
+ method: "POST",
17
+ title: "Start Agent",
18
+ schema: `${API_PREFIX}/schemas/StartAgent`,
19
+ body: { task: "" },
20
+ permission: "manage_agents",
21
+ disabledWhen: (ctx) => ctx.active
22
+ ? "Agent is already running"
23
+ : ctx.archived
24
+ ? "Agent is archived"
25
+ : !ctx.enabled
26
+ ? "Agent is disabled"
27
+ : null,
28
+ },
29
+ {
30
+ rel: "stop",
31
+ path: "/stop",
32
+ method: "POST",
33
+ title: "Stop Agent",
34
+ permission: "manage_agents",
35
+ disabledWhen: (ctx) => (!ctx.active ? "Agent is not running" : null),
36
+ },
37
+ {
38
+ rel: "disable",
39
+ path: "/disable",
40
+ method: "POST",
41
+ title: "Disable Agent",
42
+ permission: "manage_agents",
43
+ visibleWhen: (ctx) => !ctx.archived && ctx.enabled,
44
+ },
45
+ {
46
+ rel: "enable",
47
+ path: "/enable",
48
+ method: "POST",
49
+ title: "Enable Agent",
50
+ permission: "manage_agents",
51
+ visibleWhen: (ctx) => !ctx.archived && !ctx.enabled && !ctx.active,
52
+ },
53
+ {
54
+ rel: "archive",
55
+ path: "/archive",
56
+ method: "POST",
57
+ title: "Archive Agent",
58
+ permission: "manage_agents",
59
+ visibleWhen: (ctx) => !ctx.archived,
60
+ disabledWhen: (ctx) => ctx.active ? "Stop the agent before archiving" : null,
61
+ },
62
+ {
63
+ rel: "unarchive",
64
+ path: "/unarchive",
65
+ method: "POST",
66
+ title: "Unarchive Agent",
67
+ permission: "manage_agents",
68
+ visibleWhen: (ctx) => ctx.archived,
69
+ },
70
+ {
71
+ rel: "delete",
72
+ method: "DELETE",
73
+ title: "Delete Agent",
74
+ permission: "manage_agents",
75
+ visibleWhen: (ctx) => !ctx.active && ctx.archived,
76
+ hideWithoutPermission: true,
77
+ },
78
+ {
79
+ rel: "update-config",
80
+ path: "/config",
81
+ method: "PUT",
82
+ title: "Update Agent Config",
83
+ schema: `${API_PREFIX}/schemas/UpdateAgentConfig`,
84
+ permission: "manage_agents",
85
+ visibleWhen: (ctx) => !ctx.archived,
86
+ },
87
+ {
88
+ rel: "set-lead",
89
+ path: "/lead",
90
+ method: "PUT",
91
+ title: "Set Lead Agent",
92
+ schema: `${API_PREFIX}/schemas/SetLeadAgent`,
93
+ body: { leadAgentUsername: "" },
94
+ permission: "manage_agents",
95
+ visibleWhen: (ctx) => !ctx.archived,
96
+ },
97
+ {
98
+ rel: "reset-spend",
99
+ path: "/reset-spend",
100
+ method: "POST",
101
+ title: "Reset Spend",
102
+ permission: "manage_agents",
103
+ visibleWhen: (ctx) => !ctx.archived && ctx.hasSpendLimit,
104
+ },
105
+ ], href, { user, active, archived, enabled, hasSpendLimit: hasSpendLimit ?? false });
106
+ }
107
+ function agentLinks(username, config) {
108
+ const links = [
109
+ selfLink(`/agents/${username}`),
110
+ { rel: "config", href: `${API_PREFIX}/agents/${username}/config` },
111
+ { rel: "runs", href: `${API_PREFIX}/agents/${username}/runs` },
112
+ collectionLink("agents"),
113
+ ];
114
+ if (config?.mailEnabled) {
115
+ links.push({ rel: "mail", href: `${API_PREFIX}/agents/${username}/mail` });
116
+ }
117
+ if (config?.chatEnabled) {
118
+ links.push({ rel: "chat", href: `${API_PREFIX}/agents/${username}/chat` });
119
+ }
120
+ return links;
121
+ }
122
+ export default function agentsRoutes(fastify, _options) {
123
+ // GET / — List agents
124
+ fastify.get("/", {
125
+ schema: {
126
+ description: "List agents with status and metadata",
127
+ tags: ["Agents"],
128
+ querystring: AgentListRequestSchema,
129
+ response: {
130
+ 200: AgentListResponseSchema,
131
+ 500: ErrorResponseSchema,
132
+ },
133
+ },
134
+ }, async (request, _reply) => {
135
+ const { updatedSince } = request.query;
136
+ const agents = await getAgents(updatedSince);
137
+ const items = agents.map((agent) => ({
138
+ ...agent,
139
+ status: getAgentStatus(agent.id),
140
+ }));
141
+ const hasManagePermission = hasPermission(request.supervisorUser, "manage_agents");
142
+ const actions = [
143
+ {
144
+ rel: "create",
145
+ href: `${API_PREFIX}/agents`,
146
+ method: "POST",
147
+ title: "Create Agent",
148
+ schema: `${API_PREFIX}/schemas/CreateAgent`,
149
+ body: { name: "" },
150
+ ...(hasManagePermission
151
+ ? {}
152
+ : {
153
+ disabled: true,
154
+ disabledReason: "Requires manage_agents permission",
155
+ }),
156
+ },
157
+ ];
158
+ return {
159
+ items,
160
+ timestamp: new Date().toISOString(),
161
+ _links: [selfLink("/agents"), schemaLink("CreateAgent")],
162
+ _linkTemplates: [
163
+ { rel: "item", hrefTemplate: `${API_PREFIX}/agents/{name}` },
164
+ ],
165
+ _actions: actions,
166
+ };
167
+ });
168
+ // POST / — Create agent
169
+ fastify.post("/", {
170
+ preHandler: [requirePermission("manage_agents")],
171
+ schema: {
172
+ description: "Create a new agent with configuration file",
173
+ tags: ["Agents"],
174
+ body: CreateAgentConfigRequestSchema,
175
+ response: {
176
+ 200: CreateAgentConfigResponseSchema,
177
+ 400: ErrorResponseSchema,
178
+ 500: ErrorResponseSchema,
179
+ },
180
+ security: [{ cookieAuth: [] }],
181
+ },
182
+ }, async (request, reply) => {
183
+ try {
184
+ const { name, title } = request.body;
185
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
186
+ return badRequest(reply, "Agent name must contain only alphanumeric characters, hyphens, and underscores");
187
+ }
188
+ const { config } = await createAgentConfig(name, title);
189
+ return {
190
+ success: true,
191
+ message: `Agent '${name}' created successfully`,
192
+ name,
193
+ _links: agentLinks(name, config),
194
+ _actions: agentActions(name, request.supervisorUser, true, false),
195
+ };
196
+ }
197
+ catch (error) {
198
+ request.log.error(error, "Error in POST /agents route");
199
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
200
+ if (errorMessage.includes("already exists")) {
201
+ return badRequest(reply, errorMessage);
202
+ }
203
+ throw error;
204
+ }
205
+ });
206
+ // GET /:username — Agent detail with config
207
+ fastify.get("/:username", {
208
+ schema: {
209
+ description: "Get agent detail with configuration",
210
+ tags: ["Agents"],
211
+ params: AgentUsernameParamsSchema,
212
+ response: {
213
+ 200: AgentDetailResponseSchema,
214
+ 404: ErrorResponseSchema,
215
+ 500: ErrorResponseSchema,
216
+ },
217
+ },
218
+ }, async (request, reply) => {
219
+ const { username } = request.params;
220
+ const id = resolveAgentId(username);
221
+ if (!id) {
222
+ return notFound(reply, `Agent '${username}' not found`);
223
+ }
224
+ const agent = await getAgent(id);
225
+ if (!agent) {
226
+ return notFound(reply, `Agent '${username}' not found`);
227
+ }
228
+ return {
229
+ ...agent,
230
+ status: getAgentStatus(id),
231
+ _links: agentLinks(username, agent.config),
232
+ _actions: agentActions(username, request.supervisorUser, agent.enabled ?? false, agent.archived ?? false, id, agent.config?.spendLimitDollars != null),
233
+ };
234
+ });
235
+ }
236
+ //# sourceMappingURL=agents.js.map
@@ -0,0 +1,52 @@
1
+ import { registerAuthMiddleware } from "../auth-middleware.js";
2
+ import adminRoutes from "./admin.js";
3
+ import agentChatRoutes from "./agentChat.js";
4
+ import agentConfigRoutes from "./agentConfig.js";
5
+ import agentLifecycleRoutes from "./agentLifecycle.js";
6
+ import agentMailRoutes from "./agentMail.js";
7
+ import agentRunsRoutes from "./agentRuns.js";
8
+ import agentsRoutes from "./agents.js";
9
+ import attachmentRoutes from "./attachments.js";
10
+ import authRoutes from "./auth.js";
11
+ import costsRoutes from "./costs.js";
12
+ import hostsRoutes from "./hosts.js";
13
+ import modelsRoutes from "./models.js";
14
+ import rootRoutes from "./root.js";
15
+ import schemaRoutes from "./schemas.js";
16
+ import statusRoutes from "./status.js";
17
+ import userRoutes from "./users.js";
18
+ import variablesRoutes from "./variables.js";
19
+ export default async function apiRoutes(fastify, _options) {
20
+ // Register auth middleware for all routes in this scope
21
+ registerAuthMiddleware(fastify);
22
+ // Register root discovery routes
23
+ await fastify.register(rootRoutes);
24
+ // Register auth routes
25
+ await fastify.register(authRoutes);
26
+ // Register schema routes
27
+ await fastify.register(schemaRoutes, { prefix: "/schemas" });
28
+ // Register user routes
29
+ await fastify.register(userRoutes, { prefix: "/users" });
30
+ // Register status routes
31
+ await fastify.register(statusRoutes);
32
+ // Register agents routes
33
+ await fastify.register(agentsRoutes, { prefix: "/agents" });
34
+ await fastify.register(agentLifecycleRoutes, { prefix: "/agents" });
35
+ await fastify.register(agentConfigRoutes, { prefix: "/agents" });
36
+ await fastify.register(agentRunsRoutes, { prefix: "/agents" });
37
+ await fastify.register(agentMailRoutes, { prefix: "/agents" });
38
+ await fastify.register(agentChatRoutes, { prefix: "/agents" });
39
+ // Register hosts routes
40
+ await fastify.register(hostsRoutes, { prefix: "/hosts" });
41
+ // Register models routes
42
+ await fastify.register(modelsRoutes);
43
+ // Register variables routes
44
+ await fastify.register(variablesRoutes, { prefix: "/variables" });
45
+ // Register costs routes
46
+ await fastify.register(costsRoutes, { prefix: "/costs" });
47
+ // Register admin routes
48
+ await fastify.register(adminRoutes, { prefix: "/admin" });
49
+ // Register attachment routes
50
+ await fastify.register(attachmentRoutes, { prefix: "/attachments" });
51
+ }
52
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,18 @@
1
+ import { proxyDownloadFromHub } from "../services/attachmentProxyService.js";
2
+ export default function attachmentRoutes(fastify, _options) {
3
+ // GET /:id or /:id/:filename — Download attachment (proxied through hub)
4
+ const handler = async (request, reply) => {
5
+ const publicId = request.params.id;
6
+ if (!publicId) {
7
+ return reply.code(400).send({ error: "Missing attachment ID" });
8
+ }
9
+ await proxyDownloadFromHub(publicId, reply);
10
+ };
11
+ const schema = {
12
+ description: "Download an attachment by ID (proxied from hub)",
13
+ tags: ["Attachments"],
14
+ };
15
+ fastify.get("/:id", { schema }, handler);
16
+ fastify.get("/:id/:filename", { schema }, handler);
17
+ }
18
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1,103 @@
1
+ import { hashToken, SESSION_COOKIE_NAME, sessionCookieOptions, } from "@naisys/common-node";
2
+ import { authenticateAndCreateSession, deleteSession, } from "@naisys/supervisor-database";
3
+ import { AuthUserSchema, ErrorResponseSchema, LoginRequestSchema, LoginResponseSchema, LogoutResponseSchema, } from "@naisys/supervisor-shared";
4
+ import { authCache } from "../auth-middleware.js";
5
+ import { getUserByUsername, getUserPermissions, } from "../services/userService.js";
6
+ let lastLoginRequestTime = 0;
7
+ export default function authRoutes(fastify, _options) {
8
+ const app = fastify.withTypeProvider();
9
+ // LOGIN
10
+ app.post("/auth/login", {
11
+ config: {
12
+ rateLimit: {
13
+ max: 5,
14
+ timeWindow: "1 minute",
15
+ },
16
+ },
17
+ schema: {
18
+ description: "Authenticate with username and password",
19
+ tags: ["Authentication"],
20
+ body: LoginRequestSchema,
21
+ response: {
22
+ 200: LoginResponseSchema,
23
+ 401: ErrorResponseSchema,
24
+ 429: ErrorResponseSchema,
25
+ },
26
+ },
27
+ }, async (request, reply) => {
28
+ const currentTime = Date.now();
29
+ if (currentTime - lastLoginRequestTime < 5000) {
30
+ reply.code(429);
31
+ return {
32
+ success: false,
33
+ message: "Too many requests. Please wait before trying again.",
34
+ };
35
+ }
36
+ lastLoginRequestTime = currentTime;
37
+ const { username, password } = request.body;
38
+ const authResult = await authenticateAndCreateSession(username, password);
39
+ if (!authResult) {
40
+ reply.code(401);
41
+ return {
42
+ success: false,
43
+ message: "Invalid username or password",
44
+ };
45
+ }
46
+ reply.setCookie(SESSION_COOKIE_NAME, authResult.token, sessionCookieOptions(authResult.expiresAt));
47
+ const user = await getUserByUsername(username);
48
+ const permissions = user ? await getUserPermissions(user.id) : [];
49
+ return {
50
+ user: {
51
+ id: user?.id ?? 0,
52
+ username: authResult.user.username,
53
+ permissions,
54
+ },
55
+ };
56
+ });
57
+ // LOGOUT
58
+ app.post("/auth/logout", {
59
+ schema: {
60
+ description: "Log out and clear session",
61
+ tags: ["Authentication"],
62
+ response: {
63
+ 200: LogoutResponseSchema,
64
+ },
65
+ security: [{ cookieAuth: [] }],
66
+ },
67
+ }, async (request, reply) => {
68
+ const token = request.cookies?.[SESSION_COOKIE_NAME];
69
+ // Clear from hub and auth cache
70
+ if (token) {
71
+ const tokenHash = hashToken(token);
72
+ authCache.invalidate(`cookie:${tokenHash}`);
73
+ await deleteSession(tokenHash);
74
+ }
75
+ reply.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
76
+ return {
77
+ success: true,
78
+ message: "Logged out successfully",
79
+ };
80
+ });
81
+ // ME
82
+ app.get("/auth/me", {
83
+ schema: {
84
+ description: "Get current authenticated user",
85
+ tags: ["Authentication"],
86
+ response: {
87
+ 200: AuthUserSchema,
88
+ 401: ErrorResponseSchema,
89
+ },
90
+ security: [{ cookieAuth: [] }],
91
+ },
92
+ }, async (request, reply) => {
93
+ if (!request.supervisorUser) {
94
+ reply.code(401);
95
+ return {
96
+ success: false,
97
+ message: "Not authenticated",
98
+ };
99
+ }
100
+ return request.supervisorUser;
101
+ });
102
+ }
103
+ //# sourceMappingURL=auth.js.map