@naisys/hub 3.0.0-beta.10
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-hub +2 -0
- package/dist/handlers/hubAccessKeyService.js +28 -0
- package/dist/handlers/hubAgentService.js +238 -0
- package/dist/handlers/hubAttachmentService.js +227 -0
- package/dist/handlers/hubConfigService.js +85 -0
- package/dist/handlers/hubCostService.js +279 -0
- package/dist/handlers/hubHeartbeatService.js +132 -0
- package/dist/handlers/hubHostService.js +35 -0
- package/dist/handlers/hubLogService.js +121 -0
- package/dist/handlers/hubMailService.js +397 -0
- package/dist/handlers/hubModelsService.js +106 -0
- package/dist/handlers/hubRunService.js +96 -0
- package/dist/handlers/hubSendMailService.js +126 -0
- package/dist/handlers/hubUserService.js +62 -0
- package/dist/naisysHub.js +126 -0
- package/dist/services/accessKeyService.js +32 -0
- package/dist/services/agentRegistrar.js +53 -0
- package/dist/services/hostRegistrar.js +82 -0
- package/dist/services/hubServerLog.js +47 -0
- package/dist/services/naisysConnection.js +45 -0
- package/dist/services/naisysServer.js +164 -0
- package/package.json +57 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { HubEvents, } from "@naisys/hub-protocol";
|
|
2
|
+
/** Pure send-mail service with no auto-start logic, breaking the circular dependency */
|
|
3
|
+
export function createHubSendMailService(naisysServer, { hubDb }, heartbeatService) {
|
|
4
|
+
/** Send a mail message directly by user IDs */
|
|
5
|
+
async function sendMail(params) {
|
|
6
|
+
const now = new Date();
|
|
7
|
+
// Build participants string from usernames (sorted alphabetically)
|
|
8
|
+
const allUserIds = [
|
|
9
|
+
...new Set([params.fromUserId, ...params.recipientUserIds]),
|
|
10
|
+
];
|
|
11
|
+
const users = await hubDb.users.findMany({
|
|
12
|
+
where: { id: { in: allUserIds } },
|
|
13
|
+
select: { username: true },
|
|
14
|
+
});
|
|
15
|
+
const participants = users
|
|
16
|
+
.map((u) => u.username)
|
|
17
|
+
.sort()
|
|
18
|
+
.join(",");
|
|
19
|
+
// Atomic transaction: create message, link attachments, add recipients, update notifications
|
|
20
|
+
const message = await hubDb.$transaction(async (hubTx) => {
|
|
21
|
+
const msg = await hubTx.mail_messages.create({
|
|
22
|
+
data: {
|
|
23
|
+
from_user_id: params.fromUserId,
|
|
24
|
+
host_id: params.hostId,
|
|
25
|
+
kind: params.kind,
|
|
26
|
+
participants,
|
|
27
|
+
subject: params.subject,
|
|
28
|
+
body: params.body,
|
|
29
|
+
created_at: now,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
// Link uploaded attachments to the new message via junction table
|
|
33
|
+
if (params.attachmentIds?.length) {
|
|
34
|
+
// Verify all attachment IDs exist
|
|
35
|
+
const found = await hubTx.attachments.findMany({
|
|
36
|
+
where: { id: { in: params.attachmentIds } },
|
|
37
|
+
select: { id: true },
|
|
38
|
+
});
|
|
39
|
+
if (found.length !== params.attachmentIds.length) {
|
|
40
|
+
const foundIds = new Set(found.map((a) => a.id));
|
|
41
|
+
const missing = params.attachmentIds.filter((id) => !foundIds.has(id));
|
|
42
|
+
throw new Error(`Attachments not found: ${missing.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
await hubTx.mail_attachments.createMany({
|
|
45
|
+
data: params.attachmentIds.map((attId) => ({
|
|
46
|
+
message_id: msg.id,
|
|
47
|
+
attachment_id: attId,
|
|
48
|
+
})),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
await hubTx.mail_recipients.createMany({
|
|
52
|
+
data: params.recipientUserIds.map((userId) => ({
|
|
53
|
+
message_id: msg.id,
|
|
54
|
+
user_id: userId,
|
|
55
|
+
type: "to",
|
|
56
|
+
created_at: now,
|
|
57
|
+
})),
|
|
58
|
+
});
|
|
59
|
+
// Add sender as 'from' recipient for archive tracking (pre-read since they wrote it)
|
|
60
|
+
if (!params.recipientUserIds.includes(params.fromUserId)) {
|
|
61
|
+
await hubTx.mail_recipients.create({
|
|
62
|
+
data: {
|
|
63
|
+
message_id: msg.id,
|
|
64
|
+
user_id: params.fromUserId,
|
|
65
|
+
type: "from",
|
|
66
|
+
read_at: now,
|
|
67
|
+
created_at: now,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const notificationField = params.kind === "chat" ? "latest_chat_id" : "latest_mail_id";
|
|
72
|
+
await hubTx.user_notifications.updateMany({
|
|
73
|
+
where: { user_id: { in: params.recipientUserIds } },
|
|
74
|
+
data: { [notificationField]: msg.id },
|
|
75
|
+
});
|
|
76
|
+
return msg;
|
|
77
|
+
});
|
|
78
|
+
const heartbeatField = params.kind === "chat" ? "latestChatId" : "latestMailId";
|
|
79
|
+
for (const userId of params.recipientUserIds) {
|
|
80
|
+
heartbeatService.updateAgentNotification(userId, heartbeatField, message.id);
|
|
81
|
+
}
|
|
82
|
+
heartbeatService.throttledPushAgentsStatus();
|
|
83
|
+
const targetHostIds = new Set();
|
|
84
|
+
for (const userId of params.recipientUserIds) {
|
|
85
|
+
for (const hId of heartbeatService.findHostsForAgent(userId)) {
|
|
86
|
+
targetHostIds.add(hId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (targetHostIds.size > 0) {
|
|
90
|
+
const payload = {
|
|
91
|
+
recipientUserIds: params.recipientUserIds,
|
|
92
|
+
kind: params.kind,
|
|
93
|
+
};
|
|
94
|
+
for (const targetHostId of targetHostIds) {
|
|
95
|
+
naisysServer.sendMessage(targetHostId, HubEvents.MAIL_RECEIVED, payload);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Query attachment metadata for the push if there are attachments
|
|
99
|
+
let attachments;
|
|
100
|
+
if (params.attachmentIds?.length) {
|
|
101
|
+
const rows = await hubDb.attachments.findMany({
|
|
102
|
+
where: { id: { in: params.attachmentIds } },
|
|
103
|
+
select: { public_id: true, filename: true, file_size: true },
|
|
104
|
+
});
|
|
105
|
+
attachments = rows.map((r) => ({
|
|
106
|
+
id: r.public_id,
|
|
107
|
+
filename: r.filename,
|
|
108
|
+
fileSize: r.file_size,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
// Push full message data to supervisor connections
|
|
112
|
+
naisysServer.broadcastToSupervisors(HubEvents.MAIL_PUSH, {
|
|
113
|
+
recipientUserIds: params.recipientUserIds,
|
|
114
|
+
fromUserId: params.fromUserId,
|
|
115
|
+
kind: params.kind,
|
|
116
|
+
messageId: message.id,
|
|
117
|
+
subject: params.subject,
|
|
118
|
+
body: params.body,
|
|
119
|
+
createdAt: now.toISOString(),
|
|
120
|
+
participants,
|
|
121
|
+
attachments,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return { sendMail };
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=hubSendMailService.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { HubEvents } from "@naisys/hub-protocol";
|
|
2
|
+
/** Pushes the user list to NAISYS instances when they connect or when users change */
|
|
3
|
+
export function createHubUserService(naisysServer, { hubDb }, logService) {
|
|
4
|
+
async function buildUserListPayload() {
|
|
5
|
+
const dbUsers = await hubDb.users.findMany({
|
|
6
|
+
where: { archived: false },
|
|
7
|
+
select: {
|
|
8
|
+
id: true,
|
|
9
|
+
username: true,
|
|
10
|
+
enabled: true,
|
|
11
|
+
config: true,
|
|
12
|
+
lead_user_id: true,
|
|
13
|
+
api_key: true,
|
|
14
|
+
user_hosts: {
|
|
15
|
+
select: { host_id: true },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const users = dbUsers.map((u) => ({
|
|
20
|
+
userId: u.id,
|
|
21
|
+
username: u.username,
|
|
22
|
+
enabled: u.enabled,
|
|
23
|
+
leadUserId: u.lead_user_id || undefined,
|
|
24
|
+
config: JSON.parse(u.config),
|
|
25
|
+
assignedHostIds: u.user_hosts.length > 0
|
|
26
|
+
? u.user_hosts.map((uh) => uh.host_id)
|
|
27
|
+
: undefined,
|
|
28
|
+
apiKey: u.api_key || undefined,
|
|
29
|
+
}));
|
|
30
|
+
return { success: true, users };
|
|
31
|
+
}
|
|
32
|
+
async function broadcastUserList() {
|
|
33
|
+
try {
|
|
34
|
+
const payload = await buildUserListPayload();
|
|
35
|
+
logService.log(`[Hub:Users] Broadcasting ${payload.users?.length ?? 0} users to all clients`);
|
|
36
|
+
naisysServer.broadcastToAll(HubEvents.USERS_UPDATED, payload);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
logService.error(`[Hub:Users] Error broadcasting user list: ${error}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Push user list to newly connected clients
|
|
43
|
+
naisysServer.registerEvent(HubEvents.CLIENT_CONNECTED, async (hostId, connection) => {
|
|
44
|
+
try {
|
|
45
|
+
const payload = await buildUserListPayload();
|
|
46
|
+
logService.log(`[Hub:Users] Pushing ${payload.users?.length ?? 0} users to instance ${hostId}`);
|
|
47
|
+
connection.sendMessage(HubEvents.USERS_UPDATED, payload);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
logService.error(`[Hub:Users] Error querying users for instance ${hostId}: ${error}`);
|
|
51
|
+
connection.sendMessage(HubEvents.USERS_UPDATED, {
|
|
52
|
+
success: false,
|
|
53
|
+
error: String(error),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// Broadcast user list to all clients when users are created/edited
|
|
58
|
+
naisysServer.registerEvent(HubEvents.USERS_CHANGED, async () => {
|
|
59
|
+
await broadcastUserList();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=hubUserService.js.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { expandNaisysFolder } from "@naisys/common-node";
|
|
2
|
+
import { createHubDatabaseService } from "@naisys/hub-database";
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import dotenv from "dotenv";
|
|
5
|
+
import http from "http";
|
|
6
|
+
import { Server } from "socket.io";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { createHubAccessKeyService } from "./handlers/hubAccessKeyService.js";
|
|
9
|
+
import { createHubAgentService } from "./handlers/hubAgentService.js";
|
|
10
|
+
import { createHubAttachmentService } from "./handlers/hubAttachmentService.js";
|
|
11
|
+
import { createHubConfigService } from "./handlers/hubConfigService.js";
|
|
12
|
+
import { createHubCostService } from "./handlers/hubCostService.js";
|
|
13
|
+
import { createHubHeartbeatService } from "./handlers/hubHeartbeatService.js";
|
|
14
|
+
import { createHubHostService } from "./handlers/hubHostService.js";
|
|
15
|
+
import { createHubLogService } from "./handlers/hubLogService.js";
|
|
16
|
+
import { createHubMailService } from "./handlers/hubMailService.js";
|
|
17
|
+
import { createHubModelsService } from "./handlers/hubModelsService.js";
|
|
18
|
+
import { createHubRunService } from "./handlers/hubRunService.js";
|
|
19
|
+
import { createHubSendMailService } from "./handlers/hubSendMailService.js";
|
|
20
|
+
import { createHubUserService } from "./handlers/hubUserService.js";
|
|
21
|
+
import { seedAgentConfigs } from "./services/agentRegistrar.js";
|
|
22
|
+
import { loadOrCreateAccessKey } from "./services/accessKeyService.js";
|
|
23
|
+
import { createHostRegistrar } from "./services/hostRegistrar.js";
|
|
24
|
+
import { createHubServerLog } from "./services/hubServerLog.js";
|
|
25
|
+
import { createNaisysServer } from "./services/naisysServer.js";
|
|
26
|
+
/**
|
|
27
|
+
* Starts the Hub server with sync service.
|
|
28
|
+
* Can be called standalone or inline from naisys with --integrated-hub flag.
|
|
29
|
+
*/
|
|
30
|
+
export const startHub = async (startupType, startSupervisor, plugins, startupAgentPath) => {
|
|
31
|
+
try {
|
|
32
|
+
// Create log service first
|
|
33
|
+
const logService = createHubServerLog(startupType);
|
|
34
|
+
logService.log(`[Hub] Starting Hub server in ${startupType} mode...`);
|
|
35
|
+
const hubPort = Number(process.env.HUB_PORT) || 3101;
|
|
36
|
+
// Load or generate hub access key for client authentication
|
|
37
|
+
const hubAccessKey = loadOrCreateAccessKey();
|
|
38
|
+
const naisysFolder = process.env.NAISYS_FOLDER || "";
|
|
39
|
+
logService.log(`[Hub] Hub access key located at: ${naisysFolder}/cert/hub-access-key`);
|
|
40
|
+
// Schema version for sync protocol - should match NAISYS instance
|
|
41
|
+
const hubDatabaseService = await createHubDatabaseService();
|
|
42
|
+
// Seed database with agent configs from yaml files (one-time, skips if non-empty)
|
|
43
|
+
await seedAgentConfigs(hubDatabaseService, logService, startupAgentPath);
|
|
44
|
+
// Create host registrar for tracking NAISYS instance connections
|
|
45
|
+
const hostRegistrar = await createHostRegistrar(hubDatabaseService);
|
|
46
|
+
// Create HTTP server and Socket.IO instance (TLS is handled by the reverse proxy)
|
|
47
|
+
const httpServer = http.createServer();
|
|
48
|
+
// Register HTTP attachment upload/download handler before Socket.IO
|
|
49
|
+
createHubAttachmentService(httpServer, hubDatabaseService, logService);
|
|
50
|
+
const io = new Server(httpServer, {
|
|
51
|
+
path: "/hub/socket.io",
|
|
52
|
+
cors: {
|
|
53
|
+
origin: "*", // In production, restrict this
|
|
54
|
+
methods: ["GET", "POST"],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const naisysServer = createNaisysServer(io, hubAccessKey, logService, hostRegistrar);
|
|
58
|
+
// Register hub access key rotation handler
|
|
59
|
+
createHubAccessKeyService(naisysServer, logService);
|
|
60
|
+
// Register hub config service for config_get requests from NAISYS instances
|
|
61
|
+
const configService = await createHubConfigService(naisysServer, hubDatabaseService, logService);
|
|
62
|
+
// Register hub user service for user_list requests from NAISYS instances
|
|
63
|
+
createHubUserService(naisysServer, hubDatabaseService, logService);
|
|
64
|
+
// Register hub models service for seeding and broadcasting models
|
|
65
|
+
await createHubModelsService(naisysServer, hubDatabaseService, logService);
|
|
66
|
+
// Register hub host service for broadcasting connected host list
|
|
67
|
+
createHubHostService(naisysServer, hostRegistrar, logService);
|
|
68
|
+
// Register hub run service for session_create/session_increment requests
|
|
69
|
+
createHubRunService(naisysServer, hubDatabaseService, logService);
|
|
70
|
+
// Register hub heartbeat service for NAISYS instance heartbeat tracking
|
|
71
|
+
const heartbeatService = createHubHeartbeatService(naisysServer, hubDatabaseService, logService);
|
|
72
|
+
// Register hub log service for log_write events from NAISYS instances
|
|
73
|
+
createHubLogService(naisysServer, hubDatabaseService, logService, heartbeatService);
|
|
74
|
+
// Register hub cost service for cost_write events from NAISYS instances
|
|
75
|
+
const costService = createHubCostService(naisysServer, hubDatabaseService, logService, heartbeatService, configService);
|
|
76
|
+
// Register hub send mail service (pure mail sending, no auto-start logic)
|
|
77
|
+
const sendMailService = createHubSendMailService(naisysServer, hubDatabaseService, heartbeatService);
|
|
78
|
+
// Register hub agent service for agent_start requests routed to target hosts
|
|
79
|
+
const agentService = createHubAgentService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, hostRegistrar);
|
|
80
|
+
// Register hub mail service for mail events from NAISYS instances
|
|
81
|
+
createHubMailService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, agentService, costService, configService);
|
|
82
|
+
// Start listening
|
|
83
|
+
await new Promise((resolve, reject) => {
|
|
84
|
+
httpServer.once("error", reject);
|
|
85
|
+
httpServer.listen(hubPort, () => {
|
|
86
|
+
httpServer.removeListener("error", reject);
|
|
87
|
+
logService.log(`[Hub] Server listening on port ${hubPort}`);
|
|
88
|
+
resolve();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
logService.log(`[Hub] Running on http://localhost:${hubPort}/hub, logs written to file`);
|
|
92
|
+
logService.disableConsole();
|
|
93
|
+
/**
|
|
94
|
+
* There should be no dependency between supervisor and hub
|
|
95
|
+
* Sharing the same process space is to save 150 mb of node.js runtime memory on small servers
|
|
96
|
+
*/
|
|
97
|
+
let supervisorPort;
|
|
98
|
+
if (startSupervisor) {
|
|
99
|
+
// Don't import the whole fastify web server module tree unless needed
|
|
100
|
+
// Use variable to avoid compile-time type dependency on @naisys/supervisor (allows parallel builds)
|
|
101
|
+
const supervisorModule = "@naisys/supervisor";
|
|
102
|
+
const { startServer } = (await import(supervisorModule));
|
|
103
|
+
supervisorPort = await startServer("hosted", plugins, hubPort);
|
|
104
|
+
}
|
|
105
|
+
return { hubPort, supervisorPort };
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error("[Hub] Failed to start hub server:", err);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
// Start server if this file is run directly
|
|
113
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
114
|
+
dotenv.config({ quiet: true });
|
|
115
|
+
expandNaisysFolder();
|
|
116
|
+
program
|
|
117
|
+
.argument("[agent-path]", "Path to agent configuration file to seed the database (optional)")
|
|
118
|
+
.option("--supervisor", "Start Supervisor web server")
|
|
119
|
+
.option("--erp", "Start ERP web app (requires --supervisor)")
|
|
120
|
+
.parse();
|
|
121
|
+
const plugins = [];
|
|
122
|
+
if (program.opts().erp)
|
|
123
|
+
plugins.push("erp");
|
|
124
|
+
void startHub("standalone", program.opts().supervisor, plugins, program.args[0]);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=naisysHub.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
/**
|
|
5
|
+
* Loads existing hub access key from NAISYS_FOLDER/cert/hub-access-key
|
|
6
|
+
* or generates a new random key. The access key is used by clients to
|
|
7
|
+
* authenticate their Socket.IO connections to the hub.
|
|
8
|
+
*/
|
|
9
|
+
export function loadOrCreateAccessKey() {
|
|
10
|
+
const naisysFolder = process.env.NAISYS_FOLDER || "";
|
|
11
|
+
const certDir = join(naisysFolder, "cert");
|
|
12
|
+
const accessKeyPath = join(certDir, "hub-access-key");
|
|
13
|
+
if (existsSync(accessKeyPath)) {
|
|
14
|
+
return readFileSync(accessKeyPath, "utf-8").trim();
|
|
15
|
+
}
|
|
16
|
+
mkdirSync(certDir, { recursive: true });
|
|
17
|
+
const accessKey = randomBytes(32).toString("hex");
|
|
18
|
+
writeFileSync(accessKeyPath, accessKey, { mode: 0o600 });
|
|
19
|
+
return accessKey;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Rotates the hub access key by generating a new random secret.
|
|
23
|
+
* Writes the new key to disk and returns it.
|
|
24
|
+
*/
|
|
25
|
+
export function rotateAccessKey() {
|
|
26
|
+
const naisysFolder = process.env.NAISYS_FOLDER || "";
|
|
27
|
+
const accessKeyPath = join(naisysFolder, "cert", "hub-access-key");
|
|
28
|
+
const newAccessKey = randomBytes(32).toString("hex");
|
|
29
|
+
writeFileSync(accessKeyPath, newAccessKey, { mode: 0o600 });
|
|
30
|
+
return newAccessKey;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=accessKeyService.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { toUrlSafeKey } from "@naisys/common";
|
|
2
|
+
import { loadAgentConfigs } from "@naisys/common-node";
|
|
3
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
4
|
+
/** Seeds agent configs from YAML files into an empty database. Skips if users already exist. */
|
|
5
|
+
export async function seedAgentConfigs({ hubDb }, logService, startupAgentPath) {
|
|
6
|
+
// Check if users table already has rows (seed-once pattern)
|
|
7
|
+
const count = await hubDb.users.count();
|
|
8
|
+
const hasUsers = count > 0;
|
|
9
|
+
if (hasUsers) {
|
|
10
|
+
logService.log("[Hub:AgentRegistrar] Agents already seeded");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// Default to CWD when no path specified (matches standalone hub behavior)
|
|
14
|
+
const users = loadAgentConfigs(startupAgentPath || "");
|
|
15
|
+
await seedUsersToDatabase(hubDb, logService, users);
|
|
16
|
+
}
|
|
17
|
+
async function seedUsersToDatabase(hubDb, logService, users) {
|
|
18
|
+
// First pass: create all users, build loader userId → DB id map
|
|
19
|
+
const loaderIdToDbId = new Map();
|
|
20
|
+
for (const user of users.values()) {
|
|
21
|
+
const safeUsername = toUrlSafeKey(user.username);
|
|
22
|
+
const dbUser = await hubDb.users.create({
|
|
23
|
+
data: {
|
|
24
|
+
uuid: randomUUID(),
|
|
25
|
+
username: safeUsername,
|
|
26
|
+
title: user.config.title,
|
|
27
|
+
config: JSON.stringify({ ...user.config, username: safeUsername }),
|
|
28
|
+
api_key: randomBytes(32).toString("hex"),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
loaderIdToDbId.set(user.userId, dbUser.id);
|
|
32
|
+
await hubDb.user_notifications.create({
|
|
33
|
+
data: {
|
|
34
|
+
user_id: dbUser.id,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Second pass: update lead_user_id relationships
|
|
39
|
+
for (const user of users.values()) {
|
|
40
|
+
if (user.leadUserId !== undefined) {
|
|
41
|
+
const dbId = loaderIdToDbId.get(user.userId);
|
|
42
|
+
const leadDbId = loaderIdToDbId.get(user.leadUserId);
|
|
43
|
+
if (dbId !== undefined && leadDbId !== undefined) {
|
|
44
|
+
await hubDb.users.update({
|
|
45
|
+
where: { id: dbId },
|
|
46
|
+
data: { lead_user_id: leadDbId },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
logService.log(`[Hub:AgentRegistrar] Seeded ${users.size} users into database`);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=agentRegistrar.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { toUrlSafeKey } from "@naisys/common";
|
|
2
|
+
export async function createHostRegistrar({ hubDb }) {
|
|
3
|
+
/** Cache of all known hosts keyed by id */
|
|
4
|
+
const hostsById = new Map();
|
|
5
|
+
// Seed the cache from the database
|
|
6
|
+
const rows = await hubDb.hosts.findMany({
|
|
7
|
+
select: { id: true, name: true, restricted: true, host_type: true },
|
|
8
|
+
});
|
|
9
|
+
for (const row of rows) {
|
|
10
|
+
hostsById.set(row.id, {
|
|
11
|
+
hostName: row.name,
|
|
12
|
+
restricted: row.restricted,
|
|
13
|
+
hostType: row.host_type,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Register a NAISYS instance by name. Creates a new record if not found,
|
|
18
|
+
* updates last_active on every call.
|
|
19
|
+
* @returns The host's autoincrement id
|
|
20
|
+
*/
|
|
21
|
+
async function registerHost(hostName, hostType, lastIp) {
|
|
22
|
+
hostName = toUrlSafeKey(hostName);
|
|
23
|
+
const existing = await hubDb.hosts.findUnique({
|
|
24
|
+
where: { name: hostName },
|
|
25
|
+
});
|
|
26
|
+
if (existing) {
|
|
27
|
+
await hubDb.hosts.update({
|
|
28
|
+
where: { id: existing.id },
|
|
29
|
+
data: {
|
|
30
|
+
last_active: new Date().toISOString(),
|
|
31
|
+
host_type: hostType,
|
|
32
|
+
last_ip: lastIp,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
hostsById.set(existing.id, {
|
|
36
|
+
hostName,
|
|
37
|
+
restricted: existing.restricted,
|
|
38
|
+
hostType,
|
|
39
|
+
});
|
|
40
|
+
return existing.id;
|
|
41
|
+
}
|
|
42
|
+
const created = await hubDb.hosts.create({
|
|
43
|
+
data: {
|
|
44
|
+
name: hostName,
|
|
45
|
+
host_type: hostType,
|
|
46
|
+
last_ip: lastIp,
|
|
47
|
+
last_active: new Date().toISOString(),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
hostsById.set(created.id, { hostName, restricted: false, hostType });
|
|
51
|
+
return created.id;
|
|
52
|
+
}
|
|
53
|
+
/** Returns all known hosts (from DB + any newly registered) */
|
|
54
|
+
function getAllHosts() {
|
|
55
|
+
return Array.from(hostsById, ([hostId, entry]) => ({
|
|
56
|
+
hostId,
|
|
57
|
+
hostName: entry.hostName,
|
|
58
|
+
restricted: entry.restricted,
|
|
59
|
+
hostType: entry.hostType,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/** Re-read all hosts from DB and replace the in-memory cache */
|
|
63
|
+
async function refreshHosts() {
|
|
64
|
+
const rows = await hubDb.hosts.findMany({
|
|
65
|
+
select: { id: true, name: true, restricted: true, host_type: true },
|
|
66
|
+
});
|
|
67
|
+
hostsById.clear();
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
hostsById.set(row.id, {
|
|
70
|
+
hostName: row.name,
|
|
71
|
+
restricted: row.restricted,
|
|
72
|
+
hostType: row.host_type,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
registerHost,
|
|
78
|
+
getAllHosts,
|
|
79
|
+
refreshHosts,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=hostRegistrar.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import pino from "pino";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a log service for the hub.
|
|
5
|
+
* In hosted mode, logs to a file using pino.
|
|
6
|
+
* In standalone mode, uses console.log.
|
|
7
|
+
*/
|
|
8
|
+
export function createHubServerLog(startupType) {
|
|
9
|
+
if (startupType === "hosted") {
|
|
10
|
+
const logPath = path.join(process.env.NAISYS_FOLDER || "", "logs", "hub-server.log");
|
|
11
|
+
const logger = pino({
|
|
12
|
+
level: "info",
|
|
13
|
+
transport: {
|
|
14
|
+
target: "pino/file",
|
|
15
|
+
options: {
|
|
16
|
+
destination: logPath,
|
|
17
|
+
mkdir: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
let consoleEnabled = true;
|
|
22
|
+
return {
|
|
23
|
+
log: (message) => {
|
|
24
|
+
logger.info(message);
|
|
25
|
+
if (consoleEnabled) {
|
|
26
|
+
console.log(message);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
error: (message) => {
|
|
30
|
+
logger.error(message);
|
|
31
|
+
if (consoleEnabled) {
|
|
32
|
+
console.error(message);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
disableConsole: () => {
|
|
36
|
+
consoleEnabled = false;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Standalone mode - use console
|
|
41
|
+
return {
|
|
42
|
+
log: (message) => console.log(message),
|
|
43
|
+
error: (message) => console.error(message),
|
|
44
|
+
disableConsole: () => { },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=hubServerLog.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles the lifecycle of a single NAISYS instance connection to the hub.
|
|
3
|
+
* Each connected NAISYS instance gets its own NaisysConnection instance.
|
|
4
|
+
*/
|
|
5
|
+
export function createNaisysConnection(socket, connectionInfo, raiseEvent, logService) {
|
|
6
|
+
const { hostId, hostName, connectedAt, hostType } = connectionInfo;
|
|
7
|
+
logService.log(`[Hub:Connection] NAISYS instance connected: ${hostName} (${hostId})`);
|
|
8
|
+
// Forward all socket events to hub's emit function
|
|
9
|
+
// Note: Socket.IO passes (eventName, ...args) where last arg may be an ack callback
|
|
10
|
+
socket.onAny((eventName, ...args) => {
|
|
11
|
+
logService.log(`[Hub:Connection] Received ${eventName} from ${hostName}`);
|
|
12
|
+
// Pass all args including any ack callback (usually data and optional ack)
|
|
13
|
+
raiseEvent(eventName, hostId, ...args);
|
|
14
|
+
});
|
|
15
|
+
// Handle disconnect
|
|
16
|
+
socket.on("disconnect", (reason) => {
|
|
17
|
+
logService.log(`[Hub:Connection] NAISYS instance disconnected: ${hostName} (${hostId}) - ${reason}`);
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Send a message to this client's socket.
|
|
21
|
+
* If ack callback is provided, waits for client acknowledgement.
|
|
22
|
+
*/
|
|
23
|
+
function sendMessage(event, payload, ack) {
|
|
24
|
+
if (ack) {
|
|
25
|
+
socket.emit(event, payload, ack);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
socket.emit(event, payload);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Forcefully disconnect this client */
|
|
32
|
+
function disconnect() {
|
|
33
|
+
socket.disconnect(true);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
sendMessage,
|
|
37
|
+
disconnect,
|
|
38
|
+
getHostId: () => hostId,
|
|
39
|
+
getHostName: () => hostName,
|
|
40
|
+
getConnectedAt: () => connectedAt,
|
|
41
|
+
getSocketId: () => socket.id,
|
|
42
|
+
getHostType: () => hostType,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=naisysConnection.js.map
|