@naisys/hub 3.0.0-beta.39 → 3.0.0-beta.41
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/README.md +3 -1
- package/dist/handlers/hubAgentService.js +98 -36
- package/dist/handlers/hubConfigService.js +7 -3
- package/dist/handlers/hubHeartbeatService.js +39 -4
- package/dist/handlers/hubLogService.js +5 -4
- package/dist/handlers/hubRedactionService.js +93 -0
- package/dist/handlers/hubRedactionService.test.js +116 -0
- package/dist/handlers/hubRuntimeKeyService.js +27 -0
- package/dist/handlers/hubSendMailService.js +9 -5
- package/dist/handlers/hubVariablePatchService.js +54 -0
- package/dist/handlers/hubVariablePatchService.test.js +85 -0
- package/dist/naisysHub.js +15 -4
- package/npm-shrinkwrap.json +23 -23
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -46,7 +46,9 @@ The hub owns mail, context logs and [attachments](../../docs/011-mail-attachment
|
|
|
46
46
|
|
|
47
47
|
- `Authorization: Bearer` header auth with a rotatable access key
|
|
48
48
|
- Hardened spawning: no shell interpretation, timeouts on `execFileSync`
|
|
49
|
-
- API keys read from headers, not query params
|
|
49
|
+
- API keys stored as SHA-256 hashes; per-user keys read from headers, not query params
|
|
50
|
+
- Dynamic runtime API keys minted per agent and re-issued on hub restart
|
|
51
|
+
- Redaction service scrubs sensitive variables and runtime keys from logs and mail before they hit the DB
|
|
50
52
|
- Hub socket served at `/hub` for reverse-proxy friendliness (TLS terminated at the proxy)
|
|
51
53
|
|
|
52
54
|
### Deployment
|
|
@@ -1,29 +1,7 @@
|
|
|
1
|
-
import { hashToken } from "@naisys/common-node";
|
|
2
1
|
import { AgentPeekRequestSchema, AgentRunCommandRequestSchema, AgentRunPauseRequestSchema, AgentStartInboundSchema, AgentStopRequestSchema, HubEvents, } from "@naisys/hub-protocol";
|
|
3
|
-
import { randomBytes } from "crypto";
|
|
4
2
|
/** Handles agent_start requests by routing them to the least-loaded eligible host */
|
|
5
|
-
export function createHubAgentService(naisysServer, { hubDb }, logService, heartbeatService, sendMailService, hostRegistrar) {
|
|
6
|
-
|
|
7
|
-
* Mint a fresh runtime API key for a user, rotating any prior key. Plaintext
|
|
8
|
-
* is returned only here and travels once over the AGENT_START message; the
|
|
9
|
-
* DB only stores the hash. Keys persist across hub restarts/crashes/updates;
|
|
10
|
-
* revocation lives on the user disable/archive/delete paths and on graceful
|
|
11
|
-
* AGENT_STOP.
|
|
12
|
-
*/
|
|
13
|
-
async function issueRuntimeApiKey(userId) {
|
|
14
|
-
const token = randomBytes(32).toString("hex");
|
|
15
|
-
await hubDb.users.update({
|
|
16
|
-
where: { id: userId },
|
|
17
|
-
data: { api_key_hash: hashToken(token) },
|
|
18
|
-
});
|
|
19
|
-
return token;
|
|
20
|
-
}
|
|
21
|
-
async function revokeRuntimeApiKey(userId) {
|
|
22
|
-
await hubDb.users.update({
|
|
23
|
-
where: { id: userId },
|
|
24
|
-
data: { api_key_hash: null },
|
|
25
|
-
});
|
|
26
|
-
}
|
|
3
|
+
export function createHubAgentService(naisysServer, { hubDb }, logService, heartbeatService, sendMailService, hostRegistrar, runtimeKeyService, configService) {
|
|
4
|
+
const { issueRuntimeApiKey, revokeRuntimeApiKey } = runtimeKeyService;
|
|
27
5
|
/** Find the least-loaded eligible host for a given user */
|
|
28
6
|
async function findBestHost(startUserId) {
|
|
29
7
|
// Look up which hosts this user is assigned to
|
|
@@ -93,22 +71,102 @@ export function createHubAgentService(naisysServer, { hubDb }, logService, heart
|
|
|
93
71
|
return { kind: "go", bestHostId };
|
|
94
72
|
}
|
|
95
73
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
74
|
+
* Mint and ship a key with AGENT_START so the agent's authenticated from
|
|
75
|
+
* spawn time, avoiding the one-RTT window heartbeat-only would create.
|
|
76
|
+
* Heartbeat-driven reissue covers later hash mismatches.
|
|
77
|
+
*
|
|
78
|
+
* The run_session row is written up front with model_name="" so the
|
|
79
|
+
* agent's command loop (which starts before the host acks) can FK-safely
|
|
80
|
+
* write logs/costs. model_name is patched in from the ack; the session
|
|
81
|
+
* is pushed to supervisors and runId/sessionId returned to the caller
|
|
82
|
+
* only on success. Any failure rolls back the placeholder.
|
|
101
83
|
*/
|
|
102
84
|
async function dispatchAgentStart(args) {
|
|
103
85
|
const { bestHostId, payload, onResponse } = args;
|
|
104
86
|
const startUserId = payload.startUserId;
|
|
105
87
|
const runtimeApiKey = await issueRuntimeApiKey(startUserId);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
88
|
+
const lastRun = await hubDb.run_session.findFirst({
|
|
89
|
+
select: { run_id: true },
|
|
90
|
+
orderBy: { run_id: "desc" },
|
|
91
|
+
});
|
|
92
|
+
const runId = lastRun ? lastRun.run_id + 1 : 1;
|
|
93
|
+
const sessionId = 1;
|
|
94
|
+
const subagentId = 0;
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
const rowWhere = {
|
|
97
|
+
user_id: startUserId,
|
|
98
|
+
run_id: runId,
|
|
99
|
+
subagent_id: subagentId,
|
|
100
|
+
session_id: sessionId,
|
|
101
|
+
};
|
|
102
|
+
await hubDb.run_session.create({
|
|
103
|
+
data: {
|
|
104
|
+
...rowWhere,
|
|
105
|
+
host_id: bestHostId,
|
|
106
|
+
model_name: "",
|
|
107
|
+
created_at: now,
|
|
108
|
+
last_active: now,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
async function rollbackPlaceholder(reason) {
|
|
112
|
+
try {
|
|
113
|
+
await hubDb.run_session.deleteMany({ where: rowWhere });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
logService.error(`[Hub:Agents] Failed to roll back run_session row for run ${runId} (${reason}): ${err}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const sent = naisysServer.sendMessage(bestHostId, HubEvents.AGENT_START, { ...payload, runtimeApiKey, runId, sessionId }, async (response) => {
|
|
120
|
+
if (response.success && response.modelName) {
|
|
121
|
+
const modelName = response.modelName;
|
|
122
|
+
try {
|
|
123
|
+
await hubDb.run_session.updateMany({
|
|
124
|
+
where: rowWhere,
|
|
125
|
+
data: { model_name: modelName },
|
|
126
|
+
});
|
|
127
|
+
heartbeatService.addStartedAgent(bestHostId, startUserId);
|
|
128
|
+
naisysServer.broadcastToSupervisors(HubEvents.SESSION_PUSH, {
|
|
129
|
+
session: {
|
|
130
|
+
userId: startUserId,
|
|
131
|
+
runId,
|
|
132
|
+
sessionId,
|
|
133
|
+
modelName,
|
|
134
|
+
createdAt: now,
|
|
135
|
+
lastActive: now,
|
|
136
|
+
latestLogId: 0,
|
|
137
|
+
totalLines: 0,
|
|
138
|
+
totalCost: 0,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
onResponse({ ...response, runId, sessionId });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
logService.error(`[Hub:Agents] Failed to finalize run_session row for run ${runId}: ${err}`);
|
|
145
|
+
await rollbackPlaceholder("update failed");
|
|
146
|
+
onResponse({
|
|
147
|
+
success: false,
|
|
148
|
+
error: `Failed to finalize run_session row: ${err}`,
|
|
149
|
+
hostname: response.hostname,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
if (response.success && !response.modelName) {
|
|
155
|
+
logService.error(`[Hub:Agents] Host ${bestHostId} acked agent_start without modelName for user ${startUserId}; treating as failure`);
|
|
156
|
+
}
|
|
157
|
+
await rollbackPlaceholder(response.success ? "missing modelName" : "host failure");
|
|
158
|
+
onResponse(response.success
|
|
159
|
+
? {
|
|
160
|
+
success: false,
|
|
161
|
+
error: "Host did not return modelName",
|
|
162
|
+
hostname: response.hostname,
|
|
163
|
+
}
|
|
164
|
+
: response);
|
|
109
165
|
}
|
|
110
|
-
onResponse(response);
|
|
111
166
|
});
|
|
167
|
+
if (!sent) {
|
|
168
|
+
await rollbackPlaceholder("send failed");
|
|
169
|
+
}
|
|
112
170
|
return { sent };
|
|
113
171
|
}
|
|
114
172
|
/** Try to start an agent on the best available host (fire-and-forget) */
|
|
@@ -177,12 +235,16 @@ export function createHubAgentService(naisysServer, { hubDb }, logService, heart
|
|
|
177
235
|
});
|
|
178
236
|
async function sendTaskMail(startUserId, requesterUserId, taskDescription) {
|
|
179
237
|
try {
|
|
238
|
+
const mailEnabled = !!configService.getConfig().config?.mailServiceEnabled;
|
|
180
239
|
await sendMailService.sendMail({
|
|
181
240
|
fromUserId: requesterUserId,
|
|
182
241
|
recipientUserIds: [startUserId],
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
242
|
+
// Agent will send a 'Session Completed' message when session is completed
|
|
243
|
+
subject: mailEnabled ? "Agent Start" : "",
|
|
244
|
+
body: mailEnabled
|
|
245
|
+
? taskDescription
|
|
246
|
+
: `Agent Start: ${taskDescription}`,
|
|
247
|
+
kind: mailEnabled ? "mail" : "chat",
|
|
186
248
|
});
|
|
187
249
|
}
|
|
188
250
|
catch (err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildClientConfig, builtInImageModels, builtInLlmModels, } from "@naisys/common";
|
|
1
|
+
import { buildClientConfig, builtInImageModels, builtInLlmModels, OPENAI_CODEX_ACCESS_TOKEN_VAR, OPENAI_CODEX_REFRESH_TOKEN_VAR, } from "@naisys/common";
|
|
2
2
|
import { HubEvents } from "@naisys/hub-protocol";
|
|
3
3
|
import dotenv from "dotenv";
|
|
4
4
|
/** Pushes the global config to NAISYS instances when they connect or when variables change */
|
|
@@ -7,10 +7,13 @@ export async function createHubConfigService(naisysServer, { hubDb }, logService
|
|
|
7
7
|
success: false,
|
|
8
8
|
error: "Not yet loaded",
|
|
9
9
|
};
|
|
10
|
-
// API key variable names referenced by built-in models — always sensitive
|
|
10
|
+
// API key variable names referenced by built-in models — always sensitive.
|
|
11
|
+
// EXPIRES_AT is a timestamp, not a credential — keep it out so nearby
|
|
12
|
+
// millisecond values in logs don't get rewritten as redactions.
|
|
11
13
|
const sensitiveKeys = new Set([...builtInLlmModels, ...builtInImageModels]
|
|
12
14
|
.map((m) => m.apiKeyVar)
|
|
13
|
-
.filter(Boolean)
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.concat([OPENAI_CODEX_ACCESS_TOKEN_VAR, OPENAI_CODEX_REFRESH_TOKEN_VAR]));
|
|
14
17
|
// Seed DB from .env on first run
|
|
15
18
|
const existing = await hubDb.variables.findMany();
|
|
16
19
|
if (existing.length > 0) {
|
|
@@ -93,6 +96,7 @@ export async function createHubConfigService(naisysServer, { hubDb }, logService
|
|
|
93
96
|
await buildConfigPayload();
|
|
94
97
|
return {
|
|
95
98
|
getConfig: () => cachedConfig,
|
|
99
|
+
broadcastConfig,
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
/** Create variable placeholders if they don't already exist.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { hashToken } from "@naisys/common-node";
|
|
1
2
|
import { HeartbeatSchema, HUB_HEARTBEAT_INTERVAL_MS, HubEvents, } from "@naisys/hub-protocol";
|
|
2
3
|
/** Tracks NAISYS instance heartbeats and pushes aggregate active user status to all instances */
|
|
3
|
-
export function createHubHeartbeatService(naisysServer, { hubDb }, logService) {
|
|
4
|
+
export function createHubHeartbeatService(naisysServer, { hubDb }, logService, redactionService, runtimeKeyService) {
|
|
4
5
|
// Active agent user ids per host. Subagents ride under the parent's userId.
|
|
5
6
|
const hostActiveAgents = new Map();
|
|
6
7
|
const hostActiveSessions = new Map();
|
|
@@ -69,14 +70,48 @@ export function createHubHeartbeatService(naisysServer, { hubDb }, logService) {
|
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
hostActiveSessions.set(hostId, sessionMap);
|
|
73
|
+
// Self-heal: register each plaintext with the redactor (idempotent),
|
|
74
|
+
// then mint + push a fresh key if the hash is missing or mismatched.
|
|
75
|
+
// Old plaintexts accumulate per user so leaks during the rotate
|
|
76
|
+
// transition window still get scrubbed; AGENT_STOP clears the set.
|
|
77
|
+
if (parsed.runtimeApiKeys?.length) {
|
|
78
|
+
const userIds = parsed.runtimeApiKeys.map((k) => k.userId);
|
|
79
|
+
const users = await hubDb.users.findMany({
|
|
80
|
+
where: { id: { in: userIds } },
|
|
81
|
+
select: { id: true, api_key_hash: true, enabled: true, archived: true },
|
|
82
|
+
});
|
|
83
|
+
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
84
|
+
for (const claim of parsed.runtimeApiKeys) {
|
|
85
|
+
if (claim.runtimeApiKey) {
|
|
86
|
+
redactionService.registerRuntimeApiKey(claim.userId, claim.runtimeApiKey);
|
|
87
|
+
}
|
|
88
|
+
const user = userMap.get(claim.userId);
|
|
89
|
+
if (!user || !user.enabled || user.archived)
|
|
90
|
+
continue;
|
|
91
|
+
if (claim.runtimeApiKey &&
|
|
92
|
+
user.api_key_hash &&
|
|
93
|
+
hashToken(claim.runtimeApiKey) === user.api_key_hash) {
|
|
94
|
+
continue; // hub already knows this key — nothing to do
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const runtimeApiKey = await runtimeKeyService.issueRuntimeApiKey(claim.userId);
|
|
98
|
+
naisysServer.sendMessage(hostId, HubEvents.RUNTIME_KEY_REISSUE, {
|
|
99
|
+
userId: claim.userId,
|
|
100
|
+
runtimeApiKey,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logService.error(`[Hub:Heartbeat] Failed to reissue runtime key for user ${claim.userId}: ${err}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
72
108
|
}
|
|
73
109
|
catch (error) {
|
|
74
110
|
logService.error(`[Hub:Heartbeat] Error updating heartbeat for host ${hostId}: ${error}`);
|
|
75
111
|
}
|
|
76
112
|
});
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// explicitly on user disable/archive/delete.
|
|
113
|
+
// Runtime keys aren't touched on disconnect — the DB hash stays valid;
|
|
114
|
+
// if the agent reconnects, heartbeat re-registers the plaintext.
|
|
80
115
|
naisysServer.registerEvent(HubEvents.CLIENT_DISCONNECTED, (hostId) => {
|
|
81
116
|
hostActiveAgents.delete(hostId);
|
|
82
117
|
hostActiveSessions.delete(hostId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HubEvents, LogWriteRequestSchema, } from "@naisys/hub-protocol";
|
|
2
2
|
/** Handles log_write events from NAISYS instances (fire-and-forget) */
|
|
3
|
-
export function createHubLogService(naisysServer, { hubDb }, logService, heartbeatService) {
|
|
3
|
+
export function createHubLogService(naisysServer, { hubDb }, logService, heartbeatService, redactionService) {
|
|
4
4
|
// Track last pushed log ID per session for gap detection
|
|
5
5
|
const lastPushedLogId = new Map();
|
|
6
6
|
naisysServer.registerEvent(HubEvents.LOG_WRITE, async (hostId, data) => {
|
|
@@ -11,7 +11,8 @@ export function createHubLogService(naisysServer, { hubDb }, logService, heartbe
|
|
|
11
11
|
const sessionUpdates = new Map();
|
|
12
12
|
for (const entry of parsed.entries) {
|
|
13
13
|
const now = new Date().toISOString();
|
|
14
|
-
const
|
|
14
|
+
const message = redactionService.redact(entry.message);
|
|
15
|
+
const lineCount = message.split("\n").length;
|
|
15
16
|
const subagentId = entry.subagentId ?? 0;
|
|
16
17
|
// Wire format: undefined for parent (subagent_id 0 in DB), number otherwise
|
|
17
18
|
const wireSubagentId = subagentId === 0 ? undefined : subagentId;
|
|
@@ -25,7 +26,7 @@ export function createHubLogService(naisysServer, { hubDb }, logService, heartbe
|
|
|
25
26
|
role: entry.role,
|
|
26
27
|
source: entry.source ?? null,
|
|
27
28
|
type: entry.type ?? null,
|
|
28
|
-
message
|
|
29
|
+
message,
|
|
29
30
|
created_at: entry.createdAt,
|
|
30
31
|
attachment_id: entry.attachmentId ?? null,
|
|
31
32
|
},
|
|
@@ -86,7 +87,7 @@ export function createHubLogService(naisysServer, { hubDb }, logService, heartbe
|
|
|
86
87
|
role: entry.role,
|
|
87
88
|
source: entry.source,
|
|
88
89
|
type: entry.type,
|
|
89
|
-
message
|
|
90
|
+
message,
|
|
90
91
|
createdAt: entry.createdAt,
|
|
91
92
|
attachmentId: attachmentPublicId,
|
|
92
93
|
attachmentFilename,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { HubEvents } from "@naisys/hub-protocol";
|
|
2
|
+
const MIN_SECRET_LENGTH = 6;
|
|
3
|
+
const PATTERN_REPLACEMENTS = [
|
|
4
|
+
{
|
|
5
|
+
pattern: /Authorization:\s*(Bearer|Basic)\s+\S+/gi,
|
|
6
|
+
replacement: "Authorization: $1 [REDACTED]",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
10
|
+
replacement: "[REDACTED:PRIVATE_KEY]",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
14
|
+
replacement: "[REDACTED:JWT]",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
18
|
+
replacement: "[REDACTED:AWS_KEY]",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Strips sensitive values before they hit the DB or get rebroadcast. Two
|
|
23
|
+
* sources:
|
|
24
|
+
* - DB variables flagged sensitive
|
|
25
|
+
* - Runtime NAISYS_API_KEY plaintexts, registered by issueRuntimeApiKey
|
|
26
|
+
* and accumulated per-user as a Set. Heartbeat-driven re-registration
|
|
27
|
+
* self-heals across hub restarts; AGENT_STOP clears the Set.
|
|
28
|
+
*/
|
|
29
|
+
export async function createHubRedactionService(naisysServer, { hubDb }, logService) {
|
|
30
|
+
// Sorted longest-first so a secret that is a prefix of another doesn't
|
|
31
|
+
// get partially replaced before the longer match runs.
|
|
32
|
+
let dbSecrets = [];
|
|
33
|
+
const runtimeApiKeys = new Map();
|
|
34
|
+
async function rebuildDbSecrets() {
|
|
35
|
+
const rows = await hubDb.variables.findMany({ where: { sensitive: true } });
|
|
36
|
+
dbSecrets = rows
|
|
37
|
+
.filter((r) => r.value && r.value.length >= MIN_SECRET_LENGTH)
|
|
38
|
+
.map((r) => ({ value: r.value, key: r.key }))
|
|
39
|
+
.sort((a, b) => b.value.length - a.value.length);
|
|
40
|
+
}
|
|
41
|
+
function redact(text) {
|
|
42
|
+
if (!text)
|
|
43
|
+
return text ?? "";
|
|
44
|
+
let out = text;
|
|
45
|
+
for (const { value, key } of dbSecrets) {
|
|
46
|
+
if (out.includes(value)) {
|
|
47
|
+
out = out.split(value).join(`[REDACTED:${key}]`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const [userId, plaintexts] of runtimeApiKeys) {
|
|
51
|
+
for (const plaintext of plaintexts) {
|
|
52
|
+
if (out.includes(plaintext)) {
|
|
53
|
+
out = out
|
|
54
|
+
.split(plaintext)
|
|
55
|
+
.join(`[REDACTED:NAISYS_API_KEY:${userId}]`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const { pattern, replacement } of PATTERN_REPLACEMENTS) {
|
|
60
|
+
out = out.replace(pattern, replacement);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function registerRuntimeApiKey(userId, plaintext) {
|
|
65
|
+
if (!plaintext || plaintext.length < MIN_SECRET_LENGTH)
|
|
66
|
+
return;
|
|
67
|
+
let set = runtimeApiKeys.get(userId);
|
|
68
|
+
if (!set) {
|
|
69
|
+
set = new Set();
|
|
70
|
+
runtimeApiKeys.set(userId, set);
|
|
71
|
+
}
|
|
72
|
+
set.add(plaintext);
|
|
73
|
+
}
|
|
74
|
+
function revokeRuntimeApiKey(userId) {
|
|
75
|
+
runtimeApiKeys.delete(userId);
|
|
76
|
+
}
|
|
77
|
+
naisysServer.registerEvent(HubEvents.VARIABLES_CHANGED, async () => {
|
|
78
|
+
try {
|
|
79
|
+
await rebuildDbSecrets();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
logService.error(`[Hub:Redaction] Failed to rebuild secrets after VARIABLES_CHANGED: ${error}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
await rebuildDbSecrets();
|
|
86
|
+
return {
|
|
87
|
+
redact,
|
|
88
|
+
registerRuntimeApiKey,
|
|
89
|
+
revokeRuntimeApiKey,
|
|
90
|
+
rebuildDbSecrets,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=hubRedactionService.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { HubEvents } from "@naisys/hub-protocol";
|
|
2
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { createHubRedactionService } from "./hubRedactionService.js";
|
|
4
|
+
function createServerHarness() {
|
|
5
|
+
const handlers = new Map();
|
|
6
|
+
const server = {
|
|
7
|
+
registerEvent: vi.fn((event, handler) => {
|
|
8
|
+
handlers.set(event, handler);
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
async function emit(event, ...args) {
|
|
12
|
+
const handler = handlers.get(event);
|
|
13
|
+
if (!handler)
|
|
14
|
+
throw new Error(`No handler for ${event}`);
|
|
15
|
+
await handler(...args);
|
|
16
|
+
}
|
|
17
|
+
return { server, emit };
|
|
18
|
+
}
|
|
19
|
+
function createHubDb(initialRows) {
|
|
20
|
+
let rows = [...initialRows];
|
|
21
|
+
const hubDb = {
|
|
22
|
+
variables: {
|
|
23
|
+
findMany: vi.fn(({ where }) => Promise.resolve(rows.filter((r) => r.sensitive === where.sensitive))),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
hubDb,
|
|
28
|
+
setRows: (next) => {
|
|
29
|
+
rows = [...next];
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createLogger() {
|
|
34
|
+
return {
|
|
35
|
+
log: vi.fn(),
|
|
36
|
+
error: vi.fn(),
|
|
37
|
+
disableConsole: vi.fn(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
describe("hubRedactionService", () => {
|
|
41
|
+
let server;
|
|
42
|
+
let emit;
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
const harness = createServerHarness();
|
|
45
|
+
server = harness.server;
|
|
46
|
+
emit = harness.emit;
|
|
47
|
+
});
|
|
48
|
+
test("redacts sensitive variable values", async () => {
|
|
49
|
+
const { hubDb } = createHubDb([
|
|
50
|
+
{ key: "OPENAI_API_KEY", value: "sk-abc123def456", sensitive: true },
|
|
51
|
+
{ key: "PUBLIC_VAR", value: "not-a-secret", sensitive: false },
|
|
52
|
+
]);
|
|
53
|
+
const svc = await createHubRedactionService(server, { hubDb }, createLogger());
|
|
54
|
+
expect(svc.redact("call with sk-abc123def456 here")).toBe("call with [REDACTED:OPENAI_API_KEY] here");
|
|
55
|
+
expect(svc.redact("not-a-secret stays")).toBe("not-a-secret stays");
|
|
56
|
+
});
|
|
57
|
+
test("skips sensitive values shorter than 6 chars", async () => {
|
|
58
|
+
const { hubDb } = createHubDb([
|
|
59
|
+
{ key: "SHORT", value: "abc", sensitive: true },
|
|
60
|
+
{ key: "LONG_ENOUGH", value: "123456", sensitive: true },
|
|
61
|
+
]);
|
|
62
|
+
const svc = await createHubRedactionService(server, { hubDb }, createLogger());
|
|
63
|
+
expect(svc.redact("abc and 123456")).toBe("abc and [REDACTED:LONG_ENOUGH]");
|
|
64
|
+
});
|
|
65
|
+
test("longest-first ordering avoids partial replacement", async () => {
|
|
66
|
+
const { hubDb } = createHubDb([
|
|
67
|
+
{ key: "SHORT_KEY", value: "abc123", sensitive: true },
|
|
68
|
+
{ key: "LONG_KEY", value: "abc1234567", sensitive: true },
|
|
69
|
+
]);
|
|
70
|
+
const svc = await createHubRedactionService(server, { hubDb }, createLogger());
|
|
71
|
+
expect(svc.redact("token=abc1234567")).toBe("token=[REDACTED:LONG_KEY]");
|
|
72
|
+
});
|
|
73
|
+
test("registers and revokes runtime API keys per user", async () => {
|
|
74
|
+
const { hubDb } = createHubDb([]);
|
|
75
|
+
const svc = await createHubRedactionService(server, { hubDb }, createLogger());
|
|
76
|
+
svc.registerRuntimeApiKey(7, "deadbeefcafebabe");
|
|
77
|
+
expect(svc.redact("auth=deadbeefcafebabe")).toBe("auth=[REDACTED:NAISYS_API_KEY:7]");
|
|
78
|
+
svc.revokeRuntimeApiKey(7);
|
|
79
|
+
expect(svc.redact("auth=deadbeefcafebabe")).toBe("auth=deadbeefcafebabe");
|
|
80
|
+
});
|
|
81
|
+
test("accumulates plaintexts per user so old keys stay redactable mid-rotation", async () => {
|
|
82
|
+
const { hubDb } = createHubDb([]);
|
|
83
|
+
const svc = await createHubRedactionService(server, { hubDb }, createLogger());
|
|
84
|
+
svc.registerRuntimeApiKey(7, "oldkey1234567");
|
|
85
|
+
svc.registerRuntimeApiKey(7, "newkey7654321");
|
|
86
|
+
expect(svc.redact("oldkey1234567 and newkey7654321")).toBe("[REDACTED:NAISYS_API_KEY:7] and [REDACTED:NAISYS_API_KEY:7]");
|
|
87
|
+
svc.revokeRuntimeApiKey(7);
|
|
88
|
+
expect(svc.redact("oldkey1234567 and newkey7654321")).toBe("oldkey1234567 and newkey7654321");
|
|
89
|
+
});
|
|
90
|
+
test("pattern fallbacks catch shapes not in the secret list", async () => {
|
|
91
|
+
const { hubDb } = createHubDb([]);
|
|
92
|
+
const svc = await createHubRedactionService(server, { hubDb }, createLogger());
|
|
93
|
+
expect(svc.redact("Authorization: Bearer xyz.token.here")).toBe("Authorization: Bearer [REDACTED]");
|
|
94
|
+
expect(svc.redact("AKIAIOSFODNN7EXAMPLE")).toBe("[REDACTED:AWS_KEY]");
|
|
95
|
+
expect(svc.redact("header eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c trail")).toBe("header [REDACTED:JWT] trail");
|
|
96
|
+
});
|
|
97
|
+
test("redact returns empty string for null/undefined", async () => {
|
|
98
|
+
const { hubDb } = createHubDb([]);
|
|
99
|
+
const svc = await createHubRedactionService(server, { hubDb }, createLogger());
|
|
100
|
+
expect(svc.redact(null)).toBe("");
|
|
101
|
+
expect(svc.redact(undefined)).toBe("");
|
|
102
|
+
expect(svc.redact("")).toBe("");
|
|
103
|
+
});
|
|
104
|
+
test("rebuilds dbSecrets on VARIABLES_CHANGED", async () => {
|
|
105
|
+
const { hubDb, setRows } = createHubDb([
|
|
106
|
+
{ key: "OLD_KEY", value: "old-secret-value", sensitive: true },
|
|
107
|
+
]);
|
|
108
|
+
const svc = await createHubRedactionService(server, { hubDb }, createLogger());
|
|
109
|
+
expect(svc.redact("old-secret-value")).toBe("[REDACTED:OLD_KEY]");
|
|
110
|
+
setRows([{ key: "NEW_KEY", value: "new-secret-value", sensitive: true }]);
|
|
111
|
+
await emit(HubEvents.VARIABLES_CHANGED, 1);
|
|
112
|
+
expect(svc.redact("old-secret-value")).toBe("old-secret-value");
|
|
113
|
+
expect(svc.redact("new-secret-value")).toBe("[REDACTED:NEW_KEY]");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=hubRedactionService.test.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { hashToken } from "@naisys/common-node";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
/**
|
|
4
|
+
* Mints and revokes per-user runtime API keys. 32 random bytes; only the
|
|
5
|
+
* hash is persisted. Plaintext is registered with the redactor on issue so
|
|
6
|
+
* leaks get scrubbed; the user's Set is cleared on revoke.
|
|
7
|
+
*/
|
|
8
|
+
export function createHubRuntimeKeyService({ hubDb }, redactionService) {
|
|
9
|
+
async function issueRuntimeApiKey(userId) {
|
|
10
|
+
const token = randomBytes(32).toString("hex");
|
|
11
|
+
await hubDb.users.update({
|
|
12
|
+
where: { id: userId },
|
|
13
|
+
data: { api_key_hash: hashToken(token) },
|
|
14
|
+
});
|
|
15
|
+
redactionService.registerRuntimeApiKey(userId, token);
|
|
16
|
+
return token;
|
|
17
|
+
}
|
|
18
|
+
async function revokeRuntimeApiKey(userId) {
|
|
19
|
+
await hubDb.users.update({
|
|
20
|
+
where: { id: userId },
|
|
21
|
+
data: { api_key_hash: null },
|
|
22
|
+
});
|
|
23
|
+
redactionService.revokeRuntimeApiKey(userId);
|
|
24
|
+
}
|
|
25
|
+
return { issueRuntimeApiKey, revokeRuntimeApiKey };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=hubRuntimeKeyService.js.map
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { HubEvents, } from "@naisys/hub-protocol";
|
|
2
2
|
/** Pure send-mail service with no auto-start logic, breaking the circular dependency */
|
|
3
|
-
export function createHubSendMailService(naisysServer, { hubDb }, heartbeatService) {
|
|
3
|
+
export function createHubSendMailService(naisysServer, { hubDb }, heartbeatService, redactionService) {
|
|
4
4
|
/** Send a mail message directly by user IDs */
|
|
5
5
|
async function sendMail(params) {
|
|
6
6
|
const now = new Date();
|
|
7
|
+
// Redact once at entry — DB row, supervisor push, and any future fan-out
|
|
8
|
+
// all read from these locals.
|
|
9
|
+
const subject = redactionService.redact(params.subject);
|
|
10
|
+
const body = redactionService.redact(params.body);
|
|
7
11
|
// Build participants string from usernames (sorted alphabetically)
|
|
8
12
|
const allUserIds = [
|
|
9
13
|
...new Set([params.fromUserId, ...params.recipientUserIds]),
|
|
@@ -24,8 +28,8 @@ export function createHubSendMailService(naisysServer, { hubDb }, heartbeatServi
|
|
|
24
28
|
host_id: params.hostId,
|
|
25
29
|
kind: params.kind,
|
|
26
30
|
participants,
|
|
27
|
-
subject
|
|
28
|
-
body
|
|
31
|
+
subject,
|
|
32
|
+
body,
|
|
29
33
|
created_at: now,
|
|
30
34
|
},
|
|
31
35
|
});
|
|
@@ -114,8 +118,8 @@ export function createHubSendMailService(naisysServer, { hubDb }, heartbeatServi
|
|
|
114
118
|
fromUserId: params.fromUserId,
|
|
115
119
|
kind: params.kind,
|
|
116
120
|
messageId: message.id,
|
|
117
|
-
subject
|
|
118
|
-
body
|
|
121
|
+
subject,
|
|
122
|
+
body,
|
|
119
123
|
createdAt: now.toISOString(),
|
|
120
124
|
participants,
|
|
121
125
|
attachments,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { OPENAI_CODEX_ACCESS_TOKEN_VAR, OPENAI_CODEX_EXPIRES_AT_VAR, OPENAI_CODEX_REFRESH_TOKEN_VAR, } from "@naisys/common";
|
|
2
|
+
import { HubEvents, VariablePatchRequestSchema, } from "@naisys/hub-protocol";
|
|
3
|
+
// Allowlist + per-key storage policy. Without this, a compromised host could
|
|
4
|
+
// rotate any API key and have the hub broadcast it to every other host.
|
|
5
|
+
const clientPatchableVariables = new Map([
|
|
6
|
+
[OPENAI_CODEX_ACCESS_TOKEN_VAR, { sensitive: true, exportToShell: false }],
|
|
7
|
+
[OPENAI_CODEX_REFRESH_TOKEN_VAR, { sensitive: true, exportToShell: false }],
|
|
8
|
+
[OPENAI_CODEX_EXPIRES_AT_VAR, { sensitive: false, exportToShell: false }],
|
|
9
|
+
]);
|
|
10
|
+
export function createHubVariablePatchService(naisysServer, { hubDb }, configService, redactionService, logService) {
|
|
11
|
+
async function patchVariables(hostId, request) {
|
|
12
|
+
const denied = request.updates.find(({ key }) => !clientPatchableVariables.has(key));
|
|
13
|
+
if (denied) {
|
|
14
|
+
logService.error(`[Hub:Variables] Rejected patch from host ${hostId}: variable '${denied.key}' is not patchable by NAISYS clients.`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
await hubDb.$transaction(async (tx) => {
|
|
18
|
+
for (const update of request.updates) {
|
|
19
|
+
const policy = clientPatchableVariables.get(update.key);
|
|
20
|
+
await tx.variables.upsert({
|
|
21
|
+
where: { key: update.key },
|
|
22
|
+
update: {
|
|
23
|
+
value: update.value,
|
|
24
|
+
export_to_shell: policy.exportToShell,
|
|
25
|
+
sensitive: policy.sensitive,
|
|
26
|
+
updated_by: `host:${hostId}`,
|
|
27
|
+
},
|
|
28
|
+
create: {
|
|
29
|
+
key: update.key,
|
|
30
|
+
value: update.value,
|
|
31
|
+
export_to_shell: policy.exportToShell,
|
|
32
|
+
sensitive: policy.sensitive,
|
|
33
|
+
created_by: `host:${hostId}`,
|
|
34
|
+
updated_by: `host:${hostId}`,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Inline rather than firing VARIABLES_CHANGED so the redaction set is
|
|
40
|
+
// rebuilt before any subsequent log lines ingest.
|
|
41
|
+
await redactionService.rebuildDbSecrets();
|
|
42
|
+
await configService.broadcastConfig();
|
|
43
|
+
}
|
|
44
|
+
naisysServer.registerEvent(HubEvents.VARIABLE_PATCH, async (hostId, request) => {
|
|
45
|
+
try {
|
|
46
|
+
await patchVariables(hostId, request);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logService.error(`[Hub:Variables] Failed to apply variable patch from host ${hostId}: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
}, VariablePatchRequestSchema);
|
|
52
|
+
return { patchVariables };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=hubVariablePatchService.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { OPENAI_CODEX_ACCESS_TOKEN_VAR, OPENAI_CODEX_EXPIRES_AT_VAR, OPENAI_CODEX_REFRESH_TOKEN_VAR, } from "@naisys/common";
|
|
2
|
+
import { HubEvents } from "@naisys/hub-protocol";
|
|
3
|
+
import { describe, expect, test, vi } from "vitest";
|
|
4
|
+
import { createHubVariablePatchService } from "./hubVariablePatchService.js";
|
|
5
|
+
function createMocks() {
|
|
6
|
+
const variables = {
|
|
7
|
+
upsert: vi.fn(),
|
|
8
|
+
};
|
|
9
|
+
const hubDb = {
|
|
10
|
+
$transaction: vi.fn(async (callback) => callback({ variables })),
|
|
11
|
+
};
|
|
12
|
+
const configService = {
|
|
13
|
+
broadcastConfig: vi.fn(async () => { }),
|
|
14
|
+
};
|
|
15
|
+
const redactionService = {
|
|
16
|
+
rebuildDbSecrets: vi.fn(async () => { }),
|
|
17
|
+
};
|
|
18
|
+
const logService = {
|
|
19
|
+
error: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
const naisysServer = {
|
|
22
|
+
registerEvent: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
variables,
|
|
26
|
+
hubDb,
|
|
27
|
+
configService,
|
|
28
|
+
redactionService,
|
|
29
|
+
logService,
|
|
30
|
+
naisysServer,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
describe("hub variable patch service", () => {
|
|
34
|
+
test("persists Codex OAuth variables with per-key policy and broadcasts config", async () => {
|
|
35
|
+
const mocks = createMocks();
|
|
36
|
+
createHubVariablePatchService(mocks.naisysServer, { hubDb: mocks.hubDb }, mocks.configService, mocks.redactionService, mocks.logService);
|
|
37
|
+
const [, handler, schema] = mocks.naisysServer.registerEvent.mock
|
|
38
|
+
.calls[0];
|
|
39
|
+
expect(mocks.naisysServer.registerEvent).toHaveBeenCalledWith(HubEvents.VARIABLE_PATCH, expect.any(Function), expect.any(Object));
|
|
40
|
+
expect(schema.safeParse({
|
|
41
|
+
updates: [{ key: OPENAI_CODEX_ACCESS_TOKEN_VAR, value: "access" }],
|
|
42
|
+
}).success).toBe(true);
|
|
43
|
+
await handler(42, {
|
|
44
|
+
updates: [
|
|
45
|
+
{ key: OPENAI_CODEX_ACCESS_TOKEN_VAR, value: "access" },
|
|
46
|
+
{ key: OPENAI_CODEX_REFRESH_TOKEN_VAR, value: "refresh" },
|
|
47
|
+
{ key: OPENAI_CODEX_EXPIRES_AT_VAR, value: "1700000000000" },
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
expect(mocks.variables.upsert).toHaveBeenCalledTimes(3);
|
|
51
|
+
expect(mocks.variables.upsert).toHaveBeenCalledWith(expect.objectContaining({
|
|
52
|
+
where: { key: OPENAI_CODEX_ACCESS_TOKEN_VAR },
|
|
53
|
+
update: expect.objectContaining({
|
|
54
|
+
value: "access",
|
|
55
|
+
export_to_shell: false,
|
|
56
|
+
sensitive: true,
|
|
57
|
+
updated_by: "host:42",
|
|
58
|
+
}),
|
|
59
|
+
}));
|
|
60
|
+
expect(mocks.variables.upsert).toHaveBeenCalledWith(expect.objectContaining({
|
|
61
|
+
where: { key: OPENAI_CODEX_EXPIRES_AT_VAR },
|
|
62
|
+
update: expect.objectContaining({
|
|
63
|
+
value: "1700000000000",
|
|
64
|
+
export_to_shell: false,
|
|
65
|
+
sensitive: false,
|
|
66
|
+
updated_by: "host:42",
|
|
67
|
+
}),
|
|
68
|
+
}));
|
|
69
|
+
expect(mocks.redactionService.rebuildDbSecrets).toHaveBeenCalledOnce();
|
|
70
|
+
expect(mocks.configService.broadcastConfig).toHaveBeenCalledOnce();
|
|
71
|
+
});
|
|
72
|
+
test("logs and skips persistence for non-allowlisted variables", async () => {
|
|
73
|
+
const mocks = createMocks();
|
|
74
|
+
createHubVariablePatchService(mocks.naisysServer, { hubDb: mocks.hubDb }, mocks.configService, mocks.redactionService, mocks.logService);
|
|
75
|
+
const [, handler] = mocks.naisysServer.registerEvent.mock.calls[0];
|
|
76
|
+
await handler(7, {
|
|
77
|
+
updates: [{ key: "GOOGLE_API_KEY", value: "secret" }],
|
|
78
|
+
});
|
|
79
|
+
expect(mocks.hubDb.$transaction).not.toHaveBeenCalled();
|
|
80
|
+
expect(mocks.redactionService.rebuildDbSecrets).not.toHaveBeenCalled();
|
|
81
|
+
expect(mocks.configService.broadcastConfig).not.toHaveBeenCalled();
|
|
82
|
+
expect(mocks.logService.error).toHaveBeenCalledWith(expect.stringContaining("GOOGLE_API_KEY"));
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=hubVariablePatchService.test.js.map
|
package/dist/naisysHub.js
CHANGED
|
@@ -15,9 +15,12 @@ import { createHubHostService } from "./handlers/hubHostService.js";
|
|
|
15
15
|
import { createHubLogService } from "./handlers/hubLogService.js";
|
|
16
16
|
import { createHubMailService } from "./handlers/hubMailService.js";
|
|
17
17
|
import { createHubModelsService } from "./handlers/hubModelsService.js";
|
|
18
|
+
import { createHubRedactionService } from "./handlers/hubRedactionService.js";
|
|
18
19
|
import { createHubRunService } from "./handlers/hubRunService.js";
|
|
20
|
+
import { createHubRuntimeKeyService } from "./handlers/hubRuntimeKeyService.js";
|
|
19
21
|
import { createHubSendMailService } from "./handlers/hubSendMailService.js";
|
|
20
22
|
import { createHubUserService } from "./handlers/hubUserService.js";
|
|
23
|
+
import { createHubVariablePatchService } from "./handlers/hubVariablePatchService.js";
|
|
21
24
|
import { loadOrCreateAccessKey } from "./services/accessKeyService.js";
|
|
22
25
|
import { seedAgentConfigs } from "./services/agentRegistrar.js";
|
|
23
26
|
import { createHostRegistrar } from "./services/hostRegistrar.js";
|
|
@@ -65,20 +68,28 @@ export const startHub = async (startupType, startSupervisor, plugins, startupAge
|
|
|
65
68
|
createHubUserService(naisysServer, hubDatabaseService, logService);
|
|
66
69
|
// Register hub models service for seeding and broadcasting models
|
|
67
70
|
await createHubModelsService(naisysServer, hubDatabaseService, logService);
|
|
71
|
+
// Register redaction service so log/mail ingest can scrub sensitive values
|
|
72
|
+
// before they hit the DB or get rebroadcast. Must come after the models
|
|
73
|
+
// service since that upgrades built-in model API key variables to sensitive
|
|
74
|
+
// — initializing the redactor earlier would snapshot them as non-sensitive.
|
|
75
|
+
const redactionService = await createHubRedactionService(naisysServer, hubDatabaseService, logService);
|
|
76
|
+
createHubVariablePatchService(naisysServer, hubDatabaseService, configService, redactionService, logService);
|
|
77
|
+
// Shared mint/revoke for runtime API keys (agent service + heartbeat).
|
|
78
|
+
const runtimeKeyService = createHubRuntimeKeyService(hubDatabaseService, redactionService);
|
|
68
79
|
// Register hub host service for broadcasting connected host list
|
|
69
80
|
createHubHostService(naisysServer, hostRegistrar, logService);
|
|
70
81
|
// Register hub run service for session_create/session_increment requests
|
|
71
82
|
createHubRunService(naisysServer, hubDatabaseService, logService);
|
|
72
83
|
// Register hub heartbeat service for NAISYS instance heartbeat tracking
|
|
73
|
-
const heartbeatService = createHubHeartbeatService(naisysServer, hubDatabaseService, logService);
|
|
84
|
+
const heartbeatService = createHubHeartbeatService(naisysServer, hubDatabaseService, logService, redactionService, runtimeKeyService);
|
|
74
85
|
// Register hub log service for log_write events from NAISYS instances
|
|
75
|
-
createHubLogService(naisysServer, hubDatabaseService, logService, heartbeatService);
|
|
86
|
+
createHubLogService(naisysServer, hubDatabaseService, logService, heartbeatService, redactionService);
|
|
76
87
|
// Register hub cost service for cost_write events from NAISYS instances
|
|
77
88
|
const costService = createHubCostService(naisysServer, hubDatabaseService, logService, heartbeatService, configService);
|
|
78
89
|
// Register hub send mail service (pure mail sending, no auto-start logic)
|
|
79
|
-
const sendMailService = createHubSendMailService(naisysServer, hubDatabaseService, heartbeatService);
|
|
90
|
+
const sendMailService = createHubSendMailService(naisysServer, hubDatabaseService, heartbeatService, redactionService);
|
|
80
91
|
// Register hub agent service for agent_start requests routed to target hosts
|
|
81
|
-
const agentService = createHubAgentService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, hostRegistrar);
|
|
92
|
+
const agentService = createHubAgentService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, hostRegistrar, runtimeKeyService, configService);
|
|
82
93
|
// Register hub mail service for mail events from NAISYS instances
|
|
83
94
|
const mailService = createHubMailService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, agentService, costService, configService);
|
|
84
95
|
/**
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naisys/hub",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.41",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@naisys/hub",
|
|
9
|
-
"version": "3.0.0-beta.
|
|
9
|
+
"version": "3.0.0-beta.41",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@naisys/common": "3.0.0-beta.
|
|
12
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
13
|
-
"@naisys/hub-database": "3.0.0-beta.
|
|
14
|
-
"@naisys/hub-protocol": "3.0.0-beta.
|
|
11
|
+
"@naisys/common": "3.0.0-beta.41",
|
|
12
|
+
"@naisys/common-node": "3.0.0-beta.41",
|
|
13
|
+
"@naisys/hub-database": "3.0.0-beta.41",
|
|
14
|
+
"@naisys/hub-protocol": "3.0.0-beta.41",
|
|
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.
|
|
27
|
+
"@naisys/supervisor": "3.0.0-beta.41"
|
|
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.
|
|
193
|
-
"resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.
|
|
194
|
-
"integrity": "sha512-
|
|
192
|
+
"version": "3.0.0-beta.41",
|
|
193
|
+
"resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.41.tgz",
|
|
194
|
+
"integrity": "sha512-TuaJbDPsNkqTM7HCcMjGM6g9SvwohkEcOSieWAsl4z4uSOvlfYd064vjCmJkhjPNwXTiKYd3OAj23HD1adv0cg==",
|
|
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.
|
|
202
|
-
"resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.
|
|
203
|
-
"integrity": "sha512-
|
|
201
|
+
"version": "3.0.0-beta.41",
|
|
202
|
+
"resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.41.tgz",
|
|
203
|
+
"integrity": "sha512-Zmxs6kwIWdIXlZuoCuUhGiu3pky5wDZv62YsoN3N11xLRt0CB0M6jsU8n2kygSHxvjV3qZ+HXEB8GdNTUDuY3Q==",
|
|
204
204
|
"dependencies": {
|
|
205
|
-
"@naisys/common": "3.0.0-beta.
|
|
205
|
+
"@naisys/common": "3.0.0-beta.41",
|
|
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.
|
|
213
|
-
"resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.
|
|
214
|
-
"integrity": "sha512-
|
|
212
|
+
"version": "3.0.0-beta.41",
|
|
213
|
+
"resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.41.tgz",
|
|
214
|
+
"integrity": "sha512-p87AlaYVn7YT3Rw2o22+KDvRtYJWXOPDiVVUcyt4pNdB3xcqezVkf9a9LdqhqvE7hkzWaBGJsXDEDl3ILX6H5Q==",
|
|
215
215
|
"dependencies": {
|
|
216
|
-
"@naisys/common": "3.0.0-beta.
|
|
217
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
216
|
+
"@naisys/common": "3.0.0-beta.41",
|
|
217
|
+
"@naisys/common-node": "3.0.0-beta.41",
|
|
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.
|
|
226
|
-
"resolved": "https://registry.npmjs.org/@naisys/hub-protocol/-/hub-protocol-3.0.0-beta.
|
|
227
|
-
"integrity": "sha512-
|
|
225
|
+
"version": "3.0.0-beta.41",
|
|
226
|
+
"resolved": "https://registry.npmjs.org/@naisys/hub-protocol/-/hub-protocol-3.0.0-beta.41.tgz",
|
|
227
|
+
"integrity": "sha512-pTh7zBSWF+S2/hLG07m/BLlQxXwxzl/LLWF3LnHOXlwjIvByS3VKsTp0K2Slo3H/qHKz41q8sE66HNpjdHezPA==",
|
|
228
228
|
"dependencies": {
|
|
229
|
-
"@naisys/common": "3.0.0-beta.
|
|
229
|
+
"@naisys/common": "3.0.0-beta.41",
|
|
230
230
|
"zod": "^4.3.6"
|
|
231
231
|
}
|
|
232
232
|
},
|
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.41",
|
|
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.
|
|
35
|
+
"@naisys/supervisor": "3.0.0-beta.41"
|
|
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
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
45
|
-
"@naisys/hub-database": "3.0.0-beta.
|
|
46
|
-
"@naisys/hub-protocol": "3.0.0-beta.
|
|
43
|
+
"@naisys/common": "3.0.0-beta.41",
|
|
44
|
+
"@naisys/common-node": "3.0.0-beta.41",
|
|
45
|
+
"@naisys/hub-database": "3.0.0-beta.41",
|
|
46
|
+
"@naisys/hub-protocol": "3.0.0-beta.41",
|
|
47
47
|
"commander": "^14.0.3",
|
|
48
48
|
"dotenv": "^17.3.1",
|
|
49
49
|
"fastify": "^5.8.2",
|