@lensmcp/cluster 1.11.0 → 1.13.0

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.
Files changed (101) hide show
  1. package/executors/gateway/gateway-errors.d.ts +36 -0
  2. package/executors/gateway/gateway-errors.d.ts.map +1 -0
  3. package/executors/gateway/gateway-errors.js +43 -0
  4. package/executors/gateway/gateway.lib.d.ts +15 -167
  5. package/executors/gateway/gateway.lib.d.ts.map +1 -1
  6. package/executors/gateway/gateway.lib.js +24 -1034
  7. package/executors/gateway/main.prod-gateway.js +74 -2
  8. package/executors/gateway/prod-gateway.lib.d.ts +19 -143
  9. package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
  10. package/executors/gateway/prod-gateway.lib.js +16 -831
  11. package/executors/gateway/prod-runtime/access-log.d.ts +9 -0
  12. package/executors/gateway/prod-runtime/access-log.d.ts.map +1 -0
  13. package/executors/gateway/prod-runtime/access-log.js +24 -0
  14. package/executors/gateway/prod-runtime/app.d.ts +23 -0
  15. package/executors/gateway/prod-runtime/app.d.ts.map +1 -0
  16. package/executors/gateway/prod-runtime/app.js +123 -0
  17. package/executors/gateway/prod-runtime/auth.d.ts +30 -0
  18. package/executors/gateway/prod-runtime/auth.d.ts.map +1 -0
  19. package/executors/gateway/prod-runtime/auth.js +51 -0
  20. package/executors/gateway/prod-runtime/cors.d.ts +19 -0
  21. package/executors/gateway/prod-runtime/cors.d.ts.map +1 -0
  22. package/executors/gateway/prod-runtime/cors.js +40 -0
  23. package/executors/gateway/prod-runtime/edge.d.ts +16 -0
  24. package/executors/gateway/prod-runtime/edge.d.ts.map +1 -0
  25. package/executors/gateway/prod-runtime/edge.js +65 -0
  26. package/executors/gateway/prod-runtime/handler.d.ts +32 -0
  27. package/executors/gateway/prod-runtime/handler.d.ts.map +1 -0
  28. package/executors/gateway/prod-runtime/handler.js +226 -0
  29. package/executors/gateway/prod-runtime/hooks.d.ts +20 -0
  30. package/executors/gateway/prod-runtime/hooks.d.ts.map +1 -0
  31. package/executors/gateway/prod-runtime/hooks.js +42 -0
  32. package/executors/gateway/prod-runtime/observability.d.ts +23 -0
  33. package/executors/gateway/prod-runtime/observability.d.ts.map +1 -0
  34. package/executors/gateway/prod-runtime/observability.js +125 -0
  35. package/executors/gateway/prod-runtime/rollout.d.ts +18 -0
  36. package/executors/gateway/prod-runtime/rollout.d.ts.map +1 -0
  37. package/executors/gateway/prod-runtime/rollout.js +103 -0
  38. package/executors/gateway/prod-runtime/routing.d.ts +4 -0
  39. package/executors/gateway/prod-runtime/routing.d.ts.map +1 -0
  40. package/executors/gateway/prod-runtime/routing.js +40 -0
  41. package/executors/gateway/prod-runtime/server.d.ts +3 -0
  42. package/executors/gateway/prod-runtime/server.d.ts.map +1 -0
  43. package/executors/gateway/prod-runtime/server.js +79 -0
  44. package/executors/gateway/prod-runtime/trust.d.ts +12 -0
  45. package/executors/gateway/prod-runtime/trust.d.ts.map +1 -0
  46. package/executors/gateway/prod-runtime/trust.js +32 -0
  47. package/executors/gateway/prod-runtime/types.d.ts +241 -0
  48. package/executors/gateway/prod-runtime/types.d.ts.map +1 -0
  49. package/executors/gateway/prod-runtime/types.js +2 -0
  50. package/executors/gateway/prod-runtime/upgrade.d.ts +21 -0
  51. package/executors/gateway/prod-runtime/upgrade.d.ts.map +1 -0
  52. package/executors/gateway/prod-runtime/upgrade.js +116 -0
  53. package/executors/gateway/prod-runtime/upstream.d.ts +13 -0
  54. package/executors/gateway/prod-runtime/upstream.d.ts.map +1 -0
  55. package/executors/gateway/prod-runtime/upstream.js +21 -0
  56. package/executors/gateway/runtime/auth.d.ts +8 -0
  57. package/executors/gateway/runtime/auth.d.ts.map +1 -0
  58. package/executors/gateway/runtime/auth.js +60 -0
  59. package/executors/gateway/runtime/dev-auth.d.ts +9 -0
  60. package/executors/gateway/runtime/dev-auth.d.ts.map +1 -0
  61. package/executors/gateway/runtime/dev-auth.js +79 -0
  62. package/executors/gateway/runtime/discovery.d.ts +17 -0
  63. package/executors/gateway/runtime/discovery.d.ts.map +1 -0
  64. package/executors/gateway/runtime/discovery.js +111 -0
  65. package/executors/gateway/runtime/edge.d.ts +31 -0
  66. package/executors/gateway/runtime/edge.d.ts.map +1 -0
  67. package/executors/gateway/runtime/edge.js +47 -0
  68. package/executors/gateway/runtime/handler.d.ts +24 -0
  69. package/executors/gateway/runtime/handler.d.ts.map +1 -0
  70. package/executors/gateway/runtime/handler.js +165 -0
  71. package/executors/gateway/runtime/hooks.d.ts +23 -0
  72. package/executors/gateway/runtime/hooks.d.ts.map +1 -0
  73. package/executors/gateway/runtime/hooks.js +55 -0
  74. package/executors/gateway/runtime/lens-children.d.ts +13 -0
  75. package/executors/gateway/runtime/lens-children.d.ts.map +1 -0
  76. package/executors/gateway/runtime/lens-children.js +136 -0
  77. package/executors/gateway/runtime/lifecycle.d.ts +13 -0
  78. package/executors/gateway/runtime/lifecycle.d.ts.map +1 -0
  79. package/executors/gateway/runtime/lifecycle.js +179 -0
  80. package/executors/gateway/runtime/observability.d.ts +17 -0
  81. package/executors/gateway/runtime/observability.d.ts.map +1 -0
  82. package/executors/gateway/runtime/observability.js +107 -0
  83. package/executors/gateway/runtime/proxy.d.ts +20 -0
  84. package/executors/gateway/runtime/proxy.d.ts.map +1 -0
  85. package/executors/gateway/runtime/proxy.js +113 -0
  86. package/executors/gateway/runtime/scope.d.ts +16 -0
  87. package/executors/gateway/runtime/scope.d.ts.map +1 -0
  88. package/executors/gateway/runtime/scope.js +68 -0
  89. package/executors/gateway/runtime/server.d.ts +3 -0
  90. package/executors/gateway/runtime/server.d.ts.map +1 -0
  91. package/executors/gateway/runtime/server.js +168 -0
  92. package/executors/gateway/runtime/service-keys.d.ts +11 -0
  93. package/executors/gateway/runtime/service-keys.d.ts.map +1 -0
  94. package/executors/gateway/runtime/service-keys.js +49 -0
  95. package/executors/gateway/runtime/types.d.ts +234 -0
  96. package/executors/gateway/runtime/types.d.ts.map +1 -0
  97. package/executors/gateway/runtime/types.js +2 -0
  98. package/executors/gateway/runtime/upgrade.d.ts +22 -0
  99. package/executors/gateway/runtime/upgrade.d.ts.map +1 -0
  100. package/executors/gateway/runtime/upgrade.js +63 -0
  101. package/package.json +3 -3
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLensChildren = createLensChildren;
4
+ /**
5
+ * Lens-mode managed children: the dashboard + MCP SINGLETONS (cluster-wide,
6
+ * owned by the gateway), and each `lens:true` app's Vite + browser capture.
7
+ * EVERY managed child AUTO-HEALS — restarted with exponential backoff when it
8
+ * exits while the gateway is up — and a dead child NEVER kills the cluster.
9
+ */
10
+ const node_child_process_1 = require("node:child_process");
11
+ const manifest_1 = require("../manifest");
12
+ const lens_frontend_1 = require("@lensmcp/nx-plugin/lens-frontend");
13
+ const scope_1 = require("./scope");
14
+ const HEAL_BASE_MS = 1_500; // first restart delay
15
+ const HEAL_MAX_MS = 30_000; // backoff cap — a true boot-loop settles here, never hot-loops
16
+ const HEAL_HEALTHY_MS = 20_000; // ran at least this long ⇒ a FRESH crash ⇒ reset the backoff budget
17
+ function createLensChildren(rt, obs, options) {
18
+ const { emit } = obs;
19
+ const lensChildren = [];
20
+ // Auto-heal bookkeeping per child label: consecutive restart attempts (drives the exponential
21
+ // backoff) so a boot-loop settles instead of hammering, reset once a child has run healthily.
22
+ const lensHeal = new Map();
23
+ // A `SpawnChild` over the gateway's managed-children array — the SAME shape `agent-dev` passes to
24
+ // `spawnLensFrontend`. EVERY gateway-managed child AUTO-HEALS: the dashboard + MCP singletons AND each
25
+ // `lens:true` vite app are restarted when they exit while the gateway is up, with exponential backoff.
26
+ // So a transient crash (a dev-server wedge, an OOM) recovers on its own — the user never hand-restarts —
27
+ // and a child that crashes-on-boot backs off to HEAL_MAX_MS rather than spinning. A child that ran
28
+ // healthily (≥ HEAL_HEALTHY_MS) gets a fresh budget, so one that dies after an hour comes back fast. A
29
+ // dead child still NEVER kills the cluster — it simply respawns in the background.
30
+ const spawnManagedChild = (label, bin, args, env, optional = false, respawn) => {
31
+ void optional;
32
+ void respawn; // legacy knobs — auto-heal is now universal (signature kept for compat)
33
+ const spawnedAt = Date.now();
34
+ const child = (0, node_child_process_1.spawn)(bin, args, { cwd: rt.root, env: { ...process.env, ...(env ?? {}) }, stdio: 'inherit' });
35
+ lensChildren.push(child);
36
+ child.on('exit', (code, sig) => {
37
+ const idx = lensChildren.indexOf(child);
38
+ if (idx >= 0)
39
+ lensChildren.splice(idx, 1); // drop the dead handle so stop() won't signal it
40
+ if (rt.stopped())
41
+ return; // the gateway is shutting down — do NOT resurrect
42
+ const heal = lensHeal.get(label) ?? { attempts: 0 };
43
+ if (Date.now() - spawnedAt >= HEAL_HEALTHY_MS)
44
+ heal.attempts = 0; // it was healthy → fresh budget
45
+ const delay = Math.min(HEAL_MAX_MS, HEAL_BASE_MS * 2 ** heal.attempts);
46
+ heal.attempts += 1;
47
+ lensHeal.set(label, heal);
48
+ console.warn(`[gateway] lens child ${label} exited (code=${code} sig=${sig}); auto-healing in ${Math.round(delay / 1000)}s (attempt ${heal.attempts}).`);
49
+ const t = setTimeout(() => { if (!rt.stopped())
50
+ spawnManagedChild(label, bin, args, env, optional, respawn); }, delay);
51
+ t.unref?.();
52
+ });
53
+ return child;
54
+ };
55
+ const bootSingletonsAndApps = () => {
56
+ const lensApps = [...new Map(rt.routes.filter((r) => r.lens).map((r) => [r.project, r.lens])).values()];
57
+ const wantDashboard = options.dashboard !== false;
58
+ const wantMcp = options.mcp !== false && lensApps.length > 0;
59
+ if (wantDashboard) {
60
+ const dashboardBundle = (0, lens_frontend_1.findDashboardBundle)(rt.root);
61
+ if (!dashboardBundle) {
62
+ console.warn("[gateway] lens dashboard bundle not found — skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");
63
+ }
64
+ else {
65
+ // Per-project lens scope: the dashboard is mounted at lensmcp.local/<key>/ so
66
+ // MANY projects coexist (each at its own /<key> on its own port). The dashboard
67
+ // OWNS its base path (LENSMCP_DASHBOARD_BASE) and history-routes under it, so the
68
+ // gateway routes pass-through (matchRoute on the prefix; the proxy never strips).
69
+ const scope = (0, scope_1.readLensScope)(rt.root);
70
+ const dashboardPort = options.dashboardPort ?? scope.dashboardPort;
71
+ const lensHost = options.lensHost ?? 'lensmcp.local';
72
+ const lensBase = scope.basePath; // e.g. /tetros
73
+ // Pushed BEFORE the cert SANs are derived (routes.map(r=>r.host)), so lensmcp.local
74
+ // auto-joins the TLS SANs. Run `gateway:trust` to add it to /etc/hosts.
75
+ rt.routes.push({ host: lensHost, prefix: lensBase, project: '(lens-dashboard)', target: `http://localhost:${dashboardPort}` });
76
+ (0, manifest_1.sortBySpecificity)(rt.routes);
77
+ console.log(`[gateway] starting lens dashboard → http://localhost:${dashboardPort} (routed at https://${lensHost}${lensBase}/)`);
78
+ emit('info', `lens dashboard up: ${lensHost}${lensBase} → :${dashboardPort}`, 'cluster-lens-dashboard', { kind: 'lens-dashboard-up', host: lensHost, port: dashboardPort, base: lensBase });
79
+ spawnManagedChild('lens-dashboard', process.execPath, [dashboardBundle], {
80
+ LENSMCP_EVENT_FILE: rt.eventFile,
81
+ LENSMCP_DASHBOARD_PORT: String(dashboardPort),
82
+ LENSMCP_DASHBOARD_BASE: lensBase,
83
+ });
84
+ }
85
+ }
86
+ if (wantMcp) {
87
+ const mcpBundle = (0, lens_frontend_1.findMcpBundle)(rt.root);
88
+ if (!mcpBundle) {
89
+ console.warn("[gateway] MCP server bundle not found — skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");
90
+ }
91
+ else {
92
+ console.log(`[gateway] starting lens MCP server (${mcpBundle})`);
93
+ spawnManagedChild('lens-mcp', process.execPath, [mcpBundle], {
94
+ LENSMCP_EVENT_FILE: rt.eventFile,
95
+ LENSMCP_TRANSPORT: process.env['LENSMCP_TRANSPORT'] ?? 'http',
96
+ LENSMCP_PORT: process.env['LENSMCP_PORT'] ?? '3000',
97
+ });
98
+ }
99
+ }
100
+ // Spawn each `lens:true` frontend's Vite + browser capture as the upstream,
101
+ // via the SAME shared helper `agent-dev` uses (singletons excluded — owned above).
102
+ for (const app of lensApps) {
103
+ if (app.started)
104
+ continue;
105
+ app.started = true;
106
+ try {
107
+ (0, lens_frontend_1.spawnLensFrontend)({
108
+ projectRoot: app.projectRoot,
109
+ workspaceRoot: rt.root,
110
+ project: app.project,
111
+ eventFile: rt.eventFile,
112
+ port: app.port,
113
+ chrome: true,
114
+ headless: true,
115
+ // Identify this app's events on the shared bus (mirrors the serve
116
+ // executor's LENSMCP_PROJECT stamp for pods).
117
+ viteEnv: { LENSMCP_PROJECT: app.project },
118
+ log: (l) => console.log(l),
119
+ }, spawnManagedChild);
120
+ emit('info', `lens app started: ${app.project} (vite :${app.port})`, `cluster-lens-app:${app.project}`, { kind: 'lens-app-up', project: app.project, port: app.port });
121
+ }
122
+ catch (e) {
123
+ console.error(`[gateway] lens app ${app.project}: ${e.message}`);
124
+ }
125
+ }
126
+ };
127
+ const reapAll = () => {
128
+ for (const c of lensChildren) {
129
+ try {
130
+ c.kill('SIGTERM');
131
+ }
132
+ catch { /* already gone */ }
133
+ }
134
+ };
135
+ return { spawnManagedChild, bootSingletonsAndApps, reapAll };
136
+ }
@@ -0,0 +1,13 @@
1
+ import type { GatewayRuntime, ServiceCtl, SockPool } from './types';
2
+ import type { Observability } from './observability';
3
+ export interface ServiceLayer {
4
+ scanNow(pool: SockPool): number;
5
+ spawnService(svc: ServiceCtl): void;
6
+ ensureUp(svc: ServiceCtl): Promise<boolean>;
7
+ scaleUp(svc: ServiceCtl): void;
8
+ killSpawned(svc: ServiceCtl, why: string): void;
9
+ /** Start the idle sweeper; returns the interval (already `.unref()`'d). */
10
+ startSweeper(): NodeJS.Timeout;
11
+ }
12
+ export declare function createServiceLayer(rt: GatewayRuntime, obs: Observability): ServiceLayer;
13
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIrD,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,CAsIvF"}
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createServiceLayer = createServiceLayer;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * Service lifecycle: scale-from-zero, autoscale, idle kill. The gateway is the
7
+ * load balancer in front of each pod service's unix-socket pool and owns the
8
+ * pods' serverless lifecycle (eager/minPods/maxPods/idleKillSec).
9
+ */
10
+ const node_child_process_1 = require("node:child_process");
11
+ const fs = tslib_1.__importStar(require("node:fs"));
12
+ const http = tslib_1.__importStar(require("node:http"));
13
+ const path = tslib_1.__importStar(require("node:path"));
14
+ const discovery_1 = require("./discovery");
15
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
16
+ function createServiceLayer(rt, obs) {
17
+ const { emit } = obs;
18
+ const scanNow = (pool) => { pool.scannedAt = 0; return (0, discovery_1.pickSock)(pool, rt.scanTtlMs) ? pool.socks.length : 0; };
19
+ const spawnService = (svc) => {
20
+ if (rt.stopped())
21
+ return; // never spawn a process after the gateway is told to stop
22
+ const target = `${svc.project}:${svc.decl.serveTarget ?? 'serve-hmr'}`;
23
+ const min = Math.max(1, svc.decl.minPods ?? 1);
24
+ console.log(`[gateway] starting ${target} (pods=${min})…`);
25
+ emit('info', `service starting: ${svc.project} (${min} pod${min > 1 ? 's' : ''})`, `cluster-service:${svc.project}`, { kind: 'service-starting', project: svc.project, pods: min });
26
+ svc.state = 'starting';
27
+ svc.spawnAt = Date.now();
28
+ // childCount goes as a CLI option, NOT env: nx executes tasks via its
29
+ // daemon, whose environment is not the gateway's — env vars get lost.
30
+ const proc = (0, node_child_process_1.spawn)('yarn', ['nx', 'run', target, `--childCount=${min}`], {
31
+ cwd: rt.root,
32
+ detached: true,
33
+ // NX_DAEMON=false keeps the serve task IN this spawned process group.
34
+ // With the daemon on, Nx hands the long-running task to the persistent
35
+ // daemon (a DIFFERENT group), so the later process-group kill reaps only
36
+ // the `yarn nx` shim and orphans the devserver + pod tree — the classic
37
+ // "idle-kill leaves live pods" leak. (--childCount as a CLI arg already
38
+ // avoids the env-loss the daemon was relied on for.)
39
+ env: { ...process.env, NX_DAEMON: 'false' },
40
+ stdio: ['ignore', 'pipe', 'pipe'],
41
+ });
42
+ svc.lastErrors = [];
43
+ svc.buildFailed = false;
44
+ const sniff = (d) => {
45
+ const text = d.toString();
46
+ for (const line of text.split('\n')) {
47
+ if (/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(line)) {
48
+ // eslint-disable-next-line no-control-regex -- strip ANSI color codes from child output
49
+ svc.lastErrors.push(line.replace(/\x1b\[[0-9;]*m/g, '').trim());
50
+ if (svc.lastErrors.length > 12)
51
+ svc.lastErrors.shift();
52
+ svc.buildFailed = true;
53
+ emit('error', `service error: ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-error', project: svc.project, line: svc.lastErrors[svc.lastErrors.length - 1] });
54
+ }
55
+ if (/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(line))
56
+ svc.buildFailed = false;
57
+ }
58
+ };
59
+ proc.stdout?.on('data', (d) => { if (rt.stopped())
60
+ return; sniff(d); process.stdout.write(`[${svc.project}] ${d}`); });
61
+ proc.stderr?.on('data', (d) => { if (rt.stopped())
62
+ return; sniff(d); process.stderr.write(`[${svc.project}] ${d}`); });
63
+ proc.on('exit', (code) => {
64
+ svc.proc = undefined;
65
+ svc.state = 'down';
66
+ if (rt.stopped())
67
+ return; // gateway is tearing down — its dying services' exit chatter is noise
68
+ console.log(`[gateway] ${target} exited (${code})`);
69
+ emit(code === 0 ? 'info' : 'warning', `service down: ${svc.project} (exit ${code})`, `cluster-service:${svc.project}`, { kind: 'service-down', project: svc.project, code });
70
+ });
71
+ svc.proc = proc;
72
+ };
73
+ const ensureUp = async (svc) => {
74
+ if (scanNow(svc.pool) > 0) {
75
+ if (svc.state !== 'up') {
76
+ emit('info', `service up: ${svc.project} (${svc.pool.socks.length} pods, adopted)`, `cluster-service:${svc.project}`, { kind: 'service-up', project: svc.project, pods: svc.pool.socks.length, adopted: true });
77
+ }
78
+ svc.state = 'up';
79
+ return true;
80
+ }
81
+ if (rt.stopped())
82
+ return false;
83
+ if (svc.state !== 'starting' && !svc.proc)
84
+ spawnService(svc); // a live spawn supervises itself — don't stack a second one (EADDRINUSE)
85
+ const deadline = Date.now() + rt.startupTimeoutMs;
86
+ const failFastAt = Date.now() + 12_000; // a build error within 12s → stop waiting, report the cause
87
+ while (!rt.stopped() && Date.now() < deadline) {
88
+ await sleep(Math.min(700, rt.startupTimeoutMs / 4));
89
+ if (svc.buildFailed && Date.now() > failFastAt)
90
+ return false;
91
+ if (scanNow(svc.pool) > 0) {
92
+ svc.state = 'up';
93
+ svc.buildFailed = false;
94
+ const durationMs = svc.spawnAt ? Date.now() - svc.spawnAt : undefined;
95
+ console.log(`[gateway] ${svc.project} up (${svc.pool.socks.length} pods${durationMs ? `, cold start ${durationMs}ms` : ''})`);
96
+ emit('info', `cold start: ${svc.project} (${svc.pool.socks.length} pods, ${durationMs ?? '?'}ms)`, `cluster-service:${svc.project}`, { kind: 'cold-start', project: svc.project, pods: svc.pool.socks.length, durationMs });
97
+ return true;
98
+ }
99
+ if (svc.state === 'down')
100
+ return false; // spawn died
101
+ }
102
+ return false;
103
+ };
104
+ const scaleUp = (svc) => {
105
+ const max = svc.decl.maxPods ?? svc.decl.minPods ?? 1;
106
+ const cur = svc.pool.socks.length;
107
+ const admin = svc.decl.adminUrl ?? (svc.decl.port ? `http://localhost:${svc.decl.port}` : undefined);
108
+ if (!admin || cur === 0 || cur >= max || svc.inflight <= cur)
109
+ return;
110
+ if (Date.now() - svc.lastScaleAt < 5000)
111
+ return;
112
+ svc.lastScaleAt = Date.now();
113
+ const mod = admin.startsWith('https')
114
+ ? // eslint-disable-next-line @typescript-eslint/no-require-imports
115
+ require('node:https')
116
+ : http;
117
+ const r = mod.request(`${admin}/webpack/scale`, { method: 'POST', rejectUnauthorized: false }, (rs) => rs.resume());
118
+ r.on('error', () => undefined);
119
+ r.end(JSON.stringify({ pods: cur + 1 }));
120
+ console.log(`[gateway] autoscale ${svc.project}: ${cur} → ${cur + 1} pods (inflight ${svc.inflight})`);
121
+ emit('info', `autoscale: ${svc.project} ${cur} → ${cur + 1} pods`, `cluster-service:${svc.project}`, { kind: 'autoscale', project: svc.project, from: cur, to: cur + 1, inflight: svc.inflight });
122
+ };
123
+ // Idle sweeper: kill gateway-spawned lazy services after idleKillSec.
124
+ const killSpawned = (svc, why) => {
125
+ if (!svc.proc)
126
+ return;
127
+ console.log(`[gateway] ${svc.project} ${why} — scaling to zero.`);
128
+ emit('info', `idle kill: ${svc.project} (${why})`, `cluster-service:${svc.project}`, { kind: 'idle-kill', project: svc.project, why });
129
+ const dying = svc.proc;
130
+ // Detach the corpse's handlers BEFORE killing: a dying webpack/nx flushes
131
+ // buffered stdout/stderr as it exits, and those `sniff` callbacks would
132
+ // keep mutating svc.lastErrors/buildFailed (phantom build failures) — fatal
133
+ // if a fresh request re-spawns this same svc while the corpse still flushes.
134
+ try {
135
+ dying.stdout?.removeAllListeners('data');
136
+ dying.stderr?.removeAllListeners('data');
137
+ dying.removeAllListeners('exit');
138
+ }
139
+ catch { /* ignore */ }
140
+ try {
141
+ process.kill(-dying.pid, 'SIGTERM');
142
+ }
143
+ catch {
144
+ try {
145
+ dying.kill('SIGTERM');
146
+ }
147
+ catch { /* gone */ }
148
+ }
149
+ svc.proc = undefined;
150
+ svc.state = 'down';
151
+ // children die with the tree but their sock FILES survive — remove them
152
+ // so the canvas and the proxy never see ghost pods.
153
+ setTimeout(() => {
154
+ try {
155
+ for (const f of fs.readdirSync(svc.pool.dir)) {
156
+ if (f.endsWith('.sock'))
157
+ fs.unlinkSync(path.join(svc.pool.dir, f));
158
+ }
159
+ }
160
+ catch { /* gone */ }
161
+ svc.pool.socks = [];
162
+ svc.pool.scannedAt = 0;
163
+ }, 1200).unref?.();
164
+ };
165
+ const startSweeper = () => {
166
+ const sweeper = setInterval(() => {
167
+ for (const svc of rt.services) {
168
+ const idle = svc.decl.idleKillSec;
169
+ if (!svc.proc || !idle || svc.inflight > 0)
170
+ continue;
171
+ if (Date.now() - svc.lastUsed > idle * 1000)
172
+ killSpawned(svc, `idle ${idle}s`);
173
+ }
174
+ }, rt.sweepMs);
175
+ sweeper.unref();
176
+ return sweeper;
177
+ };
178
+ return { scanNow, spawnService, ensureUp, scaleUp, killSpawned, startSweeper };
179
+ }
@@ -0,0 +1,17 @@
1
+ import type * as http from 'node:http';
2
+ import type { GwTrace, Route } from './types';
3
+ export type Severity = 'info' | 'warning' | 'error';
4
+ export interface Observability {
5
+ emit(severity: Severity, title: string, fingerprint: string, raw?: Record<string, unknown>, flowCtx?: Record<string, unknown>): void;
6
+ eid(): string;
7
+ beginTrace(req: http.IncomingMessage): GwTrace | undefined;
8
+ traceStep(t: GwTrace | undefined, stepName: string, info?: Record<string, unknown>): void;
9
+ finishTrace(t: GwTrace | undefined, project: string, status: number, extra?: Record<string, unknown>): void;
10
+ recordEdge(route: Route, ms: number, status: number, caller?: string): void;
11
+ /** Start the periodic traffic-edge flush; returns the interval (already `.unref()`'d). */
12
+ startEdgeFlusher(): NodeJS.Timeout;
13
+ }
14
+ export declare function createObservability(eventFile: string, opts: {
15
+ trafficFlushMs: number;
16
+ }): Observability;
17
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/observability.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrI,GAAG,IAAI,MAAM,CAAC;IACd,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3D,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1F,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5G,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5E,0FAA0F;IAC1F,gBAAgB,IAAI,MAAM,CAAC,OAAO,CAAC;CACpC;AAKD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CA6FtG"}
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createObservability = createObservability;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * The observability layer: everything the gateway writes to the LensMCP bus.
7
+ * Bus (`emit`/`eid`), per-request mechanism trace (`beginTrace`/`traceStep`/
8
+ * `finishTrace`), and aggregated host→service traffic edges (`recordEdge` +
9
+ * `startEdgeFlusher`). Merged because they only ever vary together — trace and
10
+ * edges both emit; both need `eid`.
11
+ *
12
+ * Appends BaseEvent-shaped lines to the shared event file the MCP tails (same
13
+ * rendezvous the vite plugin and nest instrumentation use), so the lens sees
14
+ * the cluster itself: services up/down, cold starts, autoscale, idle kills, and
15
+ * aggregated traffic edges for the topology.
16
+ */
17
+ const fs = tslib_1.__importStar(require("node:fs"));
18
+ const path = tslib_1.__importStar(require("node:path"));
19
+ const hdr = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
20
+ function createObservability(eventFile, opts) {
21
+ const eid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
22
+ const emit = (severity, title, fingerprint, raw, flowCtx) => {
23
+ try {
24
+ fs.mkdirSync(path.dirname(eventFile), { recursive: true });
25
+ fs.appendFileSync(eventFile, JSON.stringify({
26
+ id: eid(), sessionId: 'pending', timestamp: Date.now(),
27
+ source: 'gateway', category: 'cluster', severity,
28
+ context: { sessionId: 'pending', ...flowCtx }, fingerprint, title, raw,
29
+ }) + '\n');
30
+ }
31
+ catch { /* lens offline — never block traffic */ }
32
+ };
33
+ // --- per-request mechanism trace (flows) ----------------------------------
34
+ // When a request carries lens flow headers (stamped by the browser client
35
+ // runtime or an instrumented upstream), the gateway narrates its own steps —
36
+ // received → route-match → auth → pod-select (+ cold-start) → response — as
37
+ // ONE `gateway-request` event correlated by the same flowId/requestId.
38
+ // Header-less traffic (curl, health checks) stays un-narrated so the bus
39
+ // doesn't bloat.
40
+ const beginTrace = (req) => {
41
+ const flowId = hdr(req.headers['x-lensmcp-flow-id']);
42
+ let requestId = hdr(req.headers['x-request-id']);
43
+ if (!flowId && !requestId && !hdr(req.headers['traceparent']))
44
+ return undefined;
45
+ if (!requestId) {
46
+ // Mint the request id AT THE EDGE and inject it downstream, so the
47
+ // gateway trace and the pod's server-request/spans share one id —
48
+ // that's the join key that attributes pod events to the routed service.
49
+ requestId = eid();
50
+ req.headers['x-request-id'] = requestId;
51
+ }
52
+ return {
53
+ startedAt: Date.now(),
54
+ method: (req.method ?? 'GET').toUpperCase(),
55
+ host: (req.headers.host || '').split(':')[0],
56
+ url: req.url ?? '/',
57
+ steps: [],
58
+ ...(flowId ? { flowId } : {}),
59
+ ...(requestId ? { requestId } : {}),
60
+ ...(hdr(req.headers['x-lensmcp-origin-node-id']) ? { originNodeId: hdr(req.headers['x-lensmcp-origin-node-id']) } : {}),
61
+ };
62
+ };
63
+ const traceStep = (t, stepName, info) => {
64
+ if (!t)
65
+ return;
66
+ t.steps.push({ step: stepName, atMs: Date.now() - t.startedAt, ...info });
67
+ };
68
+ const finishTrace = (t, project, status, extra) => {
69
+ if (!t || t.done)
70
+ return;
71
+ t.done = true;
72
+ const durationMs = Date.now() - t.startedAt;
73
+ emit(status >= 500 ? 'error' : 'info', `gateway ${t.method} ${t.host}${t.url} → ${status} (${durationMs}ms)`, `gateway-request:${project}`, {
74
+ kind: 'gateway-request', project, host: t.host, method: t.method, url: t.url,
75
+ status, durationMs, startedAt: t.startedAt, steps: t.steps, ...extra,
76
+ }, {
77
+ ...(t.flowId ? { flowId: t.flowId } : {}),
78
+ ...(t.requestId ? { requestId: t.requestId } : {}),
79
+ ...(t.originNodeId ? { originNodeId: t.originNodeId } : {}),
80
+ });
81
+ };
82
+ // --- aggregated host→service traffic edges --------------------------------
83
+ // The request-derived topology (nodes/edges appear from observed traffic).
84
+ const edges = new Map();
85
+ const recordEdge = (route, ms, status, caller) => {
86
+ const host = route.host ?? '(default)';
87
+ const key = (caller ? caller + '⇒' : '') + host + '→' + route.project;
88
+ const e = edges.get(key) ?? { host, project: route.project, caller, count: 0, totalMs: 0, errors: 0 };
89
+ e.count += 1;
90
+ e.totalMs += ms;
91
+ if (status >= 500 || status === 0)
92
+ e.errors += 1;
93
+ edges.set(key, e);
94
+ };
95
+ const startEdgeFlusher = () => {
96
+ const t = setInterval(() => {
97
+ for (const e of edges.values()) {
98
+ const avgMs = Math.round(e.totalMs / e.count);
99
+ emit(e.errors ? 'warning' : 'info', `traffic ${e.host} → ${e.project}: ${e.count} req, ~${avgMs}ms${e.errors ? `, ${e.errors} errors` : ''}`, `gateway-traffic:${e.host}->${e.project}`, { kind: 'gateway-traffic', host: e.host, project: e.project, caller: e.caller, count: e.count, avgMs, errors: e.errors });
100
+ }
101
+ edges.clear();
102
+ }, opts.trafficFlushMs);
103
+ t.unref();
104
+ return t;
105
+ };
106
+ return { emit, eid, beginTrace, traceStep, finishTrace, recordEdge, startEdgeFlusher };
107
+ }
@@ -0,0 +1,20 @@
1
+ import * as http from 'node:http';
2
+ import type * as stream from 'node:stream';
3
+ import type { GatewayRuntime, Route } from './types';
4
+ import type { Observability } from './observability';
5
+ import type { ServiceLayer } from './lifecycle';
6
+ export interface ProxyServer {
7
+ web(req: http.IncomingMessage, res: http.ServerResponse, opts: Record<string, unknown>, cb: (err: Error) => void): void;
8
+ ws(req: http.IncomingMessage, socket: stream.Duplex, head: Buffer, opts: Record<string, unknown>, cb: (err?: Error) => void): void;
9
+ on(event: 'proxyRes', cb: (proxyRes: http.IncomingMessage, req: http.IncomingMessage) => void): void;
10
+ close?(): void;
11
+ }
12
+ export interface ProxyLayer {
13
+ proxy: ProxyServer;
14
+ forward(route: Route, req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
15
+ agentForTarget(target: unknown): http.Agent | import('node:https').Agent;
16
+ destroyAgents(): void;
17
+ closeProxy(): void;
18
+ }
19
+ export declare function createProxy(rt: GatewayRuntime, obs: Observability, svcLayer: ServiceLayer): ProxyLayer;
20
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,cAAc,EAAW,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACxH,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACnI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,GAAG,IAAI,CAAC;IACrG,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,OAAO,YAAY,EAAE,KAAK,CAAC;IACzE,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,GAAG,UAAU,CA4FtG"}
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createProxy = createProxy;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * The forwarding layer: one pooled `http-proxy` instance (keep-alive agents) and
7
+ * the `forward` that drives it — pod round-robin with scale-from-zero/autoscale,
8
+ * or a TCP upstream. Owns the edge-response branding (`x-lensmcp-pod`, stripping
9
+ * `x-powered-by`, `server: LensMCP`).
10
+ */
11
+ const fs = tslib_1.__importStar(require("node:fs"));
12
+ const http = tslib_1.__importStar(require("node:http"));
13
+ const path = tslib_1.__importStar(require("node:path"));
14
+ const discovery_1 = require("./discovery");
15
+ const edge_1 = require("./edge");
16
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
17
+ const httpProxy = require('http-proxy');
18
+ function createProxy(rt, obs, svcLayer) {
19
+ const { recordEdge, finishTrace, traceStep } = obs;
20
+ // Keep-alive to upstreams (pods over unix sockets AND TCP apps). Without an
21
+ // agent http-proxy opens a fresh connection per request — a connect per call
22
+ // that ~halves throughput and adds latency. One pooled agent reuses sockets
23
+ // (keyed by socketPath / host:port); https targets get the verify-skipping
24
+ // https agent. NB: this file uses `require('node:https')` to match the source
25
+ // file's pattern (the orchestrator shadows the module with a local TLS flag).
26
+ const agentOpts = { keepAlive: true, keepAliveMsecs: 30_000, maxSockets: 1024, maxFreeSockets: 256 };
27
+ const httpAgent = new http.Agent(agentOpts);
28
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
29
+ const httpsAgent = new (require('node:https').Agent)(agentOpts);
30
+ const agentForTarget = (target) => typeof target === 'string' && target.startsWith('https:') ? httpsAgent : httpAgent;
31
+ // secure:false — upstreams may terminate TLS with the same local CA.
32
+ // proxyTimeout — a pod that accepts the socket but never responds would
33
+ // otherwise pin svc.inflight forever (blocking idle-kill, skewing autoscale)
34
+ // and leave the gateway-request trace open; the timeout fires the proxy
35
+ // error path → 502 → res 'close' → inflight decrements + trace finishes.
36
+ const proxy = httpProxy.createProxyServer({ xfwd: true, secure: false, proxyTimeout: 120_000, agent: httpAgent });
37
+ // Surface WHICH pod served the request (round-robin made visible).
38
+ proxy.on('proxyRes', (proxyRes, req) => {
39
+ const pod = req.__lensmcpPod;
40
+ if (pod)
41
+ proxyRes.headers['x-lensmcp-pod'] = pod;
42
+ // The gateway owns the edge response: drop the upstream framework leak
43
+ // (e.g. NestJS/Express `x-powered-by`) and brand the hop as the LensMCP gateway.
44
+ delete proxyRes.headers['x-powered-by'];
45
+ proxyRes.headers['server'] = 'LensMCP';
46
+ });
47
+ const forward = async (route, req, res) => {
48
+ const startedAt = Date.now();
49
+ const trace = req.__lensmcpTrace;
50
+ res.on('close', () => {
51
+ recordEdge(route, Date.now() - startedAt, res.statusCode ?? 0, req.__lensmcpCaller);
52
+ finishTrace(trace, route.project, res.statusCode ?? 0, {
53
+ ...(req.__lensmcpPod ? { pod: req.__lensmcpPod } : {}),
54
+ ...(req.__lensmcpCaller ? { caller: req.__lensmcpCaller } : {}),
55
+ });
56
+ });
57
+ if (route.prependPrefix && !req.url?.startsWith(route.prependPrefix)) {
58
+ req.url = route.prependPrefix + (req.url ?? '/');
59
+ }
60
+ if (route.pool) {
61
+ const ctl = route.svc;
62
+ if (ctl) {
63
+ ctl.lastUsed = Date.now();
64
+ ctl.inflight += 1;
65
+ res.on('close', () => { ctl.inflight -= 1; ctl.lastUsed = Date.now(); });
66
+ }
67
+ let sock = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs);
68
+ if (!sock && ctl) {
69
+ // scale from zero: start the service, buffer this request until pods are up
70
+ traceStep(trace, 'cold-start', { project: route.project });
71
+ if (await svcLayer.ensureUp(ctl))
72
+ sock = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs);
73
+ }
74
+ if (ctl)
75
+ svcLayer.scaleUp(ctl);
76
+ if (!sock) {
77
+ const errs = ctl?.lastErrors?.length ? ` Service errors: ${ctl.lastErrors.join('; ')}` : '';
78
+ (0, edge_1.sendError)(res, req, 503, 'unavailable', `Service ${route.project} is not reachable (no pods in ${route.pool.dir}).${errs} The watcher recovers and the next request retries automatically.`);
79
+ return;
80
+ }
81
+ req.__lensmcpPod = path.basename(sock, '.sock');
82
+ traceStep(trace, 'pod-select', { pod: path.basename(sock, '.sock'), pods: route.pool.socks.length, strategy: 'round-robin' });
83
+ // NO autoRewrite for a socketPath target: it's an OBJECT, and http-proxy's setRedirectHostRewrite
84
+ // does url.parse(options.target) on ANY 3xx response — which throws ("url must be a string, received
85
+ // Object") and CRASHES the gateway the instant a pod returns a redirect (e.g. the auth IdP's
86
+ // /authorize → login). A socket target has no host to rewrite anyway, and pods emit ABSOLUTE
87
+ // external redirect URLs, so no rewrite is needed.
88
+ proxy.web(req, res, { target: { socketPath: sock }, agent: httpAgent }, (err) => {
89
+ route.pool.socks = route.pool.socks.filter((s) => s !== sock); // drop dead pod until rescan
90
+ route.pool.idx = -1; // removing an element shifts indices — reset so the next pick walks cleanly from 0
91
+ try {
92
+ fs.unlinkSync(sock);
93
+ }
94
+ catch { /* already gone */ } // a refused sock is a GHOST file — remove it so rescans stay truthful
95
+ console.error(`[gateway] pod ${path.basename(sock)} of ${route.project}:`, err.message);
96
+ (0, edge_1.sendError)(res, req, 502, 'unavailable', `Pod ${path.basename(sock)} of ${route.project} unavailable — retry.`);
97
+ });
98
+ return;
99
+ }
100
+ // autoRewrite ONLY for a string (external URL) target — a non-string target crashes
101
+ // setRedirectHostRewrite's url.parse(options.target) on a 3xx (see the socketPath case above).
102
+ proxy.web(req, res, { target: route.target, autoRewrite: typeof route.target === 'string', changeOrigin: true, agent: agentForTarget(route.target) }, (err) => {
103
+ console.error(`[gateway] upstream ${route.project} (${route.target}):`, err.message);
104
+ (0, edge_1.sendError)(res, req, 502, 'unavailable', `Upstream ${route.project} unavailable.`);
105
+ });
106
+ };
107
+ const destroyAgents = () => { httpAgent.destroy(); httpsAgent.destroy(); };
108
+ const closeProxy = () => { try {
109
+ proxy.close?.();
110
+ }
111
+ catch { /* ignore */ } };
112
+ return { proxy, forward, agentForTarget, destroyAgents, closeProxy };
113
+ }
@@ -0,0 +1,16 @@
1
+ /** The base domain for the lens dashboard host — the longest common dotted
2
+ * suffix of the declared cluster hosts (e.g. ['api.tetros.ai.local',
3
+ * 'tetros.ai.local'] → 'tetros.ai.local'). Falls back to the first host, then
4
+ * to 'lens.local' if no host is declared. */
5
+ export declare function baseDomainOf(hosts: string[]): string;
6
+ /** Per-workspace lens scope (key + dashboard port + mount path). Prefers the
7
+ * CLI-written `.lensmcp/config.json` (so the gateway agrees with `lensmcp gateway`
8
+ * / `lensmcp dashboard`); falls back to deriving from the workspace. */
9
+ export declare function readLensScope(root: string): {
10
+ key: string;
11
+ dashboardPort: number;
12
+ basePath: string;
13
+ };
14
+ export declare function lensKeyFrom(root: string): string;
15
+ export declare function lensSlug(input: string): string;
16
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/scope.ts"],"names":[],"mappings":"AAOA;;;8CAG8C;AAC9C,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAUpD;AAED;;yEAEyE;AACzE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAmBpG;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ9C"}