@naisys/hub 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.
@@ -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,129 @@
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 https from "https";
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 { loadOrCreateCert } from "./services/certService.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 self-signed TLS cert and access key
37
+ const certInfo = await loadOrCreateCert();
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 shared HTTPS server and Socket.IO instance
47
+ const httpsServer = https.createServer({
48
+ key: certInfo.key,
49
+ cert: certInfo.cert,
50
+ });
51
+ // Register HTTP attachment upload/download handler before Socket.IO
52
+ createHubAttachmentService(httpsServer, hubDatabaseService, logService);
53
+ const io = new Server(httpsServer, {
54
+ cors: {
55
+ origin: "*", // In production, restrict this
56
+ methods: ["GET", "POST"],
57
+ },
58
+ });
59
+ // Create NAISYS server on /naisys namespace
60
+ const naisysServer = createNaisysServer(io.of("/naisys"), certInfo.hubAccessKey, logService, hostRegistrar);
61
+ // Register hub access key rotation handler
62
+ createHubAccessKeyService(naisysServer, logService, certInfo.cert);
63
+ // Register hub config service for config_get requests from NAISYS instances
64
+ const configService = await createHubConfigService(naisysServer, hubDatabaseService, logService);
65
+ // Register hub user service for user_list requests from NAISYS instances
66
+ createHubUserService(naisysServer, hubDatabaseService, logService);
67
+ // Register hub models service for seeding and broadcasting models
68
+ await createHubModelsService(naisysServer, hubDatabaseService, logService);
69
+ // Register hub host service for broadcasting connected host list
70
+ createHubHostService(naisysServer, hostRegistrar, logService);
71
+ // Register hub run service for session_create/session_increment requests
72
+ createHubRunService(naisysServer, hubDatabaseService, logService);
73
+ // Register hub heartbeat service for NAISYS instance heartbeat tracking
74
+ const heartbeatService = createHubHeartbeatService(naisysServer, hubDatabaseService, logService);
75
+ // Register hub log service for log_write events from NAISYS instances
76
+ createHubLogService(naisysServer, hubDatabaseService, logService, heartbeatService);
77
+ // Register hub cost service for cost_write events from NAISYS instances
78
+ const costService = createHubCostService(naisysServer, hubDatabaseService, logService, heartbeatService, configService);
79
+ // Register hub send mail service (pure mail sending, no auto-start logic)
80
+ const sendMailService = createHubSendMailService(naisysServer, hubDatabaseService, heartbeatService);
81
+ // Register hub agent service for agent_start requests routed to target hosts
82
+ const agentService = createHubAgentService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, hostRegistrar);
83
+ // Register hub mail service for mail events from NAISYS instances
84
+ createHubMailService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, agentService, costService, configService);
85
+ // Start listening
86
+ await new Promise((resolve, reject) => {
87
+ httpsServer.once("error", reject);
88
+ httpsServer.listen(hubPort, () => {
89
+ httpsServer.removeListener("error", reject);
90
+ logService.log(`[Hub] Server listening on port ${hubPort}`);
91
+ resolve();
92
+ });
93
+ });
94
+ logService.log(`[Hub] Running on wss://localhost:${hubPort}, logs written to file`);
95
+ logService.disableConsole();
96
+ /**
97
+ * There should be no dependency between supervisor and hub
98
+ * Sharing the same process space is to save 150 mb of node.js runtime memory on small servers
99
+ */
100
+ let supervisorPort;
101
+ if (startSupervisor) {
102
+ // Don't import the whole fastify web server module tree unless needed
103
+ // Use variable to avoid compile-time type dependency on @naisys/supervisor (allows parallel builds)
104
+ const supervisorModule = "@naisys/supervisor";
105
+ const { startServer } = (await import(supervisorModule));
106
+ supervisorPort = await startServer("hosted", plugins, hubPort);
107
+ }
108
+ return { hubPort, supervisorPort };
109
+ }
110
+ catch (err) {
111
+ console.error("[Hub] Failed to start hub server:", err);
112
+ process.exit(1);
113
+ }
114
+ };
115
+ // Start server if this file is run directly
116
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
117
+ dotenv.config({ quiet: true });
118
+ expandNaisysFolder();
119
+ program
120
+ .argument("[agent-path]", "Path to agent configuration file to seed the database (optional)")
121
+ .option("--supervisor", "Start Supervisor web server")
122
+ .option("--erp", "Start ERP web app (requires --supervisor)")
123
+ .parse();
124
+ const plugins = [];
125
+ if (program.opts().erp)
126
+ plugins.push("erp");
127
+ void startHub("standalone", program.opts().supervisor, plugins, program.args[0]);
128
+ }
129
+ //# sourceMappingURL=naisysHub.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,78 @@
1
+ import { computeCertFingerprint } from "@naisys/common-node";
2
+ import { randomBytes } from "crypto";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { generate } from "selfsigned";
6
+ /**
7
+ * Loads existing TLS cert/key from NAISYS_FOLDER/cert/ or generates a new
8
+ * self-signed pair plus a random access key. Returns the PEM strings and
9
+ * the hubAccessKey (fingerprint_prefix+secret) used for client auth.
10
+ */
11
+ export async function loadOrCreateCert() {
12
+ const naisysFolder = process.env.NAISYS_FOLDER || "";
13
+ const certDir = join(naisysFolder, "cert");
14
+ const keyPath = join(certDir, "hub-key.pem");
15
+ const certPath = join(certDir, "hub-cert.pem");
16
+ const accessKeyPath = join(certDir, "hub-access-key");
17
+ let key;
18
+ let cert;
19
+ if (existsSync(keyPath) && existsSync(certPath)) {
20
+ key = readFileSync(keyPath, "utf-8");
21
+ cert = readFileSync(certPath, "utf-8");
22
+ }
23
+ else {
24
+ mkdirSync(certDir, { recursive: true });
25
+ const attrs = [{ name: "commonName", value: "NAISYS Hub" }];
26
+ const notAfterDate = new Date();
27
+ notAfterDate.setFullYear(notAfterDate.getFullYear() + 10);
28
+ const pems = await generate(attrs, {
29
+ keySize: 2048,
30
+ algorithm: "sha256",
31
+ notAfterDate,
32
+ });
33
+ key = pems.private;
34
+ cert = pems.cert;
35
+ writeFileSync(keyPath, key, { mode: 0o600 });
36
+ writeFileSync(certPath, cert);
37
+ }
38
+ // Load or generate the secret access key
39
+ let hubAccessKey;
40
+ if (existsSync(accessKeyPath)) {
41
+ hubAccessKey = readFileSync(accessKeyPath, "utf-8").trim();
42
+ }
43
+ else {
44
+ const secretKey = randomBytes(8).toString("hex"); // 16 hex chars
45
+ // Compute SHA-256 fingerprint from DER-encoded cert (first 16 hex chars)
46
+ const derMatch = cert.match(/-----BEGIN CERTIFICATE-----\s*([\s\S]+?)\s*-----END CERTIFICATE-----/);
47
+ if (!derMatch) {
48
+ throw new Error("Failed to parse PEM certificate");
49
+ }
50
+ const der = Buffer.from(derMatch[1].replace(/\s/g, ""), "base64");
51
+ const fingerprint = computeCertFingerprint(der);
52
+ const fingerprintPrefix = fingerprint.substring(0, 16);
53
+ hubAccessKey = `${fingerprintPrefix}+${secretKey}`;
54
+ writeFileSync(accessKeyPath, hubAccessKey, { mode: 0o600 });
55
+ }
56
+ return { key, cert, hubAccessKey };
57
+ }
58
+ /**
59
+ * Rotates the hub access key by generating a new random secret while
60
+ * keeping the same certificate fingerprint prefix. Writes the new key
61
+ * to disk and returns it.
62
+ */
63
+ export function rotateAccessKey(cert) {
64
+ const naisysFolder = process.env.NAISYS_FOLDER || "";
65
+ const accessKeyPath = join(naisysFolder, "cert", "hub-access-key");
66
+ const secretKey = randomBytes(8).toString("hex"); // 16 hex chars
67
+ const derMatch = cert.match(/-----BEGIN CERTIFICATE-----\s*([\s\S]+?)\s*-----END CERTIFICATE-----/);
68
+ if (!derMatch) {
69
+ throw new Error("Failed to parse PEM certificate");
70
+ }
71
+ const der = Buffer.from(derMatch[1].replace(/\s/g, ""), "base64");
72
+ const fingerprint = computeCertFingerprint(der);
73
+ const fingerprintPrefix = fingerprint.substring(0, 16);
74
+ const newAccessKey = `${fingerprintPrefix}+${secretKey}`;
75
+ writeFileSync(accessKeyPath, newAccessKey, { mode: 0o600 });
76
+ return newAccessKey;
77
+ }
78
+ //# sourceMappingURL=certService.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