@naisys/hub 3.0.0-beta.38 → 3.0.0-beta.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
@@ -1,4 +1,4 @@
1
- import { createDualLogger, cwdWithTilde, ensureDotEnv, expandNaisysFolder, promptResetSuperAdminPasskey, runSetupWizard, } from "@naisys/common-node";
1
+ import { createDualLogger, cwdWithTilde, ensureDotEnv, expandNaisysFolder, promptResetSuperAdminAccount, 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";
@@ -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";
@@ -29,6 +32,7 @@ import { createNaisysServer } from "./services/naisysServer.js";
29
32
  export const startHub = async (startupType, startSupervisor, plugins, startupAgentPath, wizardRan) => {
30
33
  try {
31
34
  const agentPath = startupAgentPath || ".";
35
+ let cleanupSupervisor;
32
36
  // Create log service first
33
37
  const logService = createDualLogger("hub-server.log");
34
38
  logService.log(`[Hub] Starting Hub server in ${startupType} mode...`);
@@ -64,22 +68,30 @@ export const startHub = async (startupType, startSupervisor, plugins, startupAge
64
68
  createHubUserService(naisysServer, hubDatabaseService, logService);
65
69
  // Register hub models service for seeding and broadcasting models
66
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);
67
79
  // Register hub host service for broadcasting connected host list
68
80
  createHubHostService(naisysServer, hostRegistrar, logService);
69
81
  // Register hub run service for session_create/session_increment requests
70
82
  createHubRunService(naisysServer, hubDatabaseService, logService);
71
83
  // Register hub heartbeat service for NAISYS instance heartbeat tracking
72
- const heartbeatService = createHubHeartbeatService(naisysServer, hubDatabaseService, logService);
84
+ const heartbeatService = createHubHeartbeatService(naisysServer, hubDatabaseService, logService, redactionService, runtimeKeyService);
73
85
  // Register hub log service for log_write events from NAISYS instances
74
- createHubLogService(naisysServer, hubDatabaseService, logService, heartbeatService);
86
+ createHubLogService(naisysServer, hubDatabaseService, logService, heartbeatService, redactionService);
75
87
  // Register hub cost service for cost_write events from NAISYS instances
76
88
  const costService = createHubCostService(naisysServer, hubDatabaseService, logService, heartbeatService, configService);
77
89
  // Register hub send mail service (pure mail sending, no auto-start logic)
78
- const sendMailService = createHubSendMailService(naisysServer, hubDatabaseService, heartbeatService);
90
+ const sendMailService = createHubSendMailService(naisysServer, hubDatabaseService, heartbeatService, redactionService);
79
91
  // Register hub agent service for agent_start requests routed to target hosts
80
- const agentService = createHubAgentService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, hostRegistrar);
92
+ const agentService = createHubAgentService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, hostRegistrar, runtimeKeyService);
81
93
  // Register hub mail service for mail events from NAISYS instances
82
- createHubMailService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, agentService, costService, configService);
94
+ const mailService = createHubMailService(naisysServer, hubDatabaseService, logService, heartbeatService, sendMailService, agentService, costService, configService);
83
95
  /**
84
96
  * There should be no dependency between supervisor and hub
85
97
  * Sharing the same process space is to save 150 mb of node.js runtime memory on small servers
@@ -88,14 +100,16 @@ export const startHub = async (startupType, startSupervisor, plugins, startupAge
88
100
  // Don't import the whole fastify web server module tree unless needed
89
101
  // Use variable to avoid compile-time type dependency on @naisys/supervisor (allows parallel builds)
90
102
  const supervisorModule = "@naisys/supervisor";
91
- const { supervisorPlugin, bootstrapSupervisor } = (await import(supervisorModule));
92
- const resetSuperAdminPasskey = wizardRan
93
- ? await promptResetSuperAdminPasskey("Supervisor Setup", {
103
+ const hostedSupervisor = (await import(supervisorModule));
104
+ const { supervisorPlugin, bootstrapSupervisor } = hostedSupervisor;
105
+ cleanupSupervisor = hostedSupervisor.cleanupSupervisor;
106
+ const resetSuperAdminAccount = wizardRan
107
+ ? await promptResetSuperAdminAccount("Supervisor Setup", {
94
108
  defaultReset: !process.argv.includes("--setup"),
95
109
  })
96
110
  : false;
97
111
  // Bootstrap before plugin register so the operator prompt isn't bounded by pluginTimeout and doesn't interleave with hub connection logs.
98
- await bootstrapSupervisor({ resetSuperAdminPasskey });
112
+ await bootstrapSupervisor({ resetSuperAdminAccount });
99
113
  await fastify.register(supervisorPlugin, {
100
114
  plugins,
101
115
  serverPort,
@@ -109,11 +123,14 @@ export const startHub = async (startupType, startSupervisor, plugins, startupAge
109
123
  logService.disableConsole();
110
124
  }
111
125
  let shutdownPromise = null;
126
+ // Like NAISYS: process exit reaps sockets and Fastify. Clear known timers
127
+ // synchronously, but only wait for the DB disconnect.
112
128
  async function runShutdown() {
113
129
  try {
130
+ cleanupSupervisor?.();
114
131
  heartbeatService.cleanup();
115
- await io.close();
116
- await fastify.close();
132
+ costService.cleanup();
133
+ mailService.cleanup();
117
134
  }
118
135
  finally {
119
136
  await hubDatabaseService.disconnect();
@@ -68,7 +68,10 @@ export function createNaisysServer(nsp, initialHubAccessKey, logService, hostReg
68
68
  const { hubAccessKey: clientAccessKey, hostName, machineId: rawMachineId, instanceId: rawInstanceId, startedAt: rawStartedAt, hostType: rawHostType, clientVersion, environment: rawEnvironment, } = socket.handshake.auth;
69
69
  if (!clientAccessKey || clientAccessKey !== hubAccessKey) {
70
70
  logService.log(`[Hub] Connection rejected: invalid access key from ${socket.handshake.address}`);
71
- return next(createConnectError("Invalid access key", "invalid_access_key"));
71
+ // Non-fatal: keys can rotate while a client is in retry; the client's
72
+ // auth callback re-reads the key on each attempt so the next try picks
73
+ // up the new value.
74
+ return next(createConnectError("Invalid access key", "invalid_access_key", false));
72
75
  }
73
76
  if (!hostName) {
74
77
  logService.log(`[Hub] Connection rejected: missing hostName`);
@@ -123,7 +126,9 @@ export function createNaisysServer(nsp, initialHubAccessKey, logService, hostReg
123
126
  }
124
127
  catch (err) {
125
128
  logService.error(`[Hub] Connection rejected: failed to register host ${hostName}: ${err}`);
126
- return next(createConnectError("NAISYS instance registration failed", "registration_failed"));
129
+ // Non-fatal: registration touches the DB and can hit transient failures
130
+ // (pool timeout, deadlock); let the client keep retrying.
131
+ return next(createConnectError("NAISYS instance registration failed", "registration_failed", false));
127
132
  }
128
133
  });
129
134
  // Handle new connections
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@naisys/hub",
3
- "version": "3.0.0-beta.38",
3
+ "version": "3.0.0-beta.40",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@naisys/hub",
9
- "version": "3.0.0-beta.38",
9
+ "version": "3.0.0-beta.40",
10
10
  "dependencies": {
11
- "@naisys/common": "3.0.0-beta.38",
12
- "@naisys/common-node": "3.0.0-beta.38",
13
- "@naisys/hub-database": "3.0.0-beta.38",
14
- "@naisys/hub-protocol": "3.0.0-beta.38",
11
+ "@naisys/common": "3.0.0-beta.40",
12
+ "@naisys/common-node": "3.0.0-beta.40",
13
+ "@naisys/hub-database": "3.0.0-beta.40",
14
+ "@naisys/hub-protocol": "3.0.0-beta.40",
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.38"
27
+ "@naisys/supervisor": "3.0.0-beta.40"
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.38",
193
- "resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.38.tgz",
194
- "integrity": "sha512-YJIawcV12XNgzQz/QcM2oAYhu4O4V55tHm0JZrEdmBt2GS33VsGIVJqhDtg3sVWvtsDfh8QkK7EjwPEvgj7fzA==",
192
+ "version": "3.0.0-beta.40",
193
+ "resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.40.tgz",
194
+ "integrity": "sha512-qHXg14eU+DBz97iFoWdkx1Bn6Y6eminvB0Gbsu7u0iIFEDVXXUqnq4o0qBobHoS8g7OBTqTHtyiRkuECQIwhEQ==",
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.38",
202
- "resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.38.tgz",
203
- "integrity": "sha512-dNVvY+gWzBx4I+3chvNl74MgrcxVBFpPB+0acYSl7bcINvhqmbqLMYryR4AwOvU+dc09GDbpgugE6EWIQnn/zA==",
201
+ "version": "3.0.0-beta.40",
202
+ "resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.40.tgz",
203
+ "integrity": "sha512-+ACXA1oBPv/2YFaw1zyKMMZPUPtpSRntMwR0PBxz1DhXxTV5IgWjRHusxqr0Eh+fd17HqrpRvBw1Mp86kwAXyQ==",
204
204
  "dependencies": {
205
- "@naisys/common": "3.0.0-beta.38",
205
+ "@naisys/common": "3.0.0-beta.40",
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.38",
213
- "resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.38.tgz",
214
- "integrity": "sha512-h+e50Qjg389ewSKU2jAisexlmajEAK34lMz6FKWd8Hog/PI6v3BYKB/8wgL2HmI78apmHIjh1FRW3NQNF+D8rw==",
212
+ "version": "3.0.0-beta.40",
213
+ "resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.40.tgz",
214
+ "integrity": "sha512-Esavfu3plu4n1UymgyVkXpeLYNGC4UXtoS+QNnkvjVjZM7IGuFG5iDz37P6GFOAZA9Ir6sImES1iz0uK8D0Kxg==",
215
215
  "dependencies": {
216
- "@naisys/common": "3.0.0-beta.38",
217
- "@naisys/common-node": "3.0.0-beta.38",
216
+ "@naisys/common": "3.0.0-beta.40",
217
+ "@naisys/common-node": "3.0.0-beta.40",
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.38",
226
- "resolved": "https://registry.npmjs.org/@naisys/hub-protocol/-/hub-protocol-3.0.0-beta.38.tgz",
227
- "integrity": "sha512-HLNcDmbdQVDeOxQlohoSrQdz/EXvNKx16BZVEovlwixuGCfINgweOkjtr4qypHO0peHPD0p1ayBa2QAjRrSY5Q==",
225
+ "version": "3.0.0-beta.40",
226
+ "resolved": "https://registry.npmjs.org/@naisys/hub-protocol/-/hub-protocol-3.0.0-beta.40.tgz",
227
+ "integrity": "sha512-JlCOwypcAm8YbxZTVwszXPkaa89j3bJpJZi7uc5Cd+oXLbbJmCS1KpVgjQwdzU9TpacX8D0FkPk6gsoRT6fi5w==",
228
228
  "dependencies": {
229
- "@naisys/common": "3.0.0-beta.38",
229
+ "@naisys/common": "3.0.0-beta.40",
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.38",
3
+ "version": "3.0.0-beta.40",
4
4
  "description": "NAISYS Hub - Adds persistence and multi-instance coordination to NAISYS",
5
5
  "type": "module",
6
6
  "main": "dist/naisysHub.js",
@@ -17,6 +17,7 @@
17
17
  "dev": "tsx watch src/naisysHub.ts",
18
18
  "start": "node dist/naisysHub.js",
19
19
  "build": "tsc",
20
+ "test": "vitest run",
20
21
  "type-check": "tsc --noEmit",
21
22
  "npm:publish:dryrun": "npm publish --dry-run",
22
23
  "npm:publish": "npm publish --access public"
@@ -31,7 +32,7 @@
31
32
  "!dist/**/*.d.ts.map"
32
33
  ],
33
34
  "peerDependencies": {
34
- "@naisys/supervisor": "3.0.0-beta.38"
35
+ "@naisys/supervisor": "3.0.0-beta.40"
35
36
  },
36
37
  "peerDependenciesMeta": {
37
38
  "@naisys/supervisor": {
@@ -39,10 +40,10 @@
39
40
  }
40
41
  },
41
42
  "dependencies": {
42
- "@naisys/common": "3.0.0-beta.38",
43
- "@naisys/common-node": "3.0.0-beta.38",
44
- "@naisys/hub-database": "3.0.0-beta.38",
45
- "@naisys/hub-protocol": "3.0.0-beta.38",
43
+ "@naisys/common": "3.0.0-beta.40",
44
+ "@naisys/common-node": "3.0.0-beta.40",
45
+ "@naisys/hub-database": "3.0.0-beta.40",
46
+ "@naisys/hub-protocol": "3.0.0-beta.40",
46
47
  "commander": "^14.0.3",
47
48
  "dotenv": "^17.3.1",
48
49
  "fastify": "^5.8.2",
@@ -51,7 +52,8 @@
51
52
  "devDependencies": {
52
53
  "@types/node": "^25.5.0",
53
54
  "tsx": "^4.21.0",
54
- "typescript": "^5.9.3"
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^4.1.0"
55
57
  },
56
58
  "engines": {
57
59
  "node": ">=22.0.0"