@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,92 @@
1
+ import { builtInImageModels, builtInLlmModels, imageModelToDbFields, llmModelToDbFields, } from "@naisys/common";
2
+ import { hubDb } from "../database/hubDb.js";
3
+ export async function getAllModelsFromDb() {
4
+ return hubDb.models.findMany();
5
+ }
6
+ export async function saveLlmModel(model) {
7
+ const fields = llmModelToDbFields(model, false, true);
8
+ const existing = await hubDb.models.findUnique({
9
+ where: { key: model.key },
10
+ });
11
+ if (existing) {
12
+ await hubDb.models.update({
13
+ where: { key: model.key },
14
+ data: { ...fields, is_builtin: existing.is_builtin, is_custom: true },
15
+ });
16
+ }
17
+ else {
18
+ await hubDb.models.create({ data: fields });
19
+ }
20
+ return { success: true, message: "LLM model saved" };
21
+ }
22
+ export async function saveImageModel(model) {
23
+ const fields = imageModelToDbFields(model, false, true);
24
+ const existing = await hubDb.models.findUnique({
25
+ where: { key: model.key },
26
+ });
27
+ if (existing) {
28
+ await hubDb.models.update({
29
+ where: { key: model.key },
30
+ data: { ...fields, is_builtin: existing.is_builtin, is_custom: true },
31
+ });
32
+ }
33
+ else {
34
+ await hubDb.models.create({ data: fields });
35
+ }
36
+ return { success: true, message: "Image model saved" };
37
+ }
38
+ export async function deleteLlmModel(key) {
39
+ const existing = await hubDb.models.findUnique({ where: { key } });
40
+ if (!existing || existing.type !== "llm") {
41
+ return {
42
+ success: false,
43
+ message: "Model not found",
44
+ revertedToBuiltIn: false,
45
+ };
46
+ }
47
+ if (existing.is_builtin) {
48
+ // Reset to built-in defaults
49
+ const builtIn = builtInLlmModels.find((m) => m.key === key);
50
+ const fields = llmModelToDbFields(builtIn, true, false);
51
+ await hubDb.models.update({ where: { key }, data: fields });
52
+ return {
53
+ success: true,
54
+ message: "Custom override removed, reverted to built-in",
55
+ revertedToBuiltIn: true,
56
+ };
57
+ }
58
+ await hubDb.models.delete({ where: { key } });
59
+ return {
60
+ success: true,
61
+ message: "Custom model deleted",
62
+ revertedToBuiltIn: false,
63
+ };
64
+ }
65
+ export async function deleteImageModel(key) {
66
+ const existing = await hubDb.models.findUnique({ where: { key } });
67
+ if (!existing || existing.type !== "image") {
68
+ return {
69
+ success: false,
70
+ message: "Model not found",
71
+ revertedToBuiltIn: false,
72
+ };
73
+ }
74
+ if (existing.is_builtin) {
75
+ // Reset to built-in defaults
76
+ const builtIn = builtInImageModels.find((m) => m.key === key);
77
+ const fields = imageModelToDbFields(builtIn, true, false);
78
+ await hubDb.models.update({ where: { key }, data: fields });
79
+ return {
80
+ success: true,
81
+ message: "Custom override removed, reverted to built-in",
82
+ revertedToBuiltIn: true,
83
+ };
84
+ }
85
+ await hubDb.models.delete({ where: { key } });
86
+ return {
87
+ success: true,
88
+ message: "Custom model deleted",
89
+ revertedToBuiltIn: false,
90
+ };
91
+ }
92
+ //# sourceMappingURL=modelService.js.map
@@ -0,0 +1,164 @@
1
+ import { hubDb } from "../database/hubDb.js";
2
+ const VOWELS = "aeiou";
3
+ const CONSONANTS = "bcdfghjklmnpqrstvwxyz";
4
+ const DIGITS = "0123456789";
5
+ function randomChar(pool) {
6
+ return pool[Math.floor(Math.random() * pool.length)];
7
+ }
8
+ /** Replace each letter/digit with a random one of the same class (vowel/consonant/digit), preserving case and all other characters. */
9
+ function obfuscateText(text) {
10
+ let result = "";
11
+ for (const ch of text) {
12
+ const lower = ch.toLowerCase();
13
+ if (DIGITS.includes(lower)) {
14
+ result += randomChar(DIGITS);
15
+ }
16
+ else if (VOWELS.includes(lower)) {
17
+ const r = randomChar(VOWELS);
18
+ result += ch === lower ? r : r.toUpperCase();
19
+ }
20
+ else if (CONSONANTS.includes(lower)) {
21
+ const r = randomChar(CONSONANTS);
22
+ result += ch === lower ? r : r.toUpperCase();
23
+ }
24
+ else {
25
+ result += ch;
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+ function obfuscateFilename(filename) {
31
+ const dotIndex = filename.lastIndexOf(".");
32
+ if (dotIndex <= 0)
33
+ return "attachment";
34
+ return `attachment${filename.slice(dotIndex)}`;
35
+ }
36
+ /** Obfuscate all log message text for public preview. */
37
+ export function obfuscateLogs(data) {
38
+ return {
39
+ ...data,
40
+ logs: data.logs.map((log) => ({
41
+ ...log,
42
+ message: obfuscateText(log.message),
43
+ attachment: log.attachment
44
+ ? { id: "no-access", filename: obfuscateFilename(log.attachment.filename), fileSize: 0 }
45
+ : undefined,
46
+ })),
47
+ };
48
+ }
49
+ /** Obfuscate log push entries for WebSocket broadcast to unprivileged clients. */
50
+ export function obfuscatePushEntries(entries) {
51
+ return entries.map((entry) => ({
52
+ ...entry,
53
+ message: obfuscateText(entry.message),
54
+ attachmentId: entry.attachmentId ? "no-access" : undefined,
55
+ attachmentFilename: entry.attachmentFilename
56
+ ? obfuscateFilename(entry.attachmentFilename)
57
+ : undefined,
58
+ attachmentFileSize: entry.attachmentId ? 0 : undefined,
59
+ }));
60
+ }
61
+ export async function getRunsData(userId, updatedSince, page = 1, count = 50) {
62
+ // Build the where clause
63
+ const where = {
64
+ user_id: userId,
65
+ };
66
+ // If updatedSince is provided, only fetch runs that were updated after that time
67
+ if (updatedSince) {
68
+ where.last_active = {
69
+ gt: updatedSince,
70
+ };
71
+ }
72
+ // Only get total count on initial fetch (when updatedSince is not set)
73
+ const total = updatedSince
74
+ ? undefined
75
+ : await hubDb.run_session.count({ where });
76
+ // Get paginated runs
77
+ const runSessions = await hubDb.run_session.findMany({
78
+ where,
79
+ orderBy: {
80
+ last_active: "desc",
81
+ },
82
+ skip: (page - 1) * count,
83
+ take: count,
84
+ });
85
+ // Map database records to our API format
86
+ const runs = runSessions.map((session) => {
87
+ return {
88
+ userId: session.user_id,
89
+ runId: session.run_id,
90
+ sessionId: session.session_id,
91
+ createdAt: session.created_at.toISOString(),
92
+ lastActive: session.last_active.toISOString(),
93
+ modelName: session.model_name,
94
+ latestLogId: session.latest_log_id,
95
+ totalLines: session.total_lines,
96
+ totalCost: session.total_cost,
97
+ };
98
+ });
99
+ return {
100
+ runs,
101
+ timestamp: new Date().toISOString(),
102
+ total,
103
+ };
104
+ }
105
+ export async function getContextLog(userId, runId, sessionId, logsAfter, logsBefore) {
106
+ const where = {
107
+ user_id: userId,
108
+ run_id: runId,
109
+ session_id: sessionId,
110
+ };
111
+ // Build ID range filter for incremental fetches / gap recovery
112
+ if (logsAfter !== undefined || logsBefore !== undefined) {
113
+ where.id = {};
114
+ if (logsAfter !== undefined)
115
+ where.id.gt = logsAfter;
116
+ if (logsBefore !== undefined)
117
+ where.id.lt = logsBefore;
118
+ }
119
+ const dbLogs = await hubDb.context_log.findMany({
120
+ where,
121
+ orderBy: { id: "desc" },
122
+ select: {
123
+ id: true,
124
+ role: true,
125
+ source: true,
126
+ type: true,
127
+ message: true,
128
+ created_at: true,
129
+ users: {
130
+ select: {
131
+ username: true,
132
+ },
133
+ },
134
+ attachment: {
135
+ select: {
136
+ public_id: true,
137
+ filename: true,
138
+ file_size: true,
139
+ },
140
+ },
141
+ },
142
+ });
143
+ const logs = dbLogs.map((log) => ({
144
+ id: log.id,
145
+ username: log.users.username,
146
+ role: log.role,
147
+ source: log.source,
148
+ type: log.type,
149
+ message: log.message,
150
+ createdAt: log.created_at.toISOString(),
151
+ ...(log.attachment && {
152
+ attachment: {
153
+ id: log.attachment.public_id,
154
+ filename: log.attachment.filename,
155
+ fileSize: log.attachment.file_size,
156
+ },
157
+ }),
158
+ }));
159
+ return {
160
+ logs,
161
+ timestamp: new Date().toISOString(),
162
+ };
163
+ }
164
+ //# sourceMappingURL=runsService.js.map
@@ -0,0 +1,147 @@
1
+ import { assertUrlSafeKey } from "@naisys/common";
2
+ import { hashToken } from "@naisys/common-node";
3
+ import { getAgentApiKeyByUuid, rotateAgentApiKeyByUuid, } from "@naisys/hub-database";
4
+ import { updateUserPassword } from "@naisys/supervisor-database";
5
+ import bcrypt from "bcryptjs";
6
+ import { randomBytes, randomUUID } from "crypto";
7
+ import supervisorDb from "../database/supervisorDb.js";
8
+ export { hashToken };
9
+ const SALT_ROUNDS = 10;
10
+ export async function getUserByUsername(username) {
11
+ return supervisorDb.user.findUnique({ where: { username } });
12
+ }
13
+ export async function getUserByUsernameWithPermissions(username) {
14
+ return supervisorDb.user.findUnique({
15
+ where: { username },
16
+ include: { permissions: true },
17
+ });
18
+ }
19
+ export async function getUserByUuid(uuid) {
20
+ return supervisorDb.user.findFirst({ where: { uuid } });
21
+ }
22
+ export async function createUserForAgent(username, uuid) {
23
+ assertUrlSafeKey(username, "Username");
24
+ return supervisorDb.user.create({
25
+ data: {
26
+ username,
27
+ uuid,
28
+ isAgent: true,
29
+ },
30
+ include: { permissions: true },
31
+ });
32
+ }
33
+ // --- User CRUD ---
34
+ export async function listUsers(options) {
35
+ const { page, pageSize, search } = options;
36
+ const where = search ? { username: { contains: search } } : {};
37
+ const [items, total] = await Promise.all([
38
+ supervisorDb.user.findMany({
39
+ where,
40
+ include: { permissions: true },
41
+ orderBy: { createdAt: "desc" },
42
+ skip: (page - 1) * pageSize,
43
+ take: pageSize,
44
+ }),
45
+ supervisorDb.user.count({ where }),
46
+ ]);
47
+ return { items, total, pageSize };
48
+ }
49
+ export async function getUserById(id) {
50
+ return supervisorDb.user.findUnique({
51
+ where: { id },
52
+ include: { permissions: true },
53
+ });
54
+ }
55
+ export async function createUserWithPassword(data) {
56
+ assertUrlSafeKey(data.username, "Username");
57
+ const passwordHash = await bcrypt.hash(data.password, SALT_ROUNDS);
58
+ const uuid = randomUUID();
59
+ const user = await supervisorDb.user.create({
60
+ data: {
61
+ username: data.username,
62
+ uuid,
63
+ passwordHash,
64
+ isAgent: false,
65
+ apiKey: randomBytes(32).toString("hex"),
66
+ },
67
+ include: { permissions: true },
68
+ });
69
+ return user;
70
+ }
71
+ export async function updateUser(id, data) {
72
+ const updateData = {};
73
+ if (data.username !== undefined) {
74
+ assertUrlSafeKey(data.username, "Username");
75
+ updateData.username = data.username;
76
+ }
77
+ const updated = await supervisorDb.user.update({
78
+ where: { id },
79
+ data: updateData,
80
+ include: { permissions: true },
81
+ });
82
+ if (data.password !== undefined) {
83
+ const newHash = await bcrypt.hash(data.password, SALT_ROUNDS);
84
+ await updateUserPassword(updated.username, newHash);
85
+ }
86
+ return updated;
87
+ }
88
+ export async function deleteUser(id) {
89
+ return supervisorDb.user.delete({ where: { id } });
90
+ }
91
+ export async function grantPermission(userId, permission, grantedBy) {
92
+ return supervisorDb.userPermission.create({
93
+ data: { userId, permission, grantedBy },
94
+ });
95
+ }
96
+ export async function revokePermission(userId, permission) {
97
+ await supervisorDb.userPermission.deleteMany({
98
+ where: { userId, permission },
99
+ });
100
+ }
101
+ export async function getUserPermissions(userId) {
102
+ const perms = await supervisorDb.userPermission.findMany({
103
+ where: { userId },
104
+ select: { permission: true },
105
+ });
106
+ return perms.map((p) => p.permission);
107
+ }
108
+ export async function checkUserPermission(userId, permission) {
109
+ const perm = await supervisorDb.userPermission.findFirst({
110
+ where: { userId, permission },
111
+ });
112
+ return perm !== null;
113
+ }
114
+ export async function getUserApiKey(id) {
115
+ const user = await supervisorDb.user.findUnique({
116
+ where: { id },
117
+ select: { isAgent: true, uuid: true, apiKey: true },
118
+ });
119
+ if (!user)
120
+ return null;
121
+ if (user.isAgent) {
122
+ return getAgentApiKeyByUuid(user.uuid);
123
+ }
124
+ else {
125
+ return user.apiKey ?? null;
126
+ }
127
+ }
128
+ export async function rotateUserApiKey(id) {
129
+ const newKey = randomBytes(32).toString("hex");
130
+ const user = await supervisorDb.user.findUnique({
131
+ where: { id },
132
+ select: { isAgent: true, uuid: true },
133
+ });
134
+ if (!user)
135
+ throw new Error("User not found");
136
+ if (user.isAgent) {
137
+ await rotateAgentApiKeyByUuid(user.uuid, newKey);
138
+ }
139
+ else {
140
+ await supervisorDb.user.update({
141
+ where: { id },
142
+ data: { apiKey: newKey },
143
+ });
144
+ }
145
+ return newKey;
146
+ }
147
+ //# sourceMappingURL=userService.js.map
@@ -0,0 +1,23 @@
1
+ import { hubDb } from "../database/hubDb.js";
2
+ export async function getVariables() {
3
+ return hubDb.variables.findMany({ orderBy: { key: "asc" } });
4
+ }
5
+ export async function saveVariable(key, value, exportToShell, userUuid) {
6
+ await hubDb.variables.upsert({
7
+ where: { key },
8
+ update: { value, export_to_shell: exportToShell, updated_by: userUuid },
9
+ create: {
10
+ key,
11
+ value,
12
+ export_to_shell: exportToShell,
13
+ created_by: userUuid,
14
+ updated_by: userUuid,
15
+ },
16
+ });
17
+ return { success: true, message: "Variable saved" };
18
+ }
19
+ export async function deleteVariable(key) {
20
+ await hubDb.variables.delete({ where: { key } });
21
+ return { success: true, message: "Variable deleted" };
22
+ }
23
+ //# sourceMappingURL=variableService.js.map
@@ -0,0 +1,221 @@
1
+ import "dotenv/config";
2
+ import "./schema-registry.js";
3
+ import { expandNaisysFolder } from "@naisys/common-node";
4
+ expandNaisysFolder();
5
+ // Important to load dotenv before any other imports, to ensure environment variables are available
6
+ import cookie from "@fastify/cookie";
7
+ import cors from "@fastify/cors";
8
+ import multipart from "@fastify/multipart";
9
+ import { fastifyRateLimit as rateLimit } from "@fastify/rate-limit";
10
+ import staticFiles from "@fastify/static";
11
+ import swagger from "@fastify/swagger";
12
+ import { commonErrorHandler, MAX_ATTACHMENT_SIZE, registerLenientJsonParser, registerSecurityHeaders, SUPER_ADMIN_USERNAME, } from "@naisys/common";
13
+ import { createHubDatabaseClient } from "@naisys/hub-database";
14
+ import { createSupervisorDatabaseClient, deploySupervisorMigrations, ensureSuperAdmin, handleResetPassword, } from "@naisys/supervisor-database";
15
+ import { PermissionEnum } from "@naisys/supervisor-shared";
16
+ import Fastify from "fastify";
17
+ import { jsonSchemaTransform, jsonSchemaTransformObject, serializerCompiler, validatorCompiler, } from "fastify-type-provider-zod";
18
+ import path from "path";
19
+ import { fileURLToPath } from "url";
20
+ import { registerApiReference } from "./api-reference.js";
21
+ import { initHubDb } from "./database/hubDb.js";
22
+ import { initSupervisorDb } from "./database/supervisorDb.js";
23
+ import { initLogger } from "./logger.js";
24
+ import apiRoutes from "./routes/api.js";
25
+ import { refreshUserLookup } from "./services/agentService.js";
26
+ import { initBrowserSocket } from "./services/browserSocketService.js";
27
+ import { initHubConnection } from "./services/hubConnectionService.js";
28
+ import { getUserByUsername } from "./services/userService.js";
29
+ export const startServer = async (startupType, plugins = [], hubPort) => {
30
+ if (startupType === "hosted") {
31
+ process.env.NODE_ENV = "production";
32
+ }
33
+ const isProd = process.env.NODE_ENV === "production";
34
+ // Auto-migrate supervisor database
35
+ await deploySupervisorMigrations();
36
+ if (!(await createSupervisorDatabaseClient())) {
37
+ console.error("[Supervisor] Supervisor database not found. Cannot start without it.");
38
+ process.exit(1);
39
+ }
40
+ // Hub DB still needed for agent API key auth
41
+ await createHubDatabaseClient();
42
+ // Initialize local Prisma clients (after migrations so they don't lock the DB)
43
+ await initSupervisorDb();
44
+ await initHubDb();
45
+ // Populate in-memory user lookup for username ↔ id resolution
46
+ await refreshUserLookup();
47
+ const superAdminResult = await ensureSuperAdmin();
48
+ if (superAdminResult.created) {
49
+ console.log(`\n ${SUPER_ADMIN_USERNAME} user created. Password: ${superAdminResult.generatedPassword}`);
50
+ console.log(` Change it via the web UI or ns-admin-pw command\n`);
51
+ }
52
+ const __filename = fileURLToPath(import.meta.url);
53
+ const __dirname = path.dirname(__filename);
54
+ const fastify = Fastify({
55
+ logger:
56
+ // Log to file in hosted mode
57
+ startupType === "hosted"
58
+ ? {
59
+ level: "info",
60
+ transport: {
61
+ target: "pino/file",
62
+ options: {
63
+ destination: path.join(process.env.NAISYS_FOLDER || "", "logs", "supervisor.log"),
64
+ mkdir: true,
65
+ },
66
+ },
67
+ }
68
+ : // Log to console in standalone mode
69
+ {
70
+ level: "info",
71
+ transport: {
72
+ target: "pino-pretty",
73
+ options: {
74
+ colorize: true,
75
+ },
76
+ },
77
+ },
78
+ }).withTypeProvider();
79
+ initLogger(fastify.log);
80
+ // Connect to hub via Socket.IO for agent management
81
+ const hubUrl = hubPort ? `https://localhost:${hubPort}` : process.env.HUB_URL;
82
+ if (hubUrl) {
83
+ initHubConnection(hubUrl);
84
+ }
85
+ // Set Zod validator and serializer compilers
86
+ fastify.setValidatorCompiler(validatorCompiler);
87
+ fastify.setSerializerCompiler(serializerCompiler);
88
+ registerLenientJsonParser(fastify);
89
+ fastify.setErrorHandler(commonErrorHandler);
90
+ await fastify.register(cors, {
91
+ origin: isProd ? false : ["http://localhost:3002"],
92
+ });
93
+ registerSecurityHeaders(fastify, { enforceHsts: isProd });
94
+ await fastify.register(cookie);
95
+ // Rate limiting
96
+ await fastify.register(rateLimit, {
97
+ max: 500,
98
+ timeWindow: "1 minute",
99
+ allowList: (request) => !request.url.startsWith("/api/"),
100
+ });
101
+ await fastify.register(multipart, {
102
+ limits: { fileSize: MAX_ATTACHMENT_SIZE },
103
+ });
104
+ // Register Swagger + Scalar
105
+ await fastify.register(swagger, {
106
+ openapi: {
107
+ info: {
108
+ title: "NAISYS Supervisor API",
109
+ description: "API documentation for NAISYS Supervisor server",
110
+ version: "1.0.0",
111
+ },
112
+ components: {
113
+ securitySchemes: {
114
+ cookieAuth: {
115
+ type: "apiKey",
116
+ in: "cookie",
117
+ name: "naisys_session",
118
+ },
119
+ },
120
+ },
121
+ },
122
+ transform: jsonSchemaTransform,
123
+ transformObject: jsonSchemaTransformObject,
124
+ });
125
+ await registerApiReference(fastify);
126
+ fastify.get("/", { schema: { hide: true } }, async (_request, reply) => {
127
+ return reply.redirect("/supervisor/");
128
+ });
129
+ fastify.register(apiRoutes, { prefix: "/api/supervisor" });
130
+ // Public endpoint to expose client configuration (plugins, publicRead, etc.)
131
+ fastify.get("/api/supervisor/client-config", { schema: { hide: true } }, () => ({
132
+ plugins,
133
+ publicRead: process.env.PUBLIC_READ === "true",
134
+ permissions: PermissionEnum.options,
135
+ }));
136
+ // Conditionally load ERP plugin
137
+ if (plugins.includes("erp")) {
138
+ // Use variable to avoid compile-time type dependency on @naisys/erp (allows parallel builds)
139
+ const erpModule = "@naisys/erp";
140
+ const { erpPlugin, enableSupervisorAuth } = (await import(erpModule));
141
+ enableSupervisorAuth();
142
+ await fastify.register(erpPlugin);
143
+ }
144
+ if (isProd) {
145
+ const clientDistPath = path.join(__dirname, "../client-dist");
146
+ await fastify.register(staticFiles, {
147
+ root: clientDistPath,
148
+ prefix: "/supervisor/",
149
+ });
150
+ fastify.setNotFoundHandler((request, reply) => {
151
+ if (request.url.startsWith("/api/")) {
152
+ reply.code(404).send({ error: "API endpoint not found" });
153
+ }
154
+ else if (request.url.startsWith("/supervisor")) {
155
+ reply.sendFile("index.html");
156
+ }
157
+ else {
158
+ reply.sendFile("index.html");
159
+ }
160
+ });
161
+ }
162
+ try {
163
+ let port = Number(process.env.SUPERVISOR_PORT) || 3001;
164
+ const host = isProd ? "0.0.0.0" : "localhost";
165
+ const maxAttempts = 100;
166
+ let attempts = 0;
167
+ while (attempts < maxAttempts) {
168
+ try {
169
+ await fastify.listen({ port, host });
170
+ initBrowserSocket(fastify.server, isProd);
171
+ fastify.log.info(`[Supervisor] Running on http://${host}:${port}/supervisor`);
172
+ return port;
173
+ }
174
+ catch (err) {
175
+ if (err.code === "EADDRINUSE") {
176
+ fastify.log.warn(`[Supervisor] Port ${port} is in use, trying port ${port + 1}...`);
177
+ port++;
178
+ attempts++;
179
+ if (attempts >= maxAttempts) {
180
+ throw new Error(`Unable to find available port after ${maxAttempts} attempts`);
181
+ }
182
+ }
183
+ else {
184
+ throw err;
185
+ }
186
+ }
187
+ }
188
+ // Unreachable — the loop either returns or throws
189
+ throw new Error("Unreachable");
190
+ }
191
+ catch (err) {
192
+ console.error("[Supervisor] Failed to start:", err);
193
+ fastify.log.error(err);
194
+ process.exit(1);
195
+ }
196
+ };
197
+ // Start server if this file is run directly
198
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
199
+ if (process.argv.includes("--reset-password")) {
200
+ const usernameIdx = process.argv.indexOf("--username");
201
+ const passwordIdx = process.argv.indexOf("--password");
202
+ const username = usernameIdx !== -1 ? process.argv[usernameIdx + 1] : undefined;
203
+ const password = passwordIdx !== -1 ? process.argv[passwordIdx + 1] : undefined;
204
+ await initSupervisorDb();
205
+ void handleResetPassword({
206
+ findLocalUser: async (username) => {
207
+ const user = await getUserByUsername(username);
208
+ return user
209
+ ? { id: user.id, username: user.username, uuid: user.uuid }
210
+ : null;
211
+ },
212
+ updateLocalPassword: async () => { },
213
+ username,
214
+ password,
215
+ });
216
+ }
217
+ else {
218
+ void startServer("standalone");
219
+ }
220
+ }
221
+ //# sourceMappingURL=supervisorServer.js.map