@naisys/hub 3.0.0-beta.17 → 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 +2 -2
- package/dist/handlers/hubAttachmentService.js +162 -176
- package/dist/naisysHub.js +45 -27
- package/dist/services/hostRegistrar.js +27 -4
- package/dist/services/naisysServer.js +3 -3
- package/dist/version.js +1 -1
- package/package.json +7 -7
- package/dist/services/hubServerLog.js +0 -47
package/.env.example
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
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
|
-
|
|
133
|
+
renameSync(tmpPath, storagePath);
|
|
123
134
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
219
|
-
"Content-Type"
|
|
220
|
-
"Content-Disposition"
|
|
221
|
-
"Content-Length"
|
|
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
|
-
|
|
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
|
|
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 =
|
|
32
|
+
const logService = createDualLogger("hub-server.log");
|
|
34
33
|
logService.log(`[Hub] Starting Hub server in ${startupType} mode...`);
|
|
35
|
-
const
|
|
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
|
|
47
|
-
const
|
|
48
|
-
// Register HTTP attachment upload/download
|
|
49
|
-
createHubAttachmentService(
|
|
50
|
-
|
|
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 {
|
|
103
|
-
|
|
90
|
+
const { supervisorPlugin } = (await import(supervisorModule));
|
|
91
|
+
await fastify.register(supervisorPlugin, {
|
|
92
|
+
plugins,
|
|
93
|
+
serverPort,
|
|
94
|
+
hosted: true,
|
|
95
|
+
});
|
|
104
96
|
}
|
|
105
|
-
|
|
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
|
-
|
|
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: {
|
|
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, {
|
|
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: {
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
43
|
-
"@naisys/hub-database": "3.0.0-beta.
|
|
44
|
-
"@naisys/hub-protocol": "3.0.0-beta.
|
|
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
|
-
"
|
|
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
|