@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.
- package/bin/naisys-supervisor +2 -0
- package/client-dist/android-chrome-192x192.png +0 -0
- package/client-dist/android-chrome-512x512.png +0 -0
- package/client-dist/apple-touch-icon.png +0 -0
- package/client-dist/assets/index-BBrK4ItN.js +177 -0
- package/client-dist/assets/index-CKg0vgt5.css +1 -0
- package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
- package/client-dist/favicon-16x16.png +0 -0
- package/client-dist/favicon-32x32.png +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +49 -0
- package/client-dist/site.webmanifest +22 -0
- package/dist/api-reference.js +52 -0
- package/dist/auth-middleware.js +116 -0
- package/dist/database/hubDb.js +26 -0
- package/dist/database/supervisorDb.js +18 -0
- package/dist/error-helpers.js +13 -0
- package/dist/hateoas.js +61 -0
- package/dist/logger.js +11 -0
- package/dist/route-helpers.js +7 -0
- package/dist/routes/admin.js +209 -0
- package/dist/routes/agentChat.js +194 -0
- package/dist/routes/agentConfig.js +265 -0
- package/dist/routes/agentLifecycle.js +350 -0
- package/dist/routes/agentMail.js +171 -0
- package/dist/routes/agentRuns.js +90 -0
- package/dist/routes/agents.js +236 -0
- package/dist/routes/api.js +52 -0
- package/dist/routes/attachments.js +18 -0
- package/dist/routes/auth.js +103 -0
- package/dist/routes/costs.js +51 -0
- package/dist/routes/hosts.js +296 -0
- package/dist/routes/models.js +152 -0
- package/dist/routes/root.js +56 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/status.js +20 -0
- package/dist/routes/users.js +420 -0
- package/dist/routes/variables.js +103 -0
- package/dist/schema-registry.js +23 -0
- package/dist/services/agentConfigService.js +182 -0
- package/dist/services/agentHostStatusService.js +178 -0
- package/dist/services/agentService.js +291 -0
- package/dist/services/attachmentProxyService.js +130 -0
- package/dist/services/browserSocketService.js +78 -0
- package/dist/services/chatService.js +201 -0
- package/dist/services/configExportService.js +61 -0
- package/dist/services/costsService.js +127 -0
- package/dist/services/hostService.js +156 -0
- package/dist/services/hubConnectionService.js +333 -0
- package/dist/services/logFileService.js +11 -0
- package/dist/services/mailService.js +154 -0
- package/dist/services/modelService.js +92 -0
- package/dist/services/runsService.js +164 -0
- package/dist/services/userService.js +147 -0
- package/dist/services/variableService.js +23 -0
- package/dist/supervisorServer.js +221 -0
- 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
|