@lensmcp/cluster 1.16.12 → 1.16.13
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/basic-ssl.d.ts +1 -1
- package/basic-ssl.d.ts.map +1 -1
- package/basic-ssl.js +19 -6
- package/executors/gateway/runtime/control.d.ts +20 -0
- package/executors/gateway/runtime/control.d.ts.map +1 -0
- package/executors/gateway/runtime/control.js +115 -0
- package/executors/gateway/runtime/discovery.js +1 -1
- package/executors/gateway/runtime/lens-children.d.ts +5 -0
- package/executors/gateway/runtime/lens-children.d.ts.map +1 -1
- package/executors/gateway/runtime/lens-children.js +53 -6
- package/executors/gateway/runtime/lifecycle.d.ts.map +1 -1
- package/executors/gateway/runtime/lifecycle.js +6 -4
- package/executors/gateway/runtime/route-registry.d.ts +38 -0
- package/executors/gateway/runtime/route-registry.d.ts.map +1 -0
- package/executors/gateway/runtime/route-registry.js +72 -0
- package/executors/gateway/runtime/server.d.ts.map +1 -1
- package/executors/gateway/runtime/server.js +41 -2
- package/executors/gateway/runtime/types.d.ts +15 -0
- package/executors/gateway/runtime/types.d.ts.map +1 -1
- package/executors/trust/trust.impl.js +1 -1
- package/main.devserver.js +1 -1
- package/package.json +3 -3
package/basic-ssl.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CA_CERT_FILE = "
|
|
1
|
+
export declare const CA_CERT_FILE = "lensmcp-local-ca.crt";
|
|
2
2
|
export declare function getCertificateSync(cacheDir: string, name?: string, domains?: string[]): string;
|
|
3
3
|
/** Absolute path of the trust-once CA certificate inside `cacheDir`. */
|
|
4
4
|
export declare function caCertPath(cacheDir: string): string;
|
package/basic-ssl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"basic-ssl.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/basic-ssl.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,YAAY,
|
|
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"}
|
package/basic-ssl.js
CHANGED
|
@@ -20,7 +20,7 @@ const tslib_1 = require("tslib");
|
|
|
20
20
|
*
|
|
21
21
|
* Trust once (macOS):
|
|
22
22
|
* sudo security add-trusted-cert -d -r trustRoot \
|
|
23
|
-
* -k /Library/Keychains/System.keychain <cacheDir>/
|
|
23
|
+
* -k /Library/Keychains/System.keychain <cacheDir>/lensmcp-local-ca.crt
|
|
24
24
|
*/
|
|
25
25
|
const crypto = tslib_1.__importStar(require("node:crypto"));
|
|
26
26
|
const fs = tslib_1.__importStar(require("node:fs"));
|
|
@@ -30,9 +30,11 @@ const LEAF_MAX_AGE_MS = 25 * 24 * 60 * 60 * 1000;
|
|
|
30
30
|
/** CA lifetime (~10 years) and the remaining-validity floor for reuse. */
|
|
31
31
|
const CA_TTL_DAYS = 3650;
|
|
32
32
|
const CA_MIN_REMAINING_MS = 30 * 24 * 60 * 60 * 1000;
|
|
33
|
-
exports.CA_CERT_FILE = '
|
|
33
|
+
exports.CA_CERT_FILE = 'lensmcp-local-ca.crt';
|
|
34
|
+
/** The legacy (pre-rebrand) CA filename — swept on mint so a stale `davnx-local-ca.crt` never lingers. */
|
|
35
|
+
const LEGACY_CA_CERT_FILE = 'davnx-local-ca.crt';
|
|
34
36
|
const CA_KEY_FILE = '_ca-key.pem';
|
|
35
|
-
function getCertificateSync(cacheDir, name = '
|
|
37
|
+
function getCertificateSync(cacheDir, name = 'lensmcp.dev', domains = []) {
|
|
36
38
|
// The SAN set is part of the cache key: adding a gateway hostname must
|
|
37
39
|
// mint a fresh leaf, not serve a cached one that lacks the new SAN.
|
|
38
40
|
const sanKey = crypto
|
|
@@ -104,8 +106,8 @@ function loadOrCreateCa(cacheDir, forge) {
|
|
|
104
106
|
cert.validity.notAfter = new Date();
|
|
105
107
|
cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + CA_TTL_DAYS);
|
|
106
108
|
const attrs = [
|
|
107
|
-
{ name: 'commonName', value: '
|
|
108
|
-
{ name: 'organizationName', value: '
|
|
109
|
+
{ name: 'commonName', value: 'lensmcp local dev CA' },
|
|
110
|
+
{ name: 'organizationName', value: 'lensmcp' },
|
|
109
111
|
{ shortName: 'OU', value: 'dev' },
|
|
110
112
|
];
|
|
111
113
|
cert.setSubject(attrs);
|
|
@@ -118,6 +120,17 @@ function loadOrCreateCa(cacheDir, forge) {
|
|
|
118
120
|
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
119
121
|
fs.writeFileSync(certPath, forge.pki.certificateToPem(cert));
|
|
120
122
|
fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(keys.privateKey), { mode: 0o600 });
|
|
123
|
+
// A freshly-minted CA invalidates every cached LEAF (they were signed by the OLD CA) — sweep them so the
|
|
124
|
+
// next getCertificateSync re-mints against THIS CA (otherwise the SAN-keyed leaf cache serves a stale,
|
|
125
|
+
// old-CA-signed cert). Also drop the legacy `davnx-local-ca.crt` so the lensmcp rebrand leaves nothing behind.
|
|
126
|
+
for (const f of fs.readdirSync(cacheDir)) {
|
|
127
|
+
if (/^_leaf-.*\.pem$/.test(f) || f === LEGACY_CA_CERT_FILE) {
|
|
128
|
+
try {
|
|
129
|
+
fs.unlinkSync(path.join(cacheDir, f));
|
|
130
|
+
}
|
|
131
|
+
catch { /* best effort */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
121
134
|
return { cert, key: keys.privateKey };
|
|
122
135
|
}
|
|
123
136
|
function createLeafCertificate(forge, ca, name, domains, ttlDays = 30) {
|
|
@@ -130,7 +143,7 @@ function createLeafCertificate(forge, ca, name, domains, ttlDays = 30) {
|
|
|
130
143
|
cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + ttlDays);
|
|
131
144
|
cert.setSubject([
|
|
132
145
|
{ name: 'commonName', value: name },
|
|
133
|
-
{ name: 'organizationName', value: '
|
|
146
|
+
{ name: 'organizationName', value: 'lensmcp' },
|
|
134
147
|
{ shortName: 'OU', value: 'dev' },
|
|
135
148
|
]);
|
|
136
149
|
cert.setIssuer(ca.cert.subject.attributes);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GatewayRuntime } from './types';
|
|
2
|
+
import type { WorkspaceFragment } from './route-registry';
|
|
3
|
+
/** `~/.lensmcp/control.sock` — the well-known daemon control socket (override the home via `LENSMCP_HOME`). */
|
|
4
|
+
export declare function controlSocketPath(): string;
|
|
5
|
+
export interface ControlServerDeps {
|
|
6
|
+
rt: GatewayRuntime;
|
|
7
|
+
/** Reap a fragment's spawned devservers when it unregisters (the svcLayer's `killSpawned`). */
|
|
8
|
+
killService: (svc: WorkspaceFragment['services'][number], reason: string) => void;
|
|
9
|
+
/** Hooks so the daemon can host/tear-down a workspace's dashboard around the route merge (P3d). */
|
|
10
|
+
onRegister?: (fragment: WorkspaceFragment) => void;
|
|
11
|
+
onUnregister?: (fragment: WorkspaceFragment) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface ControlServer {
|
|
14
|
+
socketPath: string;
|
|
15
|
+
close: () => void;
|
|
16
|
+
}
|
|
17
|
+
/** Start the control listener. The socket is loopback-only (a filesystem socket), so no IP allow-listing
|
|
18
|
+
* is needed — anything that can open it is already on this machine as this user. */
|
|
19
|
+
export declare function startControlServer(deps: ControlServerDeps): ControlServer;
|
|
20
|
+
//# sourceMappingURL=control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/control.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,+GAA+G;AAC/G,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,cAAc,CAAC;IACnB,+FAA+F;IAC/F,WAAW,EAAE,CAAC,GAAG,EAAE,iBAAiB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClF,mGAAmG;IACnG,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnD,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAsBD;qFACqF;AACrF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CA2DzE"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.controlSocketPath = controlSocketPath;
|
|
4
|
+
exports.startControlServer = startControlServer;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
/**
|
|
7
|
+
* The daemon CONTROL PLANE (planning/multi-workspace-gateway.md P3, model A). A UNIX-socket HTTP listener
|
|
8
|
+
* at `~/.lensmcp/control.sock` — loopback-only by nature (a filesystem socket, no TCP), so a second
|
|
9
|
+
* workspace's `gateway start` (P4) REGISTERS its fragment into the running `:443` daemon instead of
|
|
10
|
+
* fighting for the port. The daemon reconstructs the fragment via `discoverRoutes` on the second
|
|
11
|
+
* workspace's root (so it owns the pool controllers + spawns those devservers itself — model A), merges
|
|
12
|
+
* the routes live, and reaps them on unregister.
|
|
13
|
+
*
|
|
14
|
+
* POST /register { wsKey, root, projects } → discover + register + rebuild
|
|
15
|
+
* POST /unregister { wsKey } → unregister + reap its services + rebuild
|
|
16
|
+
* GET /list → the registered workspaces
|
|
17
|
+
*/
|
|
18
|
+
const fs = tslib_1.__importStar(require("node:fs"));
|
|
19
|
+
const http = tslib_1.__importStar(require("node:http"));
|
|
20
|
+
const os = tslib_1.__importStar(require("node:os"));
|
|
21
|
+
const path = tslib_1.__importStar(require("node:path"));
|
|
22
|
+
const discovery_1 = require("./discovery");
|
|
23
|
+
/** `~/.lensmcp/control.sock` — the well-known daemon control socket (override the home via `LENSMCP_HOME`). */
|
|
24
|
+
function controlSocketPath() {
|
|
25
|
+
const home = process.env['LENSMCP_HOME'] || os.homedir();
|
|
26
|
+
return path.join(home, '.lensmcp', 'control.sock');
|
|
27
|
+
}
|
|
28
|
+
function readJsonBody(req) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let raw = '';
|
|
31
|
+
req.on('data', (c) => {
|
|
32
|
+
raw += c;
|
|
33
|
+
if (raw.length > 1_000_000)
|
|
34
|
+
reject(new Error('body too large')); // bound the loopback input
|
|
35
|
+
});
|
|
36
|
+
req.on('end', () => {
|
|
37
|
+
try {
|
|
38
|
+
resolve(raw ? JSON.parse(raw) : {});
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
reject(e);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
req.on('error', reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Start the control listener. The socket is loopback-only (a filesystem socket), so no IP allow-listing
|
|
48
|
+
* is needed — anything that can open it is already on this machine as this user. */
|
|
49
|
+
function startControlServer(deps) {
|
|
50
|
+
const { rt } = deps;
|
|
51
|
+
const socketPath = controlSocketPath();
|
|
52
|
+
fs.mkdirSync(path.dirname(socketPath), { recursive: true });
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(socketPath);
|
|
55
|
+
}
|
|
56
|
+
catch { /* no stale socket to clear */ }
|
|
57
|
+
const send = (res, code, body) => {
|
|
58
|
+
res.writeHead(code, { 'content-type': 'application/json' });
|
|
59
|
+
res.end(JSON.stringify(body));
|
|
60
|
+
};
|
|
61
|
+
const server = http.createServer((req, res) => {
|
|
62
|
+
void (async () => {
|
|
63
|
+
try {
|
|
64
|
+
const url = (req.url ?? '').split('?')[0];
|
|
65
|
+
if (req.method === 'GET' && url === '/list') {
|
|
66
|
+
return send(res, 200, {
|
|
67
|
+
workspaces: rt.registry.keys().map((key) => {
|
|
68
|
+
const f = rt.registry.get(key);
|
|
69
|
+
return { key, root: f?.root, routes: f?.routes.length ?? 0, services: f?.services.length ?? 0 };
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (req.method === 'POST' && url === '/register') {
|
|
74
|
+
const body = (await readJsonBody(req));
|
|
75
|
+
if (!body.wsKey || !body.root)
|
|
76
|
+
return send(res, 400, { error: 'wsKey and root are required' });
|
|
77
|
+
const { routes, services } = (0, discovery_1.discoverRoutes)(body.root, body.projects ?? {}, undefined, body.wsKey);
|
|
78
|
+
const fragment = { wsKey: body.wsKey, root: body.root, routes, services };
|
|
79
|
+
rt.registry.register(fragment);
|
|
80
|
+
deps.onRegister?.(fragment); // hosts the dashboard + APPENDS its route to the fragment BEFORE the rebuild
|
|
81
|
+
rt.rebuildRoutes();
|
|
82
|
+
return send(res, 200, { ok: true, wsKey: body.wsKey, routes: routes.length, services: services.length, workspaces: rt.registry.keys() });
|
|
83
|
+
}
|
|
84
|
+
if (req.method === 'POST' && url === '/unregister') {
|
|
85
|
+
const body = (await readJsonBody(req));
|
|
86
|
+
if (!body.wsKey)
|
|
87
|
+
return send(res, 400, { error: 'wsKey is required' });
|
|
88
|
+
const dropped = rt.registry.unregister(body.wsKey);
|
|
89
|
+
if (dropped) {
|
|
90
|
+
for (const svc of dropped.services)
|
|
91
|
+
deps.killService(svc, 'workspace unregistered');
|
|
92
|
+
deps.onUnregister?.(dropped);
|
|
93
|
+
}
|
|
94
|
+
rt.rebuildRoutes();
|
|
95
|
+
return send(res, 200, { ok: true, removed: !!dropped, workspaces: rt.registry.keys() });
|
|
96
|
+
}
|
|
97
|
+
send(res, 404, { error: 'not found' });
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
send(res, 500, { error: e.message });
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
});
|
|
104
|
+
server.listen(socketPath);
|
|
105
|
+
return {
|
|
106
|
+
socketPath,
|
|
107
|
+
close: () => {
|
|
108
|
+
server.close();
|
|
109
|
+
try {
|
|
110
|
+
fs.unlinkSync(socketPath);
|
|
111
|
+
}
|
|
112
|
+
catch { /* already gone */ }
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -75,7 +75,7 @@ wsKey = '') {
|
|
|
75
75
|
if (decl.lens)
|
|
76
76
|
console.warn(`[gateway] ${name}: cluster.lens is ignored for pod (service) decls — backends are instrumented by serve-hmr.`);
|
|
77
77
|
pool = { dir: sockDirFor(decl.service), socks: [], idx: -1, scannedAt: 0 };
|
|
78
|
-
svc = { project: name, decl, pool, state: 'down', lastUsed: Date.now(), inflight: 0, lastScaleAt: 0 };
|
|
78
|
+
svc = { project: name, decl, root: rootDir, wsKey, pool, state: 'down', lastUsed: Date.now(), inflight: 0, lastScaleAt: 0 };
|
|
79
79
|
services.push(svc);
|
|
80
80
|
}
|
|
81
81
|
else if (decl.lens) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SpawnChild } from '@lensmcp/nx-plugin/lens-frontend';
|
|
2
|
+
import type { WorkspaceFragment } from './route-registry';
|
|
2
3
|
import type { GatewayRuntime, GatewayRuntimeOptions } from './types';
|
|
3
4
|
import { type Observability } from './observability';
|
|
4
5
|
export interface LensChildren {
|
|
@@ -6,6 +7,10 @@ export interface LensChildren {
|
|
|
6
7
|
/** Boot the dashboard + MCP singletons and every `lens:true` app. Pushes the
|
|
7
8
|
* dashboard route onto `rt.routes` — MUST run before TLS SAN derivation. */
|
|
8
9
|
bootSingletonsAndApps(): void;
|
|
10
|
+
/** Host a REGISTERED workspace's dashboard (P3d): spawn its dashboard child on its OWN events file + a
|
|
11
|
+
* distinct port, append its `lensmcp.local/<key>` route to its fragment. Returns a reaper (kills the
|
|
12
|
+
* child WITHOUT auto-heal respawn) for the daemon to call on unregister — or undefined if no bundle. */
|
|
13
|
+
hostWorkspaceDashboard(fragment: WorkspaceFragment): (() => void) | undefined;
|
|
9
14
|
/** Reap all managed children (called from `stop()`). */
|
|
10
15
|
reapAll(): void;
|
|
11
16
|
}
|
|
@@ -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;
|
|
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,14 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createLensChildren = createLensChildren;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
4
5
|
/**
|
|
5
6
|
* Lens-mode managed children: the dashboard + MCP SINGLETONS (cluster-wide,
|
|
6
7
|
* owned by the gateway), and each `lens:true` app's Vite + browser capture.
|
|
7
8
|
* EVERY managed child AUTO-HEALS — restarted with exponential backoff when it
|
|
8
9
|
* exits while the gateway is up — and a dead child NEVER kills the cluster.
|
|
9
10
|
*/
|
|
11
|
+
const path = tslib_1.__importStar(require("node:path"));
|
|
10
12
|
const node_child_process_1 = require("node:child_process");
|
|
11
|
-
const manifest_1 = require("../manifest");
|
|
12
13
|
const lens_frontend_1 = require("@lensmcp/nx-plugin/lens-frontend");
|
|
13
14
|
const scope_1 = require("./scope");
|
|
14
15
|
const workspace_registry_1 = require("./workspace-registry");
|
|
@@ -31,6 +32,12 @@ function createLensChildren(rt, obs, options) {
|
|
|
31
32
|
// Per-project last-recycle time — the cooldown survives the kill→auto-heal→respawn (a new child handle).
|
|
32
33
|
const viteRecycleAt = new Map();
|
|
33
34
|
let lensSweeper;
|
|
35
|
+
// Per-workspace dashboard hosting (P3d): the current child by label, labels we've intentionally removed
|
|
36
|
+
// (so auto-heal does NOT respawn them on the reap kill), and the dashboard ports already in use (so two
|
|
37
|
+
// workspaces' dashboards get distinct ports).
|
|
38
|
+
const labeledChildren = new Map();
|
|
39
|
+
const removedLabels = new Set();
|
|
40
|
+
const usedDashboardPorts = new Set();
|
|
34
41
|
// A `SpawnChild` over the gateway's managed-children array — the SAME shape `agent-dev` passes to
|
|
35
42
|
// `spawnLensFrontend`. EVERY gateway-managed child AUTO-HEALS: the dashboard + MCP singletons AND each
|
|
36
43
|
// `lens:true` vite app are restarted when they exit while the gateway is up, with exponential backoff.
|
|
@@ -44,6 +51,7 @@ function createLensChildren(rt, obs, options) {
|
|
|
44
51
|
const spawnedAt = Date.now();
|
|
45
52
|
const child = (0, node_child_process_1.spawn)(bin, args, { cwd: rt.root, env: { ...process.env, ...(env ?? {}) }, stdio: 'inherit' });
|
|
46
53
|
lensChildren.push(child);
|
|
54
|
+
labeledChildren.set(label, child); // the CURRENT handle for this label (a per-workspace dashboard reap targets it)
|
|
47
55
|
// Track a `lens:true` app's vite dev server (label 'vite') for the zombie recycle. Its `LENSMCP_PROJECT`
|
|
48
56
|
// (set by lens-children when it spawns the frontend) names the app; a fresh respawn re-adds via this path.
|
|
49
57
|
if (label === 'vite')
|
|
@@ -53,8 +61,8 @@ function createLensChildren(rt, obs, options) {
|
|
|
53
61
|
if (idx >= 0)
|
|
54
62
|
lensChildren.splice(idx, 1); // drop the dead handle so stop() won't signal it
|
|
55
63
|
viteChildren.delete(child); // the auto-heal respawn re-registers a fresh handle with a new spawnedAt
|
|
56
|
-
if (rt.stopped())
|
|
57
|
-
return; //
|
|
64
|
+
if (rt.stopped() || removedLabels.has(label))
|
|
65
|
+
return; // shutting down, or this workspace unregistered — do NOT resurrect
|
|
58
66
|
const heal = lensHeal.get(label) ?? { attempts: 0 };
|
|
59
67
|
if (Date.now() - spawnedAt >= HEAL_HEALTHY_MS)
|
|
60
68
|
heal.attempts = 0; // it was healthy → fresh budget
|
|
@@ -116,12 +124,15 @@ function createLensChildren(rt, obs, options) {
|
|
|
116
124
|
// gateway routes pass-through (matchRoute on the prefix; the proxy never strips).
|
|
117
125
|
const scope = (0, scope_1.readLensScope)(rt.root);
|
|
118
126
|
const dashboardPort = options.dashboardPort ?? scope.dashboardPort;
|
|
127
|
+
usedDashboardPorts.add(dashboardPort); // reserve the daemon's own dashboard port so a registered workspace's differs
|
|
119
128
|
const lensHost = options.lensHost ?? 'lensmcp.local';
|
|
120
129
|
const lensBase = scope.basePath; // e.g. /tetros
|
|
121
130
|
// Pushed BEFORE the cert SANs are derived (routes.map(r=>r.host)), so lensmcp.local
|
|
122
131
|
// auto-joins the TLS SANs. Run `gateway:trust` to add it to /etc/hosts.
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
// Add this workspace's dashboard route to ITS fragment (not straight to rt.routes) so it survives a
|
|
133
|
+
// registry rebuild when another workspace registers/unregisters, then rebuild the live merged table.
|
|
134
|
+
rt.registry.appendRoutes(rt.wsKey, [{ host: lensHost, prefix: lensBase, project: observability_1.LENS_DASHBOARD_PROJECT, target: `http://localhost:${dashboardPort}` }]);
|
|
135
|
+
rt.rebuildRoutes();
|
|
125
136
|
// Publish this workspace to the GLOBAL registry so the root chooser at `${lensHost}/` lists it
|
|
126
137
|
// (and marks it live while this gateway runs). Best-effort — a registry write never blocks serving.
|
|
127
138
|
(0, workspace_registry_1.registerWorkspace)({ key: scope.key, root: rt.root, base: lensBase, pid: process.pid, updatedAt: new Date().toISOString() });
|
|
@@ -176,6 +187,42 @@ function createLensChildren(rt, obs, options) {
|
|
|
176
187
|
}
|
|
177
188
|
startLensSweeper(); // watch the just-spawned lens vites for a stale-source-set zombie
|
|
178
189
|
};
|
|
190
|
+
// P3d — host a REGISTERED workspace's dashboard: its OWN events file, a distinct port, and a
|
|
191
|
+
// `lensmcp.local/<key>` route on its fragment. The dashboard BUNDLE is the daemon's (rt.root); only the
|
|
192
|
+
// events file + base + port are the registered workspace's. Returns a reaper the control endpoint calls
|
|
193
|
+
// on unregister (marks the label removed so auto-heal won't respawn it, then kills the child).
|
|
194
|
+
const hostWorkspaceDashboard = (fragment) => {
|
|
195
|
+
const bundle = (0, lens_frontend_1.findDashboardBundle)(rt.root);
|
|
196
|
+
if (!bundle)
|
|
197
|
+
return undefined;
|
|
198
|
+
const scope = (0, scope_1.readLensScope)(fragment.root);
|
|
199
|
+
const lensHost = options.lensHost ?? 'lensmcp.local';
|
|
200
|
+
const lensBase = scope.basePath; // /<registered key>
|
|
201
|
+
let port = scope.dashboardPort;
|
|
202
|
+
while (usedDashboardPorts.has(port))
|
|
203
|
+
port += 1; // distinct from every other workspace's dashboard
|
|
204
|
+
usedDashboardPorts.add(port);
|
|
205
|
+
const label = `lens-dashboard:${fragment.wsKey}`;
|
|
206
|
+
const eventFile = path.join(fragment.root, '.lensmcp', 'events.jsonl'); // the registered workspace's OWN bus
|
|
207
|
+
removedLabels.delete(label); // a fresh registration after a prior unregister → allow auto-heal again
|
|
208
|
+
rt.registry.appendRoutes(fragment.wsKey, [{ host: lensHost, prefix: lensBase, project: observability_1.LENS_DASHBOARD_PROJECT, target: `http://localhost:${port}` }]);
|
|
209
|
+
console.log(`[gateway] hosting dashboard for workspace '${fragment.wsKey}' → http://localhost:${port} (routed at https://${lensHost}${lensBase}/)`);
|
|
210
|
+
emit('info', `lens dashboard up: ${lensHost}${lensBase} → :${port}`, 'cluster-lens-dashboard', { kind: 'lens-dashboard-up', host: lensHost, port, base: lensBase });
|
|
211
|
+
spawnManagedChild(label, process.execPath, [bundle], {
|
|
212
|
+
LENSMCP_EVENT_FILE: eventFile,
|
|
213
|
+
LENSMCP_DASHBOARD_PORT: String(port),
|
|
214
|
+
LENSMCP_DASHBOARD_BASE: lensBase,
|
|
215
|
+
});
|
|
216
|
+
return () => {
|
|
217
|
+
removedLabels.add(label); // stop auto-heal from resurrecting it
|
|
218
|
+
usedDashboardPorts.delete(port);
|
|
219
|
+
try {
|
|
220
|
+
labeledChildren.get(label)?.kill('SIGTERM');
|
|
221
|
+
}
|
|
222
|
+
catch { /* already gone */ }
|
|
223
|
+
labeledChildren.delete(label);
|
|
224
|
+
};
|
|
225
|
+
};
|
|
179
226
|
const reapAll = () => {
|
|
180
227
|
if (lensSweeper) {
|
|
181
228
|
clearInterval(lensSweeper);
|
|
@@ -188,5 +235,5 @@ function createLensChildren(rt, obs, options) {
|
|
|
188
235
|
catch { /* already gone */ }
|
|
189
236
|
}
|
|
190
237
|
};
|
|
191
|
-
return { spawnManagedChild, bootSingletonsAndApps, reapAll };
|
|
238
|
+
return { spawnManagedChild, bootSingletonsAndApps, hostWorkspaceDashboard, reapAll };
|
|
192
239
|
}
|
|
@@ -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,
|
|
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"}
|
|
@@ -153,7 +153,9 @@ function createServiceLayer(rt, obs) {
|
|
|
153
153
|
// childCount goes as a CLI option, NOT env: nx executes tasks via its
|
|
154
154
|
// daemon, whose environment is not the gateway's — env vars get lost.
|
|
155
155
|
const proc = (0, node_child_process_1.spawn)('yarn', ['nx', 'run', target, `--childCount=${min}`], {
|
|
156
|
-
|
|
156
|
+
// The service's OWN workspace root — `rt.root` for the daemon's own workspace, the registered
|
|
157
|
+
// workspace's root for a second one (so ONE daemon hosts several workspaces' devservers — model (A)).
|
|
158
|
+
cwd: svc.root,
|
|
157
159
|
detached: true,
|
|
158
160
|
// NX_DAEMON=false keeps the serve task IN this spawned process group.
|
|
159
161
|
// With the daemon on, Nx hands the long-running task to the persistent
|
|
@@ -162,9 +164,9 @@ function createServiceLayer(rt, obs) {
|
|
|
162
164
|
// "idle-kill leaves live pods" leak. (--childCount as a CLI arg already
|
|
163
165
|
// avoids the env-loss the daemon was relied on for.) NX_DAEMON=false also
|
|
164
166
|
// means env vars DO reach the pod, so the east-west key + CA propagate.
|
|
165
|
-
// LENSMCP_WS_KEY
|
|
166
|
-
//
|
|
167
|
-
env: { ...process.env, NX_DAEMON: 'false', ...(
|
|
167
|
+
// LENSMCP_WS_KEY (the SERVICE's owning workspace) namespaces its pod sock dir
|
|
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) },
|
|
168
170
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
169
171
|
});
|
|
170
172
|
svc.lastErrors = [];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Route, ServiceCtl } from './types';
|
|
2
|
+
export interface WorkspaceFragment {
|
|
3
|
+
/** The workspace key (`readLensScope(root).key`). */
|
|
4
|
+
wsKey: string;
|
|
5
|
+
/** Absolute workspace root — the fragment's provenance (for the chooser + reaping). */
|
|
6
|
+
root: string;
|
|
7
|
+
/** Every route this workspace contributes (host + path mounts, internal.*, its default catch-all, and —
|
|
8
|
+
* appended after discovery — its `lensmcp.local/<key>` dashboard route). */
|
|
9
|
+
routes: Route[];
|
|
10
|
+
/** The pod-service controllers this workspace owns (scale-from-zero lifecycle is per-service). */
|
|
11
|
+
services: ServiceCtl[];
|
|
12
|
+
}
|
|
13
|
+
/** A live map of `wsKey → fragment`; `merged()` is the flat table the pipeline matches against. */
|
|
14
|
+
export declare class RouteRegistry {
|
|
15
|
+
private readonly fragments;
|
|
16
|
+
/** Register (or REPLACE) a workspace's fragment. Idempotent by `wsKey`. */
|
|
17
|
+
register(fragment: WorkspaceFragment): void;
|
|
18
|
+
/** Drop a workspace's fragment; returns it (so the caller can reap its services/dashboard). */
|
|
19
|
+
unregister(wsKey: string): WorkspaceFragment | undefined;
|
|
20
|
+
has(wsKey: string): boolean;
|
|
21
|
+
get(wsKey: string): WorkspaceFragment | undefined;
|
|
22
|
+
keys(): string[];
|
|
23
|
+
size(): number;
|
|
24
|
+
/** APPEND routes to an already-registered workspace's fragment (e.g. its dashboard route, added after
|
|
25
|
+
* discovery). No-op if the workspace isn't registered. */
|
|
26
|
+
appendRoutes(wsKey: string, routes: Route[]): void;
|
|
27
|
+
/** Every registered workspace's pod-service controllers, flattened (the daemon's full lifecycle set). */
|
|
28
|
+
allServices(): ServiceCtl[];
|
|
29
|
+
/** The merged, specificity-sorted route table (host+path ▸ host ▸ per-apex catch-all). A NEW array each
|
|
30
|
+
* call — the caller rebuilds `rt.routes` in place from it so the pipeline (which reads `rt.routes` fresh
|
|
31
|
+
* per request) picks the change up atomically. */
|
|
32
|
+
merged(): Route[];
|
|
33
|
+
}
|
|
34
|
+
/** Rebuild `routes` IN PLACE from the registry (mutate the SAME array the request pipeline holds, so a
|
|
35
|
+
* live register/unregister is picked up with no restart and no torn read — JS is single-threaded, so the
|
|
36
|
+
* splice+push is atomic w.r.t. request handling). */
|
|
37
|
+
export declare function rebuildRoutesInPlace(routes: Route[], registry: RouteRegistry): void;
|
|
38
|
+
//# sourceMappingURL=route-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-registry.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/route-registry.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb;iFAC6E;IAC7E,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,kGAAkG;IAClG,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,mGAAmG;AACnG,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAElE,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAI3C,+FAA+F;IAC/F,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAMxD,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI3B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIjD,IAAI,IAAI,MAAM,EAAE;IAIhB,IAAI,IAAI,MAAM;IAId;+DAC2D;IAC3D,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI;IAKlD,yGAAyG;IACzG,WAAW,IAAI,UAAU,EAAE;IAI3B;;uDAEmD;IACnD,MAAM,IAAI,KAAK,EAAE;CAKlB;AAED;;sDAEsD;AACtD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAInF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RouteRegistry = void 0;
|
|
4
|
+
exports.rebuildRoutesInPlace = rebuildRoutesInPlace;
|
|
5
|
+
/**
|
|
6
|
+
* The live, multi-workspace ROUTE-FRAGMENT registry — the core of the shared daemon
|
|
7
|
+
* (planning/multi-workspace-gateway.md P3). ONE `:443` gateway holds a fragment per registered workspace
|
|
8
|
+
* (its routes + its pod-service controllers) and merges them into the single flat, specificity-sorted route
|
|
9
|
+
* table the request pipeline reads. A second workspace's `gateway start` REGISTERS its fragment; a stop
|
|
10
|
+
* UNREGISTERS it — the daemon rebuilds live, never restarts.
|
|
11
|
+
*
|
|
12
|
+
* For a SINGLE workspace the merged table is byte-identical to `discoverRoutes` + the dashboard route, so
|
|
13
|
+
* this is a no-op refactor until a second workspace registers. The per-apex `default` (see `matchRoute`)
|
|
14
|
+
* is what keeps two workspaces' catch-alls from shadowing each other.
|
|
15
|
+
*/
|
|
16
|
+
const manifest_1 = require("../manifest");
|
|
17
|
+
/** A live map of `wsKey → fragment`; `merged()` is the flat table the pipeline matches against. */
|
|
18
|
+
class RouteRegistry {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.fragments = new Map();
|
|
21
|
+
}
|
|
22
|
+
/** Register (or REPLACE) a workspace's fragment. Idempotent by `wsKey`. */
|
|
23
|
+
register(fragment) {
|
|
24
|
+
this.fragments.set(fragment.wsKey, fragment);
|
|
25
|
+
}
|
|
26
|
+
/** Drop a workspace's fragment; returns it (so the caller can reap its services/dashboard). */
|
|
27
|
+
unregister(wsKey) {
|
|
28
|
+
const f = this.fragments.get(wsKey);
|
|
29
|
+
this.fragments.delete(wsKey);
|
|
30
|
+
return f;
|
|
31
|
+
}
|
|
32
|
+
has(wsKey) {
|
|
33
|
+
return this.fragments.has(wsKey);
|
|
34
|
+
}
|
|
35
|
+
get(wsKey) {
|
|
36
|
+
return this.fragments.get(wsKey);
|
|
37
|
+
}
|
|
38
|
+
keys() {
|
|
39
|
+
return [...this.fragments.keys()];
|
|
40
|
+
}
|
|
41
|
+
size() {
|
|
42
|
+
return this.fragments.size;
|
|
43
|
+
}
|
|
44
|
+
/** APPEND routes to an already-registered workspace's fragment (e.g. its dashboard route, added after
|
|
45
|
+
* discovery). No-op if the workspace isn't registered. */
|
|
46
|
+
appendRoutes(wsKey, routes) {
|
|
47
|
+
const f = this.fragments.get(wsKey);
|
|
48
|
+
if (f)
|
|
49
|
+
f.routes.push(...routes);
|
|
50
|
+
}
|
|
51
|
+
/** Every registered workspace's pod-service controllers, flattened (the daemon's full lifecycle set). */
|
|
52
|
+
allServices() {
|
|
53
|
+
return [...this.fragments.values()].flatMap((f) => f.services);
|
|
54
|
+
}
|
|
55
|
+
/** The merged, specificity-sorted route table (host+path ▸ host ▸ per-apex catch-all). A NEW array each
|
|
56
|
+
* call — the caller rebuilds `rt.routes` in place from it so the pipeline (which reads `rt.routes` fresh
|
|
57
|
+
* per request) picks the change up atomically. */
|
|
58
|
+
merged() {
|
|
59
|
+
const all = [...this.fragments.values()].flatMap((f) => f.routes);
|
|
60
|
+
(0, manifest_1.sortBySpecificity)(all);
|
|
61
|
+
return all;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.RouteRegistry = RouteRegistry;
|
|
65
|
+
/** Rebuild `routes` IN PLACE from the registry (mutate the SAME array the request pipeline holds, so a
|
|
66
|
+
* live register/unregister is picked up with no restart and no torn read — JS is single-threaded, so the
|
|
67
|
+
* splice+push is atomic w.r.t. request handling). */
|
|
68
|
+
function rebuildRoutesInPlace(routes, registry) {
|
|
69
|
+
const merged = registry.merged();
|
|
70
|
+
routes.length = 0;
|
|
71
|
+
routes.push(...merged);
|
|
72
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -16,6 +16,8 @@ const http = tslib_1.__importStar(require("node:http"));
|
|
|
16
16
|
const path = tslib_1.__importStar(require("node:path"));
|
|
17
17
|
const discovery_1 = require("./discovery");
|
|
18
18
|
const scope_1 = require("./scope");
|
|
19
|
+
const route_registry_1 = require("./route-registry");
|
|
20
|
+
const control_1 = require("./control");
|
|
19
21
|
const workspace_registry_1 = require("./workspace-registry");
|
|
20
22
|
const service_keys_1 = require("./service-keys");
|
|
21
23
|
const observability_1 = require("./observability");
|
|
@@ -97,10 +99,19 @@ async function startGateway(options, context) {
|
|
|
97
99
|
// (planning/multi-workspace-gateway.md). It flows to each service's devserver via `LENSMCP_WS_KEY` (see
|
|
98
100
|
// lifecycle.ts), and `discoverRoutes` derives the matching pool dir the gateway scans.
|
|
99
101
|
const wsKey = (0, scope_1.readLensScope)(context.root).key;
|
|
100
|
-
const { routes, services } = (0, discovery_1.discoverRoutes)(context.root, context.projectsConfigurations?.projects ?? {}, undefined, wsKey);
|
|
101
|
-
if (
|
|
102
|
+
const { routes: discovered, services } = (0, discovery_1.discoverRoutes)(context.root, context.projectsConfigurations?.projects ?? {}, undefined, wsKey);
|
|
103
|
+
if (discovered.length === 0) {
|
|
102
104
|
throw new Error('[gateway] no project declares a `cluster` field — nothing to route.');
|
|
103
105
|
}
|
|
106
|
+
// --- the live multi-workspace route-fragment registry -----------------------
|
|
107
|
+
// This workspace registers its OWN fragment; `routes` is the merged table rebuilt IN PLACE from the
|
|
108
|
+
// registry (byte-identical to `discovered` for one workspace). A second workspace registers its fragment
|
|
109
|
+
// via the control endpoint and the daemon rebuilds live (planning/multi-workspace-gateway.md P3).
|
|
110
|
+
const registry = new route_registry_1.RouteRegistry();
|
|
111
|
+
registry.register({ wsKey, root: context.root, routes: discovered, services });
|
|
112
|
+
const routes = []; // the LIVE table the request pipeline reads (a stable ref, mutated in place)
|
|
113
|
+
const rebuildRoutes = () => (0, route_registry_1.rebuildRoutesInPlace)(routes, registry);
|
|
114
|
+
rebuildRoutes();
|
|
104
115
|
// --- per-service api keys (the gateway is the trust boundary) ---------------
|
|
105
116
|
const { serviceKeys, keyToProject } = (0, service_keys_1.loadServiceKeys)(context.root, services);
|
|
106
117
|
// --- the shared runtime (state + knobs; stopped() is a live getter) ---------
|
|
@@ -109,6 +120,8 @@ async function startGateway(options, context) {
|
|
|
109
120
|
wsKey,
|
|
110
121
|
eventFile,
|
|
111
122
|
routes,
|
|
123
|
+
registry,
|
|
124
|
+
rebuildRoutes,
|
|
112
125
|
services,
|
|
113
126
|
serviceKeys,
|
|
114
127
|
keyToProject,
|
|
@@ -123,6 +136,31 @@ async function startGateway(options, context) {
|
|
|
123
136
|
const obs = (0, observability_1.createObservability)(eventFile, { trafficFlushMs });
|
|
124
137
|
const svcLayer = (0, lifecycle_1.createServiceLayer)(rt, obs);
|
|
125
138
|
const lens = (0, lens_children_1.createLensChildren)(rt, obs, options);
|
|
139
|
+
// The daemon CONTROL PLANE (loopback ~/.lensmcp/control.sock): a second workspace's `gateway start`
|
|
140
|
+
// REGISTERS its fragment here instead of fighting for :443, and the daemon HOSTS its services (spawns
|
|
141
|
+
// them, model A) + its dashboard (planning/multi-workspace-gateway.md P3). Harmless for a lone gateway.
|
|
142
|
+
const dashboardReapers = new Map();
|
|
143
|
+
let controlServer;
|
|
144
|
+
try {
|
|
145
|
+
controlServer = (0, control_1.startControlServer)({
|
|
146
|
+
rt,
|
|
147
|
+
killService: (svc, reason) => svcLayer.killSpawned(svc, reason),
|
|
148
|
+
onRegister: (fragment) => {
|
|
149
|
+
// Host the registered workspace's dashboard (appends its lensmcp.local/<key> route to the fragment
|
|
150
|
+
// BEFORE the control handler's rebuild) and remember its reaper.
|
|
151
|
+
const reap = lens.hostWorkspaceDashboard(fragment);
|
|
152
|
+
if (reap)
|
|
153
|
+
dashboardReapers.set(fragment.wsKey, reap);
|
|
154
|
+
},
|
|
155
|
+
onUnregister: (fragment) => {
|
|
156
|
+
dashboardReapers.get(fragment.wsKey)?.();
|
|
157
|
+
dashboardReapers.delete(fragment.wsKey);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
console.warn('[gateway] control plane failed to start (multi-workspace register disabled):', e.message);
|
|
163
|
+
}
|
|
126
164
|
const proxyLayer = (0, proxy_1.createProxy)(rt, obs, svcLayer);
|
|
127
165
|
// Edge JWT verification (mirrors the prod gateway): when a JWKS source is configured/derivable, the dev
|
|
128
166
|
// edge verifies RS256 tokens by `kid` against the IdP's published keys and REJECTS anything it can't
|
|
@@ -235,6 +273,7 @@ async function startGateway(options, context) {
|
|
|
235
273
|
clearInterval(sweeper);
|
|
236
274
|
obs.emit('info', 'gateway down', 'cluster-gateway', { kind: 'gateway-down' });
|
|
237
275
|
jwtVerifier?.stop(); // clear the JWKS periodic-refresh timer
|
|
276
|
+
controlServer?.close(); // tear down the control plane + its socket
|
|
238
277
|
(0, workspace_registry_1.unregisterWorkspace)(wsKey); // drop this workspace from the global chooser registry (graceful stop)
|
|
239
278
|
for (const svc of services)
|
|
240
279
|
svcLayer.killSpawned(svc, 'gateway shutdown');
|
|
@@ -11,6 +11,7 @@ import type * as stream from 'node:stream';
|
|
|
11
11
|
import type { GatewayExecutorSchema } from '../schema';
|
|
12
12
|
import type { AuthPolicy, Attrs } from '../manifest';
|
|
13
13
|
import type { GatewayErrorReason } from '../gateway-errors';
|
|
14
|
+
import type { RouteRegistry } from './route-registry';
|
|
14
15
|
export type { Attrs };
|
|
15
16
|
export interface GatewayContext {
|
|
16
17
|
root: string;
|
|
@@ -111,6 +112,13 @@ export interface LensFrontend {
|
|
|
111
112
|
export interface ServiceCtl {
|
|
112
113
|
project: string;
|
|
113
114
|
decl: ClusterProjectDecl;
|
|
115
|
+
/** The owning workspace's absolute root — the `cwd` for its `nx run <svc>:serve-hmr` spawn. For the
|
|
116
|
+
* daemon's OWN workspace this is `rt.root`; for a registered second workspace it's THAT workspace's root
|
|
117
|
+
* (so one daemon can host several workspaces' devservers — planning/multi-workspace-gateway.md P3, (A)). */
|
|
118
|
+
root: string;
|
|
119
|
+
/** The owning workspace's key — passed as `LENSMCP_WS_KEY` to the spawn so the devserver writes its pods
|
|
120
|
+
* to `$TMPDIR/<wsKey>/<svc>-devserver` (the namespaced pool the daemon scans for this fragment). */
|
|
121
|
+
wsKey: string;
|
|
114
122
|
pool: SockPool;
|
|
115
123
|
proc?: ChildProcess;
|
|
116
124
|
state: 'down' | 'starting' | 'up';
|
|
@@ -245,7 +253,14 @@ export interface GatewayRuntime {
|
|
|
245
253
|
* each service devserver as `LENSMCP_WS_KEY`. */
|
|
246
254
|
readonly wsKey: string;
|
|
247
255
|
readonly eventFile: string;
|
|
256
|
+
/** The LIVE merged route table (mutated in place from `registry` on every register/unregister, so the
|
|
257
|
+
* request pipeline — which reads `rt.routes` fresh per request — picks the change up with no restart). */
|
|
248
258
|
readonly routes: Route[];
|
|
259
|
+
/** The multi-workspace route-fragment registry (planning/multi-workspace-gateway.md P3). For a single
|
|
260
|
+
* workspace it holds ONE fragment and `routes` is byte-identical to `discoverRoutes` + the dashboard. */
|
|
261
|
+
readonly registry: RouteRegistry;
|
|
262
|
+
/** Rebuild `routes` in place from `registry` — called after a fragment register/unregister/append. */
|
|
263
|
+
readonly rebuildRoutes: () => void;
|
|
249
264
|
readonly services: ServiceCtl[];
|
|
250
265
|
readonly serviceKeys: Record<string, string>;
|
|
251
266
|
readonly keyToProject: ReadonlyMap<string, string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,7 +39,7 @@ async function trustExecutor(options, context) {
|
|
|
39
39
|
return { success: true };
|
|
40
40
|
}
|
|
41
41
|
// --- 1) CA into the System keychain (skip when already there) -----------
|
|
42
|
-
const present = (0, node_child_process_1.spawnSync)('security', ['find-certificate', '-c', '
|
|
42
|
+
const present = (0, node_child_process_1.spawnSync)('security', ['find-certificate', '-c', 'lensmcp local dev CA', '/Library/Keychains/System.keychain'], {
|
|
43
43
|
stdio: 'ignore',
|
|
44
44
|
}).status === 0;
|
|
45
45
|
if (present) {
|
package/main.devserver.js
CHANGED
|
@@ -674,7 +674,7 @@ else {
|
|
|
674
674
|
const basicSsl = require('./basic-ssl');
|
|
675
675
|
const certDomains = GATEWAY_ROUTES.map((r) => r.host).filter((h) => !!h);
|
|
676
676
|
const certCacheDir = path.join(process.cwd(), 'node_modules', '.cache', 'davnx-webpack');
|
|
677
|
-
gatewayPem = basicSsl.getCertificateSync(certCacheDir, SERVICE_NAME || '
|
|
677
|
+
gatewayPem = basicSsl.getCertificateSync(certCacheDir, SERVICE_NAME || 'lensmcp.dev', certDomains);
|
|
678
678
|
gatewayCaPath = basicSsl.caCertPath(certCacheDir);
|
|
679
679
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
680
680
|
const httpsMod = require('node:https');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.13",
|
|
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.
|
|
69
|
-
"@lensmcp/nx-plugin": "1.16.
|
|
68
|
+
"@lensmcp/node-instrumentation": "1.16.13",
|
|
69
|
+
"@lensmcp/nx-plugin": "1.16.13",
|
|
70
70
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
71
71
|
"glob": "^11.0.0",
|
|
72
72
|
"http-proxy": "^1.18.0",
|