@camstack/server 1.2.483 → 1.2.484

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -1,13 +1,9 @@
1
1
  # CamStack Server Environment
2
2
  # Copy to .env and customize. Loaded via Node.js --env-file flag.
3
3
 
4
- # Fixed JWT secret (prevents token invalidation on server restart)
5
- # Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
6
- CAMSTACK_JWT_SECRET=
7
-
8
- # Admin credentials
9
- CAMSTACK_ADMIN_USER=admin
10
- CAMSTACK_ADMIN_PASS=changeme
4
+ # No secret is configured through the environment (D797): the session signing
5
+ # key and the cluster secret live in the hub's keychain, and a fresh hub seeds
6
+ # the admin account admin / changeme (the first sign-in forces a change).
11
7
 
12
8
  # Data directory (default: camstack-data)
13
9
  # CAMSTACK_DATA=camstack-data
@@ -73,7 +73,8 @@ function ensurePersistedNodeId(configPath, dataDir) {
73
73
  raw.nodeId = id;
74
74
  try {
75
75
  fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
76
- fs.writeFileSync(resolvedPath, JSON.stringify(raw, null, 2), 'utf-8');
76
+ // 0600: the same file holds the cluster secret.
77
+ fs.writeFileSync(resolvedPath, JSON.stringify(raw, null, 2), { encoding: 'utf-8', mode: 0o600 });
77
78
  }
78
79
  catch {
79
80
  /* best-effort */
@@ -89,9 +90,10 @@ function loadAgentConfig(configPath, dataDirOverride) {
89
90
  const envName = process.env.CAMSTACK_NODE_ID ??
90
91
  process.env.CAMSTACK_AGENT_NAME ??
91
92
  `${os.hostname()}-${os.arch()}`;
92
- // Environment variables for hub connection (optional — agent starts without)
93
+ // Environment variable for the hub connection (optional — agent starts
94
+ // without). The cluster secret is NOT one of them (D797): it lives only in
95
+ // agent.json.
93
96
  const envHubAddress = process.env.CAMSTACK_HUB_ADDRESS || undefined;
94
- const envSecret = process.env.CAMSTACK_CLUSTER_SECRET || undefined;
95
97
  const configFullPath = path.resolve(resolvedDataDir, filePath);
96
98
  const nodeId = ensurePersistedNodeId(filePath, resolvedDataDir);
97
99
  // Read persisted config — these override env vars
@@ -112,7 +114,7 @@ function loadAgentConfig(configPath, dataDirOverride) {
112
114
  // Config file wins over env for all user-editable fields
113
115
  const effectiveName = fileName ?? envName;
114
116
  const effectiveHub = fileHubAddress ?? envHubAddress;
115
- const effectiveSecret = fileSecret ?? envSecret;
117
+ const effectiveSecret = fileSecret;
116
118
  return {
117
119
  nodeId,
118
120
  name: effectiveName,
@@ -151,9 +151,11 @@ function readConfigFile(configPath) {
151
151
  return {};
152
152
  }
153
153
  }
154
+ /** agent.json holds the cluster secret (D797): always 0600. */
154
155
  function writeConfigFile(configPath, data) {
155
156
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
156
- fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf-8');
157
+ fs.writeFileSync(configPath, JSON.stringify(data, null, 2), { encoding: 'utf-8', mode: 0o600 });
158
+ fs.chmodSync(configPath, 0o600);
157
159
  }
158
160
  /** Live Moleculer registry snapshot — reused by agent-bootstrap's Option A. */
159
161
  function getRegistryNodes(broker) {
@@ -149,7 +149,6 @@ async function startAgent(configPath) {
149
149
  mode: 'agent',
150
150
  hubAddress: config.hubAddress,
151
151
  logLevel: config.logLevel,
152
- secret: config.secret,
153
152
  });
154
153
  /**
155
154
  * Reconnect: stop the current broker, reload config from file,
@@ -184,7 +183,6 @@ async function startAgent(configPath) {
184
183
  mode: 'agent',
185
184
  hubAddress: fresh.hubAddress,
186
185
  logLevel: fresh.logLevel,
187
- secret: fresh.secret,
188
186
  });
189
187
  await broker.start();
190
188
  console.log('[Agent] Reconnected successfully');
@@ -327,7 +325,7 @@ async function startAgent(configPath) {
327
325
  })
328
326
  .catch((err) => {
329
327
  if ((0, system_1.isClusterSecretMismatchError)(err)) {
330
- consoleLogger.error('hub registration rejected: cluster secret mismatch — correct CAMSTACK_CLUSTER_SECRET and restart the agent');
328
+ consoleLogger.error('hub registration rejected: cluster secret mismatch — enter the hub\'s cluster secret (System → Cluster) in this agent\'s setup (agent.json) and restart the agent');
331
329
  // Surface the mismatch in the UI status.
332
330
  hubConnectionStateOverride = 'secret-mismatch';
333
331
  return;
@@ -4522,6 +4522,15 @@ function createCapRouter_keychainAdmin(getProvider, createRemoteProxy) {
4522
4522
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4523
4523
  return p.reveal(methodInput);
4524
4524
  }),
4525
+ setSystemSecret: trpc_middleware_js_1.adminProcedure
4526
+ .input(types_62.keychainAdminCapability.methods.setSystemSecret.input.loose())
4527
+ .output(types_62.keychainAdminCapability.methods.setSystemSecret.output)
4528
+ .mutation(async ({ input, ctx }) => {
4529
+ const { nodeId, ...methodInput } = input;
4530
+ const p = resolveProvider('keychain-admin', nodeId, () => getProvider(ctx), createRemoteProxy);
4531
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4532
+ return p.setSystemSecret(methodInput);
4533
+ }),
4525
4534
  deleteOrphans: trpc_middleware_js_1.adminProcedure
4526
4535
  .input(types_62.keychainAdminCapability.methods.deleteOrphans.input.loose())
4527
4536
  .output(types_62.keychainAdminCapability.methods.deleteOrphans.output)
@@ -282,8 +282,11 @@ function makeDeviceScopeLookup(addonRegistry) {
282
282
  * Context factory for hub-internal calls originating from the trusted
283
283
  * Moleculer mesh (the `$core-caps` bridge service in `core-cap-bridge.ts`).
284
284
  *
285
- * Cluster membership is gated by `CAMSTACK_CLUSTER_SECRET`, so a
286
- * mesh-originated call is treated as a fully-trusted admin: it carries
285
+ * A mesh-originated call is treated as a fully-trusted admin. NOTE (D797):
286
+ * the cluster secret (System → Cluster) gates `$hub.registerNode` only — the
287
+ * Moleculer TCP transport itself authenticates no peer, so anything that can
288
+ * reach the hub's mesh port can call this. Keep that port off untrusted
289
+ * networks; transport-level peer authentication is on the roadmap. It carries
287
290
  * a synthetic `isAdmin` user, which makes `protectedProcedure` /
288
291
  * `adminProcedure` pass without a JWT and skips the scope-access
289
292
  * matcher entirely. There is no HTTP request behind the call.
@@ -39,10 +39,9 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.ignoredTlsCertSources = ignoredTlsCertSources;
42
+ exports.retiredSecretSources = retiredSecretSources;
42
43
  exports.loadBootstrapConfig = loadBootstrapConfig;
43
- exports.autoGenerateJwtSecret = autoGenerateJwtSecret;
44
44
  exports.setupInfra = setupInfra;
45
- const node_crypto_1 = require("node:crypto");
46
45
  const fs = __importStar(require("node:fs"));
47
46
  const path = __importStar(require("node:path"));
48
47
  const types_1 = require("@camstack/types");
@@ -54,21 +53,34 @@ const storage_location_manager_1 = require("../core/storage/storage-location-man
54
53
  // ---------------------------------------------------------------------------
55
54
  const CONFIG_DEFAULTS = {
56
55
  server: { port: 4443, host: '0.0.0.0', dataPath: 'camstack-data' },
57
- auth: {
58
- jwtSecret: null,
59
- adminUsername: 'admin',
60
- adminPassword: 'changeme',
61
- },
62
56
  };
57
+ /**
58
+ * Bootstrap env overrides. No SECRET is among them (D783, D797): the session
59
+ * signing key and the cluster secret live in the keychain, and the first-boot
60
+ * admin is fixed (`admin` / `changeme`).
61
+ */
63
62
  const ENV_VAR_MAP = {
64
63
  CAMSTACK_PORT: 'server.port',
65
64
  CAMSTACK_HOST: 'server.host',
66
65
  CAMSTACK_DATA: 'server.dataPath',
67
- CAMSTACK_JWT_SECRET: 'auth.jwtSecret',
68
- CAMSTACK_ADMIN_USER: 'auth.adminUsername',
69
- CAMSTACK_ADMIN_PASS: 'auth.adminPassword',
70
66
  CAMSTACK_TLS_ENABLED: 'tls.enabled',
71
67
  };
68
+ /**
69
+ * Env variables that once carried a secret into the hub. Ignored since D797 —
70
+ * still named so a container template that sets them is told, not silently
71
+ * obeyed or silently dropped. Only their PRESENCE is read, never the value.
72
+ */
73
+ const RETIRED_SECRET_ENV = [
74
+ 'CAMSTACK_JWT_SECRET',
75
+ 'CAMSTACK_ADMIN_USER',
76
+ 'CAMSTACK_ADMIN_PASS',
77
+ 'CAMSTACK_CLUSTER_SECRET',
78
+ 'CAMSTACK_SECRET_KEY',
79
+ 'HF_TOKEN',
80
+ 'HUGGING_FACE_HUB_TOKEN',
81
+ ];
82
+ /** `config.yaml` `auth.*` keys that once held a credential — ignored too (D797). */
83
+ const RETIRED_AUTH_KEYS = ['jwtSecret', 'adminUsername', 'adminPassword'];
72
84
  /**
73
85
  * Env variables that once set the node's certificate. Ignored since D726:
74
86
  * the admin UI (Network → Local access) is the one way to set it. Still
@@ -103,6 +115,19 @@ function ignoredTlsCertSources(env, raw) {
103
115
  const fromYaml = IGNORED_TLS_CERT_KEYS.filter((key) => tls[key] !== undefined).map((key) => `tls.${key}`);
104
116
  return [...fromEnv, ...fromYaml];
105
117
  }
118
+ /**
119
+ * Every retired secret source still present: env variables set (non-blank) and
120
+ * `config.yaml` `auth.*` keys. Names only — a value is never read into a log.
121
+ */
122
+ function retiredSecretSources(env, raw) {
123
+ const fromEnv = RETIRED_SECRET_ENV.filter((name) => {
124
+ const value = env[name];
125
+ return value !== undefined && value !== '';
126
+ });
127
+ const auth = (0, types_1.asJsonObject)(raw['auth']) ?? {};
128
+ const fromYaml = RETIRED_AUTH_KEYS.filter((key) => auth[key] !== undefined && auth[key] !== null).map((key) => `auth.${key}`);
129
+ return [...fromEnv, ...fromYaml];
130
+ }
106
131
  // ---------------------------------------------------------------------------
107
132
  // loadBootstrapConfig
108
133
  // ---------------------------------------------------------------------------
@@ -166,6 +191,13 @@ function loadBootstrapConfig(configPath) {
166
191
  raw = setNested(raw, configPath_, coerced);
167
192
  console.log(`[Phase1] Env override: ${envKey} → ${configPath_}`);
168
193
  }
194
+ const retired = retiredSecretSources(process.env, raw);
195
+ if (retired.length > 0) {
196
+ console.warn(`[Phase1] WARN ${retired.join(', ')} ignored — no secret is read from the environment or ` +
197
+ 'config.yaml any more (D797). The session signing key and the cluster secret live in the ' +
198
+ 'keychain (System → Cluster for the cluster secret); a fresh hub seeds admin / changeme. ' +
199
+ 'Remove these entries — after importing their values with scripts/ops/keychain-migrate-hub.mjs.');
200
+ }
169
201
  const ignored = ignoredTlsCertSources(process.env, raw);
170
202
  if (ignored.length > 0) {
171
203
  console.warn(`[Phase1] WARN ${ignored.join(', ')} ignored — the certificate is set only in the admin UI. ` +
@@ -174,48 +206,16 @@ function loadBootstrapConfig(configPath) {
174
206
  return config_schema_1.bootstrapSchema.parse(raw);
175
207
  }
176
208
  // ---------------------------------------------------------------------------
177
- // autoGenerateJwtSecret
178
- // ---------------------------------------------------------------------------
179
- /**
180
- * If `jwtSecret` is null in the parsed config, generate a random secret,
181
- * persist it to the YAML file, and return an updated config.
182
- */
183
- function autoGenerateJwtSecret(configPath, bootstrapConfig) {
184
- if (bootstrapConfig.auth.jwtSecret !== null) {
185
- return bootstrapConfig;
186
- }
187
- const secret = (0, node_crypto_1.randomBytes)(32).toString('hex');
188
- console.log('[Phase1] jwtSecret is null — auto-generating and writing to config.yaml');
189
- let raw = {};
190
- if (fs.existsSync(configPath)) {
191
- raw = (0, types_1.asJsonObject)(yaml.load(fs.readFileSync(configPath, 'utf-8'))) ?? {};
192
- }
193
- const authSection = (0, types_1.asJsonObject)(raw.auth) ?? {};
194
- raw.auth = { ...authSection, jwtSecret: secret };
195
- const tmpPath = `${configPath}.tmp`;
196
- try {
197
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
198
- fs.writeFileSync(tmpPath, yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }), 'utf-8');
199
- fs.renameSync(tmpPath, configPath);
200
- }
201
- catch (err) {
202
- console.warn('[Phase1] Could not write auto-generated jwtSecret to config.yaml:', err);
203
- }
204
- return {
205
- ...bootstrapConfig,
206
- auth: { ...bootstrapConfig.auth, jwtSecret: secret },
207
- };
208
- }
209
- // ---------------------------------------------------------------------------
210
209
  // setupInfra
211
210
  // ---------------------------------------------------------------------------
212
211
  /**
213
212
  * Phase 2: Create StorageLocationManager, ensure directories, configure TLS.
214
213
  * Returns the full InfraContext needed by subsequent boot phases.
215
214
  */
216
- async function setupInfra(configPath, bootstrapConfig) {
217
- // Auto-generate jwtSecret if not set
218
- const config = autoGenerateJwtSecret(configPath, bootstrapConfig);
215
+ async function setupInfra(_configPath, bootstrapConfig) {
216
+ // No secret is generated here: the session signing key is the keychain's
217
+ // (local-auth loads or creates it once the keychain is up — D797).
218
+ const config = bootstrapConfig;
219
219
  const dataPath = path.resolve(config.server.dataPath);
220
220
  const port = config.server.port;
221
221
  const host = config.server.host;
@@ -85,13 +85,6 @@ class MoleculerService {
85
85
  * hub consumers share the same snapshot.
86
86
  */
87
87
  readinessRegistry;
88
- /**
89
- * Resolved cluster secret (`CAMSTACK_CLUSTER_SECRET` env, else
90
- * `cluster.secret` config), or `undefined` when none is configured.
91
- * Threaded both into the broker factory and the hub's
92
- * `expectedClusterSecretHash` so `$hub.registerNode` can gate on it.
93
- */
94
- clusterSecret;
95
88
  /**
96
89
  * UDS server that listens for addon-runners spawned by this hub node.
97
90
  * `null` when the UDS server failed to start (children run broker-only).
@@ -193,13 +186,14 @@ class MoleculerService {
193
186
  // moleculer→eventemitter2 whose types are unresolvable at this
194
187
  // boundary, so the inference falls to `error` and trips
195
188
  // `no-unsafe-assignment`. Going via `unknown` documents the boundary.
196
- this.clusterSecret =
197
- process.env['CAMSTACK_CLUSTER_SECRET'] ?? this.config.get('cluster.secret');
189
+ // No cluster secret here (D797): the broker is created before the keychain
190
+ // can answer, and the Moleculer namespace was never authentication (TCP
191
+ // ignores it; UDP discovery broadcasts it in clear). The secret gates
192
+ // `$hub.registerNode`, read from the keychain at call time.
198
193
  const broker = (0, system_1.createBroker)({
199
194
  nodeID: 'hub',
200
195
  mode: 'hub',
201
196
  logLevel: this.config.get('moleculer.logLevel') ?? 'warn',
202
- secret: this.clusterSecret,
203
197
  ...(tcpPort && !Number.isNaN(tcpPort) ? { tcpPort } : {}),
204
198
  ...(udpPort && !Number.isNaN(udpPort) ? { udpPort } : {}),
205
199
  });
@@ -274,9 +268,8 @@ class MoleculerService {
274
268
  onUnregisterNode: (nodeId) => {
275
269
  this.removeNodeFromRegistry(nodeId);
276
270
  },
277
- expectedClusterSecretHash: this.clusterSecret
278
- ? (0, system_1.hashClusterSecret)(this.clusterSecret)
279
- : undefined,
271
+ // System → Cluster, from the keychain (D797): off / on / unknown.
272
+ clusterSecretGate: system_1.getProcessClusterSecretGate,
280
273
  };
281
274
  const hubService = (0, system_1.createHubService)(hubDeps);
282
275
  this.brokerSafe.createService(hubService);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.483",
3
+ "version": "1.2.484",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -37,18 +37,18 @@
37
37
  ]
38
38
  },
39
39
  "dependencies": {
40
- "@camstack/addon-admin-ui": "1.2.356",
41
- "@camstack/addon-agent-ui": "1.2.193",
40
+ "@camstack/addon-admin-ui": "1.2.357",
41
+ "@camstack/addon-agent-ui": "1.2.194",
42
42
  "@camstack/addon-auth": "1.2.132",
43
43
  "@camstack/addon-decoder-nodeav": "1.2.121",
44
44
  "@camstack/addon-notifiers": "1.2.128",
45
45
  "@camstack/addon-pipeline": "1.2.346",
46
46
  "@camstack/addon-pipeline-orchestrator": "1.2.298",
47
47
  "@camstack/addon-post-analysis": "1.2.345",
48
- "@camstack/sdk": "1.2.126",
49
- "@camstack/system": "1.2.378",
50
- "@camstack/types": "1.2.329",
51
- "@camstack/ui-library": "1.2.251",
48
+ "@camstack/sdk": "1.2.127",
49
+ "@camstack/system": "1.2.379",
50
+ "@camstack/types": "1.2.330",
51
+ "@camstack/ui-library": "1.2.252",
52
52
  "@fastify/compress": "^9.0.0",
53
53
  "@fastify/cookie": "^11.0.2",
54
54
  "@fastify/cors": "^11.2.0",