@lensmcp/cluster 1.16.13 → 1.16.15

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.
@@ -1 +1 @@
1
- {"version":3,"file":"basic-ssl.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/basic-ssl.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,YAAY,yBAAyB,CAAC;AAKnD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAgB,EAAE,OAAO,GAAE,MAAM,EAAO,GAAG,MAAM,CAoCzG;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMrD"}
1
+ {"version":3,"file":"basic-ssl.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/basic-ssl.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,YAAY,yBAAyB,CAAC;AAKnD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAgB,EAAE,OAAO,GAAE,MAAM,EAAO,GAAG,MAAM,CA0CzG;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMrD"}
package/basic-ssl.js CHANGED
@@ -35,6 +35,15 @@ exports.CA_CERT_FILE = 'lensmcp-local-ca.crt';
35
35
  const LEGACY_CA_CERT_FILE = 'davnx-local-ca.crt';
36
36
  const CA_KEY_FILE = '_ca-key.pem';
37
37
  function getCertificateSync(cacheDir, name = 'lensmcp.dev', domains = []) {
38
+ // Resolve the CA FIRST: loadOrCreateCa mints a fresh CA (and sweeps every OLD-CA leaf) whenever the CA
39
+ // cert is missing/expired — e.g. after a rebrand renamed the CA file. Doing this BEFORE the leaf-cache read
40
+ // is what makes a CA change actually take effect: otherwise a stale, old-CA-signed leaf short-circuits the
41
+ // mint and the rebrand never applies (the bug that needed a manual cache clear). On a normal run the CA is
42
+ // unchanged, nothing is swept, and the SAN-keyed leaf cache below still fast-paths.
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
44
+ const forge = require('node-forge');
45
+ fs.mkdirSync(cacheDir, { recursive: true });
46
+ const ca = loadOrCreateCa(cacheDir, forge);
38
47
  // The SAN set is part of the cache key: adding a gateway hostname must
39
48
  // mint a fresh leaf, not serve a cached one that lacks the new SAN.
40
49
  const sanKey = crypto
@@ -46,16 +55,12 @@ function getCertificateSync(cacheDir, name = 'lensmcp.dev', domains = []) {
46
55
  try {
47
56
  const stat = fs.statSync(leafPath);
48
57
  if (Date.now() - stat.ctimeMs < LEAF_MAX_AGE_MS) {
49
- return fs.readFileSync(leafPath, 'utf8');
58
+ return fs.readFileSync(leafPath, 'utf8'); // reached only when the CA was NOT just re-minted (a mint sweeps leaves)
50
59
  }
51
60
  }
52
61
  catch {
53
62
  /* no cache yet */
54
63
  }
55
- // eslint-disable-next-line @typescript-eslint/no-require-imports
56
- const forge = require('node-forge');
57
- fs.mkdirSync(cacheDir, { recursive: true });
58
- const ca = loadOrCreateCa(cacheDir, forge);
59
64
  const pem = createLeafCertificate(forge, ca, name, domains);
60
65
  fs.writeFileSync(leafPath, pem);
61
66
  // Stale leaves and the pre-1.3.4 self-signed cache just rot here — sweep
@@ -1 +1 @@
1
- {"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAQnF,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAM7E,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC9E,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CA8MvH"}
1
+ {"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAQnF,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAM7E,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC9E,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CAkNvH"}
@@ -206,6 +206,9 @@ function createLensChildren(rt, obs, options) {
206
206
  const eventFile = path.join(fragment.root, '.lensmcp', 'events.jsonl'); // the registered workspace's OWN bus
207
207
  removedLabels.delete(label); // a fresh registration after a prior unregister → allow auto-heal again
208
208
  rt.registry.appendRoutes(fragment.wsKey, [{ host: lensHost, prefix: lensBase, project: observability_1.LENS_DASHBOARD_PROJECT, target: `http://localhost:${port}` }]);
209
+ // Publish the hosted workspace to the GLOBAL chooser registry (keyed on the DAEMON's pid — the daemon is
210
+ // what keeps it live), so `lensmcp.local/` lists it. Reaped on unregister below.
211
+ (0, workspace_registry_1.registerWorkspace)({ key: fragment.wsKey, root: fragment.root, base: lensBase, pid: process.pid, updatedAt: new Date().toISOString() });
209
212
  console.log(`[gateway] hosting dashboard for workspace '${fragment.wsKey}' → http://localhost:${port} (routed at https://${lensHost}${lensBase}/)`);
210
213
  emit('info', `lens dashboard up: ${lensHost}${lensBase} → :${port}`, 'cluster-lens-dashboard', { kind: 'lens-dashboard-up', host: lensHost, port, base: lensBase });
211
214
  spawnManagedChild(label, process.execPath, [bundle], {
@@ -216,6 +219,7 @@ function createLensChildren(rt, obs, options) {
216
219
  return () => {
217
220
  removedLabels.add(label); // stop auto-heal from resurrecting it
218
221
  usedDashboardPorts.delete(port);
222
+ (0, workspace_registry_1.unregisterWorkspace)(fragment.wsKey); // drop it from the chooser registry
219
223
  try {
220
224
  labeledChildren.get(label)?.kill('SIGTERM');
221
225
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAapE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA4CrD;;sGAEsG;AACtG,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,oBAAoB,CAAC,EACpG,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,gBAAgB,GACpB,kBAAkB,GAAG,SAAS,GAAG,IAAI,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAMT;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CA+PvF"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAapE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA4CrD;;sGAEsG;AACtG,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,oBAAoB,CAAC,EACpG,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,gBAAgB,GACpB,kBAAkB,GAAG,SAAS,GAAG,IAAI,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAMT;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CAwQvF"}
@@ -166,7 +166,16 @@ function createServiceLayer(rt, obs) {
166
166
  // means env vars DO reach the pod, so the east-west key + CA propagate.
167
167
  // LENSMCP_WS_KEY (the SERVICE's owning workspace) namespaces its pod sock dir
168
168
  // ($TMPDIR/<wsKey>/<service>-devserver) — the namespaced pool the daemon scans for this fragment.
169
- env: { ...process.env, NX_DAEMON: 'false', ...(svc.wsKey ? { LENSMCP_WS_KEY: svc.wsKey } : {}), ...eastWestEnv(rt, svc.project) },
169
+ // LENSMCP_EVENT_FILE points a REGISTERED workspace's pods at ITS OWN bus (<root>/.lensmcp/events.jsonl),
170
+ // so its dashboard shows its live traces, not the daemon's. The daemon's own services keep rt.eventFile
171
+ // exactly (svc.root === rt.root), so single-workspace behavior is unchanged.
172
+ env: {
173
+ ...process.env,
174
+ NX_DAEMON: 'false',
175
+ ...(svc.wsKey ? { LENSMCP_WS_KEY: svc.wsKey } : {}),
176
+ LENSMCP_EVENT_FILE: svc.wsKey === rt.wsKey ? rt.eventFile : path.join(svc.root, '.lensmcp', 'events.jsonl'),
177
+ ...eastWestEnv(rt, svc.project),
178
+ },
170
179
  stdio: ['ignore', 'pipe', 'pipe'],
171
180
  });
172
181
  svc.lastErrors = [];
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAsB,MAAM,SAAS,CAAC;AAkDxH,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAqNxB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAsB,MAAM,SAAS,CAAC;AAkDxH,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CA0OxB"}
@@ -140,6 +140,10 @@ async function startGateway(options, context) {
140
140
  // REGISTERS its fragment here instead of fighting for :443, and the daemon HOSTS its services (spawns
141
141
  // them, model A) + its dashboard (planning/multi-workspace-gateway.md P3). Harmless for a lone gateway.
142
142
  const dashboardReapers = new Map();
143
+ // Assigned after the TLS servers are built (below) — re-mints the gateway cert with the CURRENT route
144
+ // hosts and hot-swaps it, so a workspace that registers AFTER boot gets its hostnames into the cert SANs
145
+ // (otherwise the browser sees ERR_CERT_COMMON_NAME_INVALID for the new workspace's hosts).
146
+ let refreshCert = () => { };
143
147
  let controlServer;
144
148
  try {
145
149
  controlServer = (0, control_1.startControlServer)({
@@ -151,10 +155,12 @@ async function startGateway(options, context) {
151
155
  const reap = lens.hostWorkspaceDashboard(fragment);
152
156
  if (reap)
153
157
  dashboardReapers.set(fragment.wsKey, reap);
158
+ refreshCert(); // the new workspace's hosts must join the gateway cert SANs
154
159
  },
155
160
  onUnregister: (fragment) => {
156
161
  dashboardReapers.get(fragment.wsKey)?.();
157
162
  dashboardReapers.delete(fragment.wsKey);
163
+ refreshCert();
158
164
  },
159
165
  });
160
166
  }
@@ -214,12 +220,15 @@ async function startGateway(options, context) {
214
220
  // --- TLS + listeners --------------------------------------------------------
215
221
  let pem;
216
222
  let caPath;
223
+ let mintCert;
217
224
  if (https) {
218
225
  // eslint-disable-next-line @typescript-eslint/no-require-imports
219
226
  const basicSsl = require('../../../basic-ssl');
220
227
  const cacheDir = (0, service_keys_1.gatewayCacheDir)(context.root);
221
- const hosts = routes.map((r) => r.host).filter((h) => !!h);
222
- pem = basicSsl.getCertificateSync(cacheDir, 'gateway', hosts);
228
+ // SANs = the CURRENT route hosts. `rt.routes` is rebuilt live on register, so a re-mint (refreshCert
229
+ // below) automatically includes a newly-registered workspace's hostnames.
230
+ mintCert = () => basicSsl.getCertificateSync(cacheDir, 'gateway', rt.routes.map((r) => r.host).filter((h) => !!h));
231
+ pem = mintCert();
223
232
  caPath = basicSsl.caCertPath(cacheDir);
224
233
  }
225
234
  const servers = [];
@@ -253,6 +262,18 @@ async function startGateway(options, context) {
253
262
  });
254
263
  servers.push(server);
255
264
  }
265
+ // Now that the TLS servers exist, wire the live cert refresh: on a workspace register/unregister the
266
+ // control endpoint calls this to re-mint the gateway cert with the CURRENT route hosts and hot-swap it
267
+ // into every HTTP/2 secure server (setSecureContext), so a dynamically-registered workspace's hostnames
268
+ // are covered (no ERR_CERT_COMMON_NAME_INVALID).
269
+ if (mintCert) {
270
+ refreshCert = () => {
271
+ const fresh = mintCert();
272
+ for (const s of servers) {
273
+ s.setSecureContext?.({ key: fresh, cert: fresh });
274
+ }
275
+ };
276
+ }
256
277
  console.log('[gateway] Routes:');
257
278
  for (const r of routes) {
258
279
  const dest = r.pool ? `pods@${r.pool.dir}` : r.target;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.16.13",
3
+ "version": "1.16.15",
4
4
  "description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -65,8 +65,8 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@lensmcp/node-instrumentation": "1.16.13",
69
- "@lensmcp/nx-plugin": "1.16.13",
68
+ "@lensmcp/node-instrumentation": "1.16.15",
69
+ "@lensmcp/nx-plugin": "1.16.15",
70
70
  "fork-ts-checker-webpack-plugin": "^9.0.0",
71
71
  "glob": "^11.0.0",
72
72
  "http-proxy": "^1.18.0",