@naisys/hub 3.0.0-beta.44 → 3.0.0-beta.46

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.
@@ -85,6 +85,29 @@ export function createHubAgentService(naisysServer, { hubDb }, logService, heart
85
85
  const { bestHostId, payload, onResponse } = args;
86
86
  const startUserId = payload.startUserId;
87
87
  const runtimeApiKey = await issueRuntimeApiKey(startUserId);
88
+ // Fetch before run_session.create — a query failure here would otherwise
89
+ // leak the placeholder. Hashes ship so the host can skip already-on-disk files.
90
+ const startupAttachmentRows = await hubDb.user_startup_attachments.findMany({
91
+ where: { user_id: startUserId },
92
+ orderBy: { path: "asc" },
93
+ include: {
94
+ attachment: {
95
+ select: {
96
+ public_id: true,
97
+ filename: true,
98
+ file_size: true,
99
+ file_hash: true,
100
+ },
101
+ },
102
+ },
103
+ });
104
+ const startupAttachments = startupAttachmentRows.map((r) => ({
105
+ publicId: r.attachment.public_id,
106
+ filename: r.attachment.filename,
107
+ fileSize: r.attachment.file_size,
108
+ fileHash: r.attachment.file_hash,
109
+ path: r.path,
110
+ }));
88
111
  const lastRun = await hubDb.run_session.findFirst({
89
112
  select: { run_id: true },
90
113
  orderBy: { run_id: "desc" },
@@ -116,7 +139,13 @@ export function createHubAgentService(naisysServer, { hubDb }, logService, heart
116
139
  logService.error(`[Hub:Agents] Failed to roll back run_session row for run ${runId} (${reason}): ${err}`);
117
140
  }
118
141
  }
119
- const sent = naisysServer.sendMessage(bestHostId, HubEvents.AGENT_START, { ...payload, runtimeApiKey, runId, sessionId }, async (response) => {
142
+ const sent = naisysServer.sendMessage(bestHostId, HubEvents.AGENT_START, {
143
+ ...payload,
144
+ runtimeApiKey,
145
+ runId,
146
+ sessionId,
147
+ ...(startupAttachments.length > 0 ? { startupAttachments } : {}),
148
+ }, async (response) => {
120
149
  if (response.success && response.modelName) {
121
150
  const modelName = response.modelName;
122
151
  try {
@@ -24,10 +24,25 @@ export function createHubHeartbeatService(naisysServer, { hubDb }, logService, r
24
24
  const activeUserIds = [
25
25
  ...new Set(parsed.activeSessions.map((s) => s.userId)),
26
26
  ];
27
- // Update in-memory per-host active agent IDs
27
+ // Memory before DB: supervisor online badges read this map live, so a
28
+ // slow SQLite write shouldn't make heartbeats look stale.
28
29
  hostActiveAgents.set(hostId, activeUserIds);
30
+ const now = new Date().toISOString();
31
+ const sessionMap = new Map();
32
+ for (const session of parsed.activeSessions) {
33
+ const subagentId = session.subagentId ?? 0;
34
+ sessionMap.set(sessionKey(session.userId, subagentId), {
35
+ userId: session.userId,
36
+ subagentId: subagentId === 0 ? null : subagentId,
37
+ runId: session.runId,
38
+ sessionId: session.sessionId,
39
+ lastActive: now,
40
+ paused: session.paused,
41
+ state: session.state,
42
+ });
43
+ }
44
+ hostActiveSessions.set(hostId, sessionMap);
29
45
  try {
30
- const now = new Date().toISOString();
31
46
  // Update host last_active
32
47
  await hubDb.hosts.updateMany({
33
48
  where: { id: hostId },
@@ -43,19 +58,6 @@ export function createHubHeartbeatService(naisysServer, { hubDb }, logService, r
43
58
  // Bump run_session.last_active for each active session so the run-online
44
59
  // badge stays lit even during quiet periods with no log writes. The
45
60
  // aggregate SESSION_HEARTBEAT broadcast runs on its own interval below.
46
- const sessionMap = new Map();
47
- for (const session of parsed.activeSessions) {
48
- const subagentId = session.subagentId ?? 0;
49
- sessionMap.set(sessionKey(session.userId, subagentId), {
50
- userId: session.userId,
51
- subagentId: subagentId === 0 ? null : subagentId,
52
- runId: session.runId,
53
- sessionId: session.sessionId,
54
- lastActive: now,
55
- paused: session.paused,
56
- state: session.state,
57
- });
58
- }
59
61
  if (parsed.activeSessions.length > 0) {
60
62
  await hubDb.run_session.updateMany({
61
63
  where: {
@@ -69,7 +71,6 @@ export function createHubHeartbeatService(naisysServer, { hubDb }, logService, r
69
71
  data: { last_active: now },
70
72
  });
71
73
  }
72
- hostActiveSessions.set(hostId, sessionMap);
73
74
  // Self-heal: register each plaintext with the redactor (idempotent),
74
75
  // then mint + push a fresh key if the hash is missing or mismatched.
75
76
  // Old plaintexts accumulate per user so leaks during the rotate
@@ -0,0 +1,100 @@
1
+ import { HUB_HEARTBEAT_INTERVAL_MS, HubEvents } from "@naisys/hub-protocol";
2
+ import { afterEach, describe, expect, test, vi } from "vitest";
3
+ import { createHubHeartbeatService } from "./hubHeartbeatService.js";
4
+ function createDeferred() {
5
+ let resolve;
6
+ const promise = new Promise((r) => {
7
+ resolve = r;
8
+ });
9
+ return { promise, resolve };
10
+ }
11
+ function createServerHarness() {
12
+ const handlers = new Map();
13
+ const server = {
14
+ registerEvent: vi.fn((event, handler) => {
15
+ handlers.set(event, handler);
16
+ }),
17
+ broadcastToAll: vi.fn(),
18
+ broadcastToSupervisors: vi.fn(),
19
+ sendMessage: vi.fn(() => true),
20
+ };
21
+ return { server, handlers };
22
+ }
23
+ function createLogger() {
24
+ return {
25
+ log: vi.fn(),
26
+ error: vi.fn(),
27
+ disableConsole: vi.fn(),
28
+ };
29
+ }
30
+ describe("hubHeartbeatService", () => {
31
+ afterEach(() => {
32
+ vi.useRealTimers();
33
+ vi.restoreAllMocks();
34
+ });
35
+ test("publishes session heartbeats from memory before heartbeat DB writes finish", async () => {
36
+ vi.useFakeTimers();
37
+ vi.setSystemTime(new Date("2026-05-09T12:00:00Z"));
38
+ const { server, handlers } = createServerHarness();
39
+ const hostUpdate = createDeferred();
40
+ const hubDb = {
41
+ hosts: {
42
+ updateMany: vi.fn(() => hostUpdate.promise),
43
+ },
44
+ user_notifications: {
45
+ updateMany: vi.fn(() => Promise.resolve({ count: 1 })),
46
+ },
47
+ run_session: {
48
+ updateMany: vi.fn(() => Promise.resolve({ count: 1 })),
49
+ },
50
+ users: {
51
+ findMany: vi.fn(() => Promise.resolve([])),
52
+ },
53
+ };
54
+ const redactionService = {
55
+ registerRuntimeApiKey: vi.fn(),
56
+ };
57
+ const runtimeKeyService = {
58
+ issueRuntimeApiKey: vi.fn(),
59
+ };
60
+ const service = createHubHeartbeatService(server, { hubDb }, createLogger(), redactionService, runtimeKeyService);
61
+ try {
62
+ const heartbeatHandler = handlers.get(HubEvents.HEARTBEAT);
63
+ if (!heartbeatHandler) {
64
+ throw new Error("HEARTBEAT handler was not registered");
65
+ }
66
+ const handlerPromise = Promise.resolve(heartbeatHandler(42, {
67
+ activeSessions: [
68
+ {
69
+ userId: 1,
70
+ runId: 7,
71
+ sessionId: 2,
72
+ paused: true,
73
+ state: "Waiting",
74
+ },
75
+ ],
76
+ }));
77
+ expect(hubDb.hosts.updateMany).toHaveBeenCalled();
78
+ await vi.advanceTimersByTimeAsync(HUB_HEARTBEAT_INTERVAL_MS);
79
+ expect(server.broadcastToSupervisors).toHaveBeenCalledWith(HubEvents.SESSION_HEARTBEAT, {
80
+ updates: [
81
+ {
82
+ userId: 1,
83
+ runId: 7,
84
+ subagentId: undefined,
85
+ sessionId: 2,
86
+ lastActive: "2026-05-09T12:00:00.000Z",
87
+ paused: true,
88
+ state: "Waiting",
89
+ },
90
+ ],
91
+ });
92
+ hostUpdate.resolve({ count: 1 });
93
+ await handlerPromise;
94
+ }
95
+ finally {
96
+ service.cleanup();
97
+ }
98
+ });
99
+ });
100
+ //# sourceMappingURL=hubHeartbeatService.test.js.map
@@ -52,6 +52,7 @@ export function createHubMailService(naisysServer, { hubDb }, logService, heartb
52
52
  const parsed = MailSendRequestSchema.parse(data);
53
53
  await sendMailService.sendMail({
54
54
  fromUserId: parsed.fromUserId,
55
+ fromRunId: parsed.fromRunId,
55
56
  recipientUserIds: parsed.toUserIds,
56
57
  subject: parsed.subject,
57
58
  body: parsed.body,
@@ -226,7 +227,7 @@ export function createHubMailService(naisysServer, { hubDb }, logService, heartb
226
227
  user_id: parsed.userId,
227
228
  read_at: null,
228
229
  },
229
- data: { read_at: new Date() },
230
+ data: { read_at: new Date(), read_run_id: parsed.runId ?? null },
230
231
  });
231
232
  ack({ success: true });
232
233
  // Push read receipts to supervisor connections
@@ -2,7 +2,9 @@ import { HubEvents } from "@naisys/hub-protocol";
2
2
  const MIN_SECRET_LENGTH = 6;
3
3
  const PATTERN_REPLACEMENTS = [
4
4
  {
5
- pattern: /Authorization:\s*(Bearer|Basic)\s+\S+/gi,
5
+ // Stop at whitespace or quote chars so the closing quote survives, and
6
+ // skip unexpanded shell-var refs ($FOO, ${FOO}) which aren't real secrets.
7
+ pattern: /Authorization:\s*(Bearer|Basic)\s+(?!\$)[^\s"']+/gi,
6
8
  replacement: "Authorization: $1 [REDACTED]",
7
9
  },
8
10
  {
@@ -91,6 +91,9 @@ describe("hubRedactionService", () => {
91
91
  const { hubDb } = createHubDb([]);
92
92
  const svc = await createHubRedactionService(server, { hubDb }, createLogger());
93
93
  expect(svc.redact("Authorization: Bearer xyz.token.here")).toBe("Authorization: Bearer [REDACTED]");
94
+ expect(svc.redact('curl -H "Authorization: Bearer xyz.token.here" url')).toBe('curl -H "Authorization: Bearer [REDACTED]" url');
95
+ expect(svc.redact('curl -H "Authorization: Bearer $NAISYS_API_KEY" url')).toBe('curl -H "Authorization: Bearer $NAISYS_API_KEY" url');
96
+ expect(svc.redact('curl -H "Authorization: Bearer ${NAISYS_API_KEY}" url')).toBe('curl -H "Authorization: Bearer ${NAISYS_API_KEY}" url');
94
97
  expect(svc.redact("AKIAIOSFODNN7EXAMPLE")).toBe("[REDACTED:AWS_KEY]");
95
98
  expect(svc.redact("header eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c trail")).toBe("header [REDACTED:JWT] trail");
96
99
  });
@@ -68,6 +68,7 @@ export function createHubSendMailService(naisysServer, { hubDb }, heartbeatServi
68
68
  user_id: params.fromUserId,
69
69
  type: "from",
70
70
  read_at: now,
71
+ read_run_id: params.fromRunId ?? null,
71
72
  created_at: now,
72
73
  },
73
74
  });
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@naisys/hub",
3
- "version": "3.0.0-beta.44",
3
+ "version": "3.0.0-beta.46",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@naisys/hub",
9
- "version": "3.0.0-beta.44",
9
+ "version": "3.0.0-beta.46",
10
10
  "dependencies": {
11
- "@naisys/common": "3.0.0-beta.44",
12
- "@naisys/common-node": "3.0.0-beta.44",
13
- "@naisys/hub-database": "3.0.0-beta.44",
14
- "@naisys/hub-protocol": "3.0.0-beta.44",
11
+ "@naisys/common": "3.0.0-beta.46",
12
+ "@naisys/common-node": "3.0.0-beta.46",
13
+ "@naisys/hub-database": "3.0.0-beta.46",
14
+ "@naisys/hub-protocol": "3.0.0-beta.46",
15
15
  "commander": "^14.0.3",
16
16
  "dotenv": "^17.3.1",
17
17
  "fastify": "^5.8.2",
@@ -24,7 +24,7 @@
24
24
  "node": ">=22.0.0"
25
25
  },
26
26
  "peerDependencies": {
27
- "@naisys/supervisor": "3.0.0-beta.44"
27
+ "@naisys/supervisor": "3.0.0-beta.46"
28
28
  },
29
29
  "peerDependenciesMeta": {
30
30
  "@naisys/supervisor": {
@@ -189,32 +189,32 @@
189
189
  "license": "MIT"
190
190
  },
191
191
  "node_modules/@naisys/common": {
192
- "version": "3.0.0-beta.44",
193
- "resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.44.tgz",
194
- "integrity": "sha512-g7ZiYVZTXa+qhPdzNrc0CDjx6ks/Gl8uJ+GVJlocyeClU+eKfbsc7/NU4f8S+DUyxrwZIA082r1ZR9YATjbc4g==",
192
+ "version": "3.0.0-beta.46",
193
+ "resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.46.tgz",
194
+ "integrity": "sha512-C08UtZtQLp0/3u1cAAfWRtFssphQgrkKk63HhhE/ntCZFWUlfhgTB4xtBabwJZitp+hqjLJbC3A7yRIGv6gj5w==",
195
195
  "dependencies": {
196
196
  "semver": "^7.7.4",
197
197
  "zod": "^4.3.6"
198
198
  }
199
199
  },
200
200
  "node_modules/@naisys/common-node": {
201
- "version": "3.0.0-beta.44",
202
- "resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.44.tgz",
203
- "integrity": "sha512-8I/1Q+cqxW204x8HY24cnoMn2fNNd4d+s5VX6/0G4Fotcx08n1fRFqNENDmGfhEPOlvS9KBpVhyTqr5/Ur/GVw==",
201
+ "version": "3.0.0-beta.46",
202
+ "resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.46.tgz",
203
+ "integrity": "sha512-MV+HYrb8lLWrvCkF5Of4qvi0989Y1w9T3CkQrdm/zajkXtwN685uBUg7ty++rcvNnVeZWcDptl46Kb5PSaKjCw==",
204
204
  "dependencies": {
205
- "@naisys/common": "3.0.0-beta.44",
205
+ "@naisys/common": "3.0.0-beta.46",
206
206
  "better-sqlite3": "^12.6.2",
207
207
  "js-yaml": "^4.1.1",
208
208
  "pino": "^10.3.1"
209
209
  }
210
210
  },
211
211
  "node_modules/@naisys/hub-database": {
212
- "version": "3.0.0-beta.44",
213
- "resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.44.tgz",
214
- "integrity": "sha512-DXrWAdr9BHvv3pQUderMhG70r3FfydN9qygtm1OHSYgUa51tQ+e7mkEsfOINK4nmt+cg6MoW1s4kPeh7LovLVw==",
212
+ "version": "3.0.0-beta.46",
213
+ "resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.46.tgz",
214
+ "integrity": "sha512-o2PIeGv9Rt2M0LSKIhDA9MZLdYzhswn8uIL/AuP4CFd8zwq564p3a7g4wT34cjgOplyHLEFdaj1KWOcPbQVeTw==",
215
215
  "dependencies": {
216
- "@naisys/common": "3.0.0-beta.44",
217
- "@naisys/common-node": "3.0.0-beta.44",
216
+ "@naisys/common": "3.0.0-beta.46",
217
+ "@naisys/common-node": "3.0.0-beta.46",
218
218
  "@prisma/adapter-better-sqlite3": "^7.5.0",
219
219
  "@prisma/client": "^7.5.0",
220
220
  "better-sqlite3": "^12.6.2",
@@ -222,11 +222,11 @@
222
222
  }
223
223
  },
224
224
  "node_modules/@naisys/hub-protocol": {
225
- "version": "3.0.0-beta.44",
226
- "resolved": "https://registry.npmjs.org/@naisys/hub-protocol/-/hub-protocol-3.0.0-beta.44.tgz",
227
- "integrity": "sha512-k4DOla7oxBuGSMRLcnPimT6kaAzXPvBd8d7o4/HjeogoOMRRPbXXtqGb8YT9zXjjUKsZurOmEFW5NmFuYpkCsQ==",
225
+ "version": "3.0.0-beta.46",
226
+ "resolved": "https://registry.npmjs.org/@naisys/hub-protocol/-/hub-protocol-3.0.0-beta.46.tgz",
227
+ "integrity": "sha512-XcKK22yHxsjzY9uAN8ljpRaMHFnVzepWez0tCtkAc936XR5X+0keybKsvAirvxONBAfOYKJ72jXV5+16qqfllA==",
228
228
  "dependencies": {
229
- "@naisys/common": "3.0.0-beta.44",
229
+ "@naisys/common": "3.0.0-beta.46",
230
230
  "zod": "^4.3.6"
231
231
  }
232
232
  },
@@ -2521,9 +2521,9 @@
2521
2521
  }
2522
2522
  },
2523
2523
  "node_modules/zod": {
2524
- "version": "4.3.6",
2525
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
2526
- "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
2524
+ "version": "4.4.3",
2525
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
2526
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
2527
2527
  "license": "MIT",
2528
2528
  "funding": {
2529
2529
  "url": "https://github.com/sponsors/colinhacks"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naisys/hub",
3
- "version": "3.0.0-beta.44",
3
+ "version": "3.0.0-beta.46",
4
4
  "description": "NAISYS Hub - Adds persistence and multi-instance coordination to NAISYS",
5
5
  "type": "module",
6
6
  "main": "dist/naisysHub.js",
@@ -32,7 +32,7 @@
32
32
  "!dist/**/*.d.ts.map"
33
33
  ],
34
34
  "peerDependencies": {
35
- "@naisys/supervisor": "3.0.0-beta.44"
35
+ "@naisys/supervisor": "3.0.0-beta.46"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@naisys/supervisor": {
@@ -40,10 +40,10 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@naisys/common": "3.0.0-beta.44",
44
- "@naisys/common-node": "3.0.0-beta.44",
45
- "@naisys/hub-database": "3.0.0-beta.44",
46
- "@naisys/hub-protocol": "3.0.0-beta.44",
43
+ "@naisys/common": "3.0.0-beta.46",
44
+ "@naisys/common-node": "3.0.0-beta.46",
45
+ "@naisys/hub-database": "3.0.0-beta.46",
46
+ "@naisys/hub-protocol": "3.0.0-beta.46",
47
47
  "commander": "^14.0.3",
48
48
  "dotenv": "^17.3.1",
49
49
  "fastify": "^5.8.2",