@naisys/hub 3.0.0-beta.18 → 3.0.0-beta.19

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/.env.example CHANGED
@@ -1,5 +1,5 @@
1
1
  # Hub server port
2
- HUB_PORT=3101
2
+ SERVER_PORT=3101
3
3
 
4
- # NAISYS folder for logs and auto-generated TLS certificates
4
+ # NAISYS folder for logs and file uploads
5
5
  NAISYS_FOLDER="~/.naisys"
@@ -7,221 +7,207 @@ import { pipeline, Writable } from "stream";
7
7
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
8
8
  /**
9
9
  * HTTP attachment upload/download service.
10
- * Registers a `request` handler on the raw HTTPS server for `/attachments` paths.
11
- * Non-matching paths are ignored so Socket.IO still works.
10
+ * Registers Fastify routes for `/hub/attachments` paths.
12
11
  */
13
- export function createHubAttachmentService(httpServer, { hubDb }, logService) {
12
+ export function createHubAttachmentService(fastify, { hubDb }, logService) {
14
13
  const naisysFolder = process.env.NAISYS_FOLDER || "";
15
- httpServer.on("request", (req, res) => {
16
- const url = new URL(req.url || "", `https://${req.headers.host || "localhost"}`);
17
- const pathname = url.pathname;
18
- if (pathname === "/hub/attachments" && req.method === "POST") {
19
- handleUpload(url, req, res).catch((err) => {
20
- logService.error(`[Hub:Attachment] Upload error: ${err}`);
21
- if (!res.writableEnded) {
22
- res.writeHead(500, { "Content-Type": "application/json" });
23
- res.end(JSON.stringify({ error: "Internal server error" }));
24
- }
25
- });
26
- }
27
- else if (pathname.startsWith("/hub/attachments/") &&
28
- pathname !== "/hub/attachments/" &&
29
- req.method === "GET") {
30
- handleDownload(pathname, req, res).catch((err) => {
31
- logService.error(`[Hub:Attachment] Download error: ${err}`);
32
- if (!res.writableEnded) {
33
- res.writeHead(500, { "Content-Type": "application/json" });
34
- res.end(JSON.stringify({ error: "Internal server error" }));
35
- }
36
- });
37
- }
38
- // Non-matching paths: do nothing — let Socket.IO handle them
39
- });
40
14
  async function resolveUserByApiKey(apiKey) {
41
15
  const user = await hubDb.users.findUnique({ where: { api_key: apiKey } });
42
16
  return user?.id ?? null;
43
17
  }
44
- async function handleUpload(url, req, res) {
45
- const apiKey = extractBearerToken(req.headers.authorization);
46
- const filename = url.searchParams.get("filename");
47
- const fileSizeStr = url.searchParams.get("filesize");
48
- const fileHash = url.searchParams.get("filehash");
49
- const purpose = url.searchParams.get("purpose");
50
- if (!apiKey) {
51
- res.writeHead(401, { "Content-Type": "application/json" });
52
- res.end(JSON.stringify({ error: "Missing Authorization header" }));
53
- return;
54
- }
55
- if (!filename || !fileSizeStr || !fileHash || !purpose) {
56
- res.writeHead(400, { "Content-Type": "application/json" });
57
- res.end(JSON.stringify({
58
- error: "Missing required query params: filename, filesize, filehash, purpose",
59
- }));
60
- return;
61
- }
62
- if (purpose !== "mail" && purpose !== "context") {
63
- res.writeHead(400, { "Content-Type": "application/json" });
64
- res.end(JSON.stringify({
65
- error: 'Invalid purpose. Must be "mail" or "context"',
66
- }));
67
- return;
68
- }
69
- const fileSize = parseInt(fileSizeStr, 10);
70
- if (isNaN(fileSize) || fileSize <= 0) {
71
- res.writeHead(400, { "Content-Type": "application/json" });
72
- res.end(JSON.stringify({ error: "Invalid filesize" }));
73
- return;
74
- }
75
- if (fileSize > MAX_FILE_SIZE) {
76
- res.writeHead(413, { "Content-Type": "application/json" });
77
- res.end(JSON.stringify({
78
- error: `File too large. Max size: ${MAX_FILE_SIZE} bytes`,
79
- }));
80
- return;
81
- }
82
- const userId = await resolveUserByApiKey(apiKey);
83
- if (userId == null) {
84
- res.writeHead(401, { "Content-Type": "application/json" });
85
- res.end(JSON.stringify({ error: "Invalid API key" }));
86
- return;
87
- }
88
- // Stream to temp file, then move to content-addressable path
89
- const tmpDir = join(naisysFolder, "tmp", "hub", "attachments");
90
- mkdirSync(tmpDir, { recursive: true });
91
- const tmpPath = join(tmpDir, `${Date.now()}_${userId}_${Math.random().toString(36).slice(2)}`);
92
- const hash = createHash("sha256");
93
- let bytesWritten = 0;
94
- const fileStream = createWriteStream(tmpPath);
95
- const success = await new Promise((resolve) => {
96
- const sizeChecker = new Writable({
97
- write(chunk, _encoding, callback) {
98
- bytesWritten += chunk.length;
99
- if (bytesWritten > MAX_FILE_SIZE) {
100
- callback(new Error("File exceeds size limit"));
101
- return;
18
+ // Upload route encapsulated so the raw content-type parser doesn't leak
19
+ fastify.register((scope, _opts, done) => {
20
+ // Prevent Fastify from consuming the request body — we stream it to disk
21
+ scope.removeAllContentTypeParsers();
22
+ scope.addContentTypeParser("*", (_request, _payload, cb) => {
23
+ cb(null);
24
+ });
25
+ scope.post("/hub/attachments", async (request, reply) => {
26
+ try {
27
+ const apiKey = extractBearerToken(request.headers.authorization);
28
+ if (!apiKey) {
29
+ return reply
30
+ .code(401)
31
+ .send({ error: "Missing Authorization header" });
32
+ }
33
+ const url = new URL(request.url, `https://${request.headers.host || "localhost"}`);
34
+ const filename = url.searchParams.get("filename");
35
+ const fileSizeStr = url.searchParams.get("filesize");
36
+ const fileHash = url.searchParams.get("filehash");
37
+ const purpose = url.searchParams.get("purpose");
38
+ if (!filename || !fileSizeStr || !fileHash || !purpose) {
39
+ return reply.code(400).send({
40
+ error: "Missing required query params: filename, filesize, filehash, purpose",
41
+ });
42
+ }
43
+ if (purpose !== "mail" && purpose !== "context") {
44
+ return reply.code(400).send({
45
+ error: 'Invalid purpose. Must be "mail" or "context"',
46
+ });
47
+ }
48
+ const fileSize = parseInt(fileSizeStr, 10);
49
+ if (isNaN(fileSize) || fileSize <= 0) {
50
+ return reply.code(400).send({ error: "Invalid filesize" });
51
+ }
52
+ if (fileSize > MAX_FILE_SIZE) {
53
+ return reply.code(413).send({
54
+ error: `File too large. Max size: ${MAX_FILE_SIZE} bytes`,
55
+ });
56
+ }
57
+ const userId = await resolveUserByApiKey(apiKey);
58
+ if (userId == null) {
59
+ return reply.code(401).send({ error: "Invalid API key" });
60
+ }
61
+ // Stream to temp file, then move to content-addressable path
62
+ const tmpDir = join(naisysFolder, "tmp", "hub", "attachments");
63
+ mkdirSync(tmpDir, { recursive: true });
64
+ const tmpPath = join(tmpDir, `${Date.now()}_${userId}_${Math.random().toString(36).slice(2)}`);
65
+ const hash = createHash("sha256");
66
+ let bytesWritten = 0;
67
+ const fileStream = createWriteStream(tmpPath);
68
+ // Stream the raw request body (not consumed by Fastify thanks to our parser)
69
+ const req = request.raw;
70
+ const success = await new Promise((resolve) => {
71
+ const sizeChecker = new Writable({
72
+ write(chunk, _encoding, callback) {
73
+ bytesWritten += chunk.length;
74
+ if (bytesWritten > MAX_FILE_SIZE) {
75
+ callback(new Error("File exceeds size limit"));
76
+ return;
77
+ }
78
+ hash.update(chunk);
79
+ fileStream.write(chunk, callback);
80
+ },
81
+ final(callback) {
82
+ fileStream.end(callback);
83
+ },
84
+ });
85
+ pipeline(req, sizeChecker, (err) => {
86
+ if (err) {
87
+ fileStream.destroy();
88
+ try {
89
+ unlinkSync(tmpPath);
90
+ }
91
+ catch {
92
+ /* ignore */
93
+ }
94
+ resolve(false);
95
+ }
96
+ else {
97
+ resolve(true);
98
+ }
99
+ });
100
+ });
101
+ if (!success) {
102
+ return reply
103
+ .code(413)
104
+ .send({ error: "File exceeds size limit during upload" });
105
+ }
106
+ // Verify hash
107
+ const computedHash = hash.digest("hex");
108
+ if (computedHash !== fileHash) {
109
+ try {
110
+ unlinkSync(tmpPath);
111
+ }
112
+ catch {
113
+ /* ignore */
102
114
  }
103
- hash.update(chunk);
104
- fileStream.write(chunk, callback);
105
- },
106
- final(callback) {
107
- fileStream.end(callback);
108
- },
109
- });
110
- pipeline(req, sizeChecker, (err) => {
111
- if (err) {
112
- fileStream.destroy();
115
+ return reply.code(400).send({
116
+ error: `Hash mismatch. Expected: ${fileHash}, got: ${computedHash}`,
117
+ });
118
+ }
119
+ // Move to content-addressable path: attachments/hub/<first2>/<next2>/<fullhash>
120
+ const storageDir = join(naisysFolder, "attachments", "hub", computedHash.slice(0, 2), computedHash.slice(2, 4));
121
+ mkdirSync(storageDir, { recursive: true });
122
+ const storagePath = join(storageDir, computedHash);
123
+ if (existsSync(storagePath)) {
124
+ // Dedup: identical file already on disk, discard temp
113
125
  try {
114
126
  unlinkSync(tmpPath);
115
127
  }
116
128
  catch {
117
129
  /* ignore */
118
130
  }
119
- resolve(false);
120
131
  }
121
132
  else {
122
- resolve(true);
133
+ renameSync(tmpPath, storagePath);
123
134
  }
124
- });
125
- });
126
- if (!success) {
127
- res.writeHead(413, { "Content-Type": "application/json" });
128
- res.end(JSON.stringify({ error: "File exceeds size limit during upload" }));
129
- return;
130
- }
131
- // Verify hash
132
- const computedHash = hash.digest("hex");
133
- if (computedHash !== fileHash) {
134
- try {
135
- unlinkSync(tmpPath);
136
- }
137
- catch {
138
- /* ignore */
139
- }
140
- res.writeHead(400, { "Content-Type": "application/json" });
141
- res.end(JSON.stringify({
142
- error: `Hash mismatch. Expected: ${fileHash}, got: ${computedHash}`,
143
- }));
144
- return;
145
- }
146
- // Move to content-addressable path: attachments/hub/<first2>/<next2>/<fullhash>
147
- const storageDir = join(naisysFolder, "attachments", "hub", computedHash.slice(0, 2), computedHash.slice(2, 4));
148
- mkdirSync(storageDir, { recursive: true });
149
- const storagePath = join(storageDir, computedHash);
150
- if (existsSync(storagePath)) {
151
- // Dedup: identical file already on disk, discard temp
152
- try {
153
- unlinkSync(tmpPath);
135
+ // Create DB record
136
+ const record = await hubDb.attachments.create({
137
+ data: {
138
+ public_id: randomBytes(8).toString("base64url").slice(0, 10),
139
+ filepath: storagePath,
140
+ filename,
141
+ file_size: bytesWritten,
142
+ file_hash: computedHash,
143
+ purpose: purpose,
144
+ uploaded_by: userId,
145
+ },
146
+ });
147
+ const attachmentId = record.id;
148
+ logService.log(`[Hub:Attachment] Uploaded attachment ${attachmentId}: ${filename} (${bytesWritten} bytes) by user ${userId}`);
149
+ return reply.send({ id: attachmentId });
154
150
  }
155
- catch {
156
- /* ignore */
151
+ catch (err) {
152
+ logService.error(`[Hub:Attachment] Upload error: ${err}`);
153
+ return reply.code(500).send({ error: "Internal server error" });
157
154
  }
158
- }
159
- else {
160
- renameSync(tmpPath, storagePath);
161
- }
162
- // Create DB record
163
- const record = await hubDb.attachments.create({
164
- data: {
165
- public_id: randomBytes(8).toString("base64url").slice(0, 10),
166
- filepath: storagePath,
167
- filename,
168
- file_size: bytesWritten,
169
- file_hash: computedHash,
170
- purpose: purpose,
171
- uploaded_by: userId,
172
- },
173
155
  });
174
- const attachmentId = record.id;
175
- logService.log(`[Hub:Attachment] Uploaded attachment ${attachmentId}: ${filename} (${bytesWritten} bytes) by user ${userId}`);
176
- res.writeHead(200, { "Content-Type": "application/json" });
177
- res.end(JSON.stringify({ id: attachmentId }));
178
- }
179
- async function handleDownload(pathname, req, res) {
180
- const apiKey = extractBearerToken(req.headers.authorization);
156
+ done();
157
+ });
158
+ // Download routes
159
+ async function handleDownload(publicId, request, reply) {
160
+ const apiKey = extractBearerToken(request.headers.authorization);
181
161
  if (!apiKey) {
182
- res.writeHead(401, { "Content-Type": "application/json" });
183
- res.end(JSON.stringify({ error: "Missing Authorization header" }));
184
- return;
162
+ return reply.code(401).send({ error: "Missing Authorization header" });
185
163
  }
186
164
  const userId = await resolveUserByApiKey(apiKey);
187
165
  if (userId == null) {
188
- res.writeHead(401, { "Content-Type": "application/json" });
189
- res.end(JSON.stringify({ error: "Invalid API key" }));
190
- return;
166
+ return reply.code(401).send({ error: "Invalid API key" });
191
167
  }
192
- // Parse public ID from /hub/attachments/<publicId> or /hub/attachments/<publicId>/<filename>
193
- const segments = pathname.slice("/hub/attachments/".length).split("/");
194
- const publicId = segments[0];
195
168
  if (!publicId) {
196
- res.writeHead(400, { "Content-Type": "application/json" });
197
- res.end(JSON.stringify({ error: "Missing attachment ID" }));
198
- return;
169
+ return reply.code(400).send({ error: "Missing attachment ID" });
199
170
  }
200
171
  const attachment = await hubDb.attachments.findUnique({
201
172
  where: { public_id: publicId },
202
173
  });
203
174
  if (!attachment) {
204
- res.writeHead(404, { "Content-Type": "application/json" });
205
- res.end(JSON.stringify({ error: "Attachment not found" }));
206
- return;
175
+ return reply.code(404).send({ error: "Attachment not found" });
207
176
  }
208
177
  if (!existsSync(attachment.filepath)) {
209
- res.writeHead(404, { "Content-Type": "application/json" });
210
- res.end(JSON.stringify({ error: "Attachment file missing from disk" }));
211
- return;
178
+ return reply
179
+ .code(404)
180
+ .send({ error: "Attachment file missing from disk" });
212
181
  }
213
182
  const stat = statSync(attachment.filepath);
214
183
  const contentType = mimeFromFilename(attachment.filename);
215
184
  const disposition = contentType.startsWith("image/")
216
185
  ? "inline"
217
186
  : "attachment";
218
- res.writeHead(200, {
219
- "Content-Type": contentType,
220
- "Content-Disposition": `${disposition}; filename="${attachment.filename.replace(/"/g, '\\"')}"`,
221
- "Content-Length": stat.size,
222
- });
187
+ reply
188
+ .header("Content-Type", contentType)
189
+ .header("Content-Disposition", `${disposition}; filename="${attachment.filename.replace(/"/g, '\\"')}"`)
190
+ .header("Content-Length", stat.size);
223
191
  const readStream = createReadStream(attachment.filepath);
224
- readStream.pipe(res);
192
+ return reply.send(readStream);
225
193
  }
194
+ fastify.get("/hub/attachments/:publicId/:filename", async (request, reply) => {
195
+ try {
196
+ return await handleDownload(request.params.publicId, request, reply);
197
+ }
198
+ catch (err) {
199
+ logService.error(`[Hub:Attachment] Download error: ${err}`);
200
+ return reply.code(500).send({ error: "Internal server error" });
201
+ }
202
+ });
203
+ fastify.get("/hub/attachments/:publicId", async (request, reply) => {
204
+ try {
205
+ return await handleDownload(request.params.publicId, request, reply);
206
+ }
207
+ catch (err) {
208
+ logService.error(`[Hub:Attachment] Download error: ${err}`);
209
+ return reply.code(500).send({ error: "Internal server error" });
210
+ }
211
+ });
226
212
  }
227
213
  //# sourceMappingURL=hubAttachmentService.js.map
package/dist/naisysHub.js CHANGED
@@ -1,8 +1,8 @@
1
- import { ensureDotEnv, expandNaisysFolder } from "@naisys/common-node";
1
+ import { createDualLogger, ensureDotEnv, expandNaisysFolder, runSetupWizard, } from "@naisys/common-node";
2
2
  import { createHubDatabaseService } from "@naisys/hub-database";
3
3
  import { program } from "commander";
4
4
  import dotenv from "dotenv";
5
- import http from "http";
5
+ import Fastify from "fastify";
6
6
  import { Server } from "socket.io";
7
7
  import { fileURLToPath } from "url";
8
8
  import { createHubAccessKeyService } from "./handlers/hubAccessKeyService.js";
@@ -21,7 +21,6 @@ import { createHubUserService } from "./handlers/hubUserService.js";
21
21
  import { loadOrCreateAccessKey } from "./services/accessKeyService.js";
22
22
  import { seedAgentConfigs } from "./services/agentRegistrar.js";
23
23
  import { createHostRegistrar } from "./services/hostRegistrar.js";
24
- import { createHubServerLog } from "./services/hubServerLog.js";
25
24
  import { createNaisysServer } from "./services/naisysServer.js";
26
25
  /**
27
26
  * Starts the Hub server with sync service.
@@ -30,9 +29,9 @@ import { createNaisysServer } from "./services/naisysServer.js";
30
29
  export const startHub = async (startupType, startSupervisor, plugins, startupAgentPath) => {
31
30
  try {
32
31
  // Create log service first
33
- const logService = createHubServerLog(startupType);
32
+ const logService = createDualLogger("hub-server.log");
34
33
  logService.log(`[Hub] Starting Hub server in ${startupType} mode...`);
35
- const hubPort = Number(process.env.HUB_PORT) || 3101;
34
+ const serverPort = Number(process.env.SERVER_PORT) || 3101;
36
35
  // Load or generate hub access key for client authentication
37
36
  const hubAccessKey = loadOrCreateAccessKey();
38
37
  const naisysFolder = process.env.NAISYS_FOLDER || "";
@@ -43,11 +42,12 @@ export const startHub = async (startupType, startSupervisor, plugins, startupAge
43
42
  await seedAgentConfigs(hubDatabaseService, logService, startupAgentPath);
44
43
  // Create host registrar for tracking NAISYS instance connections
45
44
  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, {
45
+ // Create Fastify instance (TLS is handled by the reverse proxy)
46
+ const fastify = Fastify();
47
+ // Register HTTP attachment upload/download routes
48
+ createHubAttachmentService(fastify, hubDatabaseService, logService);
49
+ // Attach Socket.IO to the underlying HTTP server
50
+ const io = new Server(fastify.server, {
51
51
  path: "/hub/socket.io",
52
52
  cors: {
53
53
  origin: "*", // In production, restrict this
@@ -79,30 +79,28 @@ export const startHub = async (startupType, startSupervisor, plugins, startupAge
79
79
  const agentService = createHubAgentService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, hostRegistrar);
80
80
  // Register hub mail service for mail events from NAISYS instances
81
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
82
  /**
94
83
  * There should be no dependency between supervisor and hub
95
84
  * Sharing the same process space is to save 150 mb of node.js runtime memory on small servers
96
85
  */
97
- let supervisorPort;
98
86
  if (startSupervisor) {
99
87
  // Don't import the whole fastify web server module tree unless needed
100
88
  // Use variable to avoid compile-time type dependency on @naisys/supervisor (allows parallel builds)
101
89
  const supervisorModule = "@naisys/supervisor";
102
- const { startServer } = (await import(supervisorModule));
103
- supervisorPort = await startServer("hosted", plugins, hubPort);
90
+ const { supervisorPlugin } = (await import(supervisorModule));
91
+ await fastify.register(supervisorPlugin, {
92
+ plugins,
93
+ serverPort,
94
+ hosted: true,
95
+ });
104
96
  }
105
- return { hubPort, supervisorPort };
97
+ // Start listening
98
+ await fastify.listen({ port: serverPort, host: "0.0.0.0" });
99
+ logService.log(`[Hub] Running on http://localhost:${serverPort}/hub, logs written to file`);
100
+ if (startupType === "hosted") {
101
+ logService.disableConsole();
102
+ }
103
+ return { serverPort };
106
104
  }
107
105
  catch (err) {
108
106
  console.error("[Hub] Failed to start hub server:", err);
@@ -112,16 +110,36 @@ export const startHub = async (startupType, startSupervisor, plugins, startupAge
112
110
  // Start server if this file is run directly
113
111
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
114
112
  dotenv.config({ quiet: true });
115
- await ensureDotEnv(new URL("../.env.example", import.meta.url));
113
+ const hubWizardConfig = {
114
+ title: "NAISYS Hub Setup",
115
+ sections: [
116
+ {
117
+ type: "fields",
118
+ comment: "Hub server configuration",
119
+ fields: [
120
+ { key: "NAISYS_FOLDER", label: "NAISYS Data Folder" },
121
+ { key: "SERVER_PORT", label: "Server Port" },
122
+ ],
123
+ },
124
+ ],
125
+ };
126
+ const hubExampleUrl = new URL("../.env.example", import.meta.url);
127
+ if (process.argv.includes("--setup")) {
128
+ const { default: path } = await import("path");
129
+ await runSetupWizard(path.resolve(".env"), hubExampleUrl, hubWizardConfig);
130
+ process.exit(0);
131
+ }
132
+ await ensureDotEnv(hubExampleUrl, hubWizardConfig);
116
133
  expandNaisysFolder();
117
134
  program
118
135
  .argument("[agent-path]", "Path to agent configuration file to seed the database (optional)")
119
136
  .option("--supervisor", "Start Supervisor web server")
120
137
  .option("--erp", "Start ERP web app (requires --supervisor)")
138
+ .option("--setup", "Run interactive setup wizard")
121
139
  .parse();
122
140
  const plugins = [];
123
141
  if (program.opts().erp)
124
142
  plugins.push("erp");
125
- void startHub("standalone", program.opts().supervisor, plugins, program.args[0]);
143
+ void startHub("standalone", program.opts().supervisor, plugins, program.args[0] || ".");
126
144
  }
127
145
  //# sourceMappingURL=naisysHub.js.map
@@ -4,13 +4,20 @@ export async function createHostRegistrar({ hubDb }) {
4
4
  const hostsById = new Map();
5
5
  // Seed the cache from the database
6
6
  const rows = await hubDb.hosts.findMany({
7
- select: { id: true, name: true, restricted: true, host_type: true },
7
+ select: {
8
+ id: true,
9
+ name: true,
10
+ restricted: true,
11
+ host_type: true,
12
+ last_version: true,
13
+ },
8
14
  });
9
15
  for (const row of rows) {
10
16
  hostsById.set(row.id, {
11
17
  hostName: row.name,
12
18
  restricted: row.restricted,
13
19
  hostType: row.host_type,
20
+ lastVersion: row.last_version ?? "",
14
21
  });
15
22
  }
16
23
  /**
@@ -18,7 +25,7 @@ export async function createHostRegistrar({ hubDb }) {
18
25
  * updates last_active on every call.
19
26
  * @returns The host's autoincrement id
20
27
  */
21
- async function registerHost(hostName, hostType, lastIp) {
28
+ async function registerHost(hostName, hostType, lastIp, clientVersion) {
22
29
  hostName = toUrlSafeKey(hostName);
23
30
  const existing = await hubDb.hosts.findUnique({
24
31
  where: { name: hostName },
@@ -30,12 +37,14 @@ export async function createHostRegistrar({ hubDb }) {
30
37
  last_active: new Date().toISOString(),
31
38
  host_type: hostType,
32
39
  last_ip: lastIp,
40
+ last_version: clientVersion,
33
41
  },
34
42
  });
35
43
  hostsById.set(existing.id, {
36
44
  hostName,
37
45
  restricted: existing.restricted,
38
46
  hostType,
47
+ lastVersion: clientVersion,
39
48
  });
40
49
  return existing.id;
41
50
  }
@@ -44,10 +53,16 @@ export async function createHostRegistrar({ hubDb }) {
44
53
  name: hostName,
45
54
  host_type: hostType,
46
55
  last_ip: lastIp,
56
+ last_version: clientVersion,
47
57
  last_active: new Date().toISOString(),
48
58
  },
49
59
  });
50
- hostsById.set(created.id, { hostName, restricted: false, hostType });
60
+ hostsById.set(created.id, {
61
+ hostName,
62
+ restricted: false,
63
+ hostType,
64
+ lastVersion: clientVersion,
65
+ });
51
66
  return created.id;
52
67
  }
53
68
  /** Returns all known hosts (from DB + any newly registered) */
@@ -57,12 +72,19 @@ export async function createHostRegistrar({ hubDb }) {
57
72
  hostName: entry.hostName,
58
73
  restricted: entry.restricted,
59
74
  hostType: entry.hostType,
75
+ lastVersion: entry.lastVersion,
60
76
  }));
61
77
  }
62
78
  /** Re-read all hosts from DB and replace the in-memory cache */
63
79
  async function refreshHosts() {
64
80
  const rows = await hubDb.hosts.findMany({
65
- select: { id: true, name: true, restricted: true, host_type: true },
81
+ select: {
82
+ id: true,
83
+ name: true,
84
+ restricted: true,
85
+ host_type: true,
86
+ last_version: true,
87
+ },
66
88
  });
67
89
  hostsById.clear();
68
90
  for (const row of rows) {
@@ -70,6 +92,7 @@ export async function createHostRegistrar({ hubDb }) {
70
92
  hostName: row.name,
71
93
  restricted: row.restricted,
72
94
  hostType: row.host_type,
95
+ lastVersion: row.last_version ?? "",
73
96
  });
74
97
  }
75
98
  }
@@ -70,7 +70,8 @@ export function createNaisysServer(nsp, initialHubAccessKey, logService, hostReg
70
70
  }
71
71
  try {
72
72
  const hostType = (typeof rawHostType === "string" ? rawHostType : "naisys");
73
- const hostId = await hostRegistrar.registerHost(hostName, hostType, socket.handshake.address);
73
+ const resolvedVersion = typeof clientVersion === "string" ? clientVersion : "";
74
+ const hostId = await hostRegistrar.registerHost(hostName, hostType, socket.handshake.address, resolvedVersion);
74
75
  // Reject duplicate naisys connections (supervisors may have multiple)
75
76
  if (hostType === "naisys" && naisysConnections.has(hostId)) {
76
77
  logService.log(`[Hub] Connection rejected: host '${hostName}' is already connected`);
@@ -79,8 +80,7 @@ export function createNaisysServer(nsp, initialHubAccessKey, logService, hostReg
79
80
  socket.data.hostId = hostId;
80
81
  socket.data.hostName = hostName;
81
82
  socket.data.hostType = hostType;
82
- socket.data.clientVersion =
83
- typeof clientVersion === "string" ? clientVersion : "";
83
+ socket.data.clientVersion = resolvedVersion;
84
84
  next();
85
85
  }
86
86
  catch (err) {
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
- import { getGitCommitHash } from "@naisys/common-node";
2
1
  import { readFileSync } from "node:fs";
3
2
  import { dirname, join } from "node:path";
4
3
  import { fileURLToPath } from "node:url";
4
+ import { getGitCommitHash } from "@naisys/common-node";
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
  // Read the hub's own package.json (one level up from dist/)
7
7
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naisys/hub",
3
- "version": "3.0.0-beta.18",
3
+ "version": "3.0.0-beta.19",
4
4
  "description": "NAISYS Hub - Adds persistence and multi-instance coordination to NAISYS",
5
5
  "type": "module",
6
6
  "main": "dist/naisysHub.js",
@@ -30,7 +30,7 @@
30
30
  "!dist/**/*.d.ts.map"
31
31
  ],
32
32
  "peerDependencies": {
33
- "@naisys/supervisor": "3.0.0-beta.18"
33
+ "@naisys/supervisor": "3.0.0-beta.19"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@naisys/supervisor": {
@@ -38,13 +38,13 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@naisys/common": "3.0.0-beta.18",
42
- "@naisys/common-node": "3.0.0-beta.18",
43
- "@naisys/hub-database": "3.0.0-beta.18",
44
- "@naisys/hub-protocol": "3.0.0-beta.18",
41
+ "@naisys/common": "3.0.0-beta.19",
42
+ "@naisys/common-node": "3.0.0-beta.19",
43
+ "@naisys/hub-database": "3.0.0-beta.19",
44
+ "@naisys/hub-protocol": "3.0.0-beta.19",
45
45
  "commander": "^14.0.3",
46
46
  "dotenv": "^17.3.1",
47
- "pino": "^10.3.1",
47
+ "fastify": "^5.8.2",
48
48
  "socket.io": "^4.8.3"
49
49
  },
50
50
  "devDependencies": {
@@ -1,47 +0,0 @@
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