@lensmcp/cluster 1.16.19 → 1.16.21

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":"otel-tracing.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/otel-tracing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAiD,KAAK,IAAI,EAA0C,MAAM,oBAAoB,CAAC;AAKtI,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtE,+EAA+E;AAC/E,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC;AAE7B,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,SAAS,CAAC;IAC3G,sFAAsF;IACtF,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5D,8EAA8E;IAC9E,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC;IAClG,2DAA2D;IAC3D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAcD;2EAC2E;AAC3E,wBAAgB,YAAY,CAAC,IAAI,GAAE,cAAmB,GAAG,aAAa,GAAG,SAAS,CA+BjF"}
1
+ {"version":3,"file":"otel-tracing.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/otel-tracing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAiD,KAAK,IAAI,EAA0C,MAAM,oBAAoB,CAAC;AAKtI,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtE,+EAA+E;AAC/E,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC;AAE7B,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,SAAS,CAAC;IAC3G,sFAAsF;IACtF,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5D,8EAA8E;IAC9E,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC;IAClG,2DAA2D;IAC3D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAcD;2EAC2E;AAC3E,wBAAgB,YAAY,CAAC,IAAI,GAAE,cAAmB,GAAG,aAAa,GAAG,SAAS,CAgCjF"}
@@ -46,10 +46,11 @@ function setupTracing(opts = {}) {
46
46
  const url = /\/v1\/traces$/.test(base) ? base : `${base}/v1/traces`;
47
47
  const exporter = new exporter_trace_otlp_http_1.OTLPTraceExporter({ url, ...(opts.headers ? { headers: opts.headers } : {}) });
48
48
  const provider = new sdk_trace_base_1.BasicTracerProvider({
49
- resource: new resources_1.Resource({ 'service.name': opts.serviceName ?? process.env['OTEL_SERVICE_NAME'] ?? 'lensmcp-gateway' }),
49
+ resource: (0, resources_1.resourceFromAttributes)({ 'service.name': opts.serviceName ?? process.env['OTEL_SERVICE_NAME'] ?? 'lensmcp-gateway' }),
50
50
  sampler: new sdk_trace_base_1.ParentBasedSampler({ root: new sdk_trace_base_1.TraceIdRatioBasedSampler(opts.sampleRatio ?? 1) }),
51
+ // OTel 2.0: span processors are constructor config (the mutating `addSpanProcessor` was removed).
52
+ spanProcessors: [new sdk_trace_base_1.BatchSpanProcessor(exporter)],
51
53
  });
52
- provider.addSpanProcessor(new sdk_trace_base_1.BatchSpanProcessor(exporter));
53
54
  const tracer = provider.getTracer('lensmcp-gateway');
54
55
  const propagator = new core_1.W3CTraceContextPropagator();
55
56
  return {
@@ -5,5 +5,16 @@ export interface EdgeAuth {
5
5
  internal(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
6
6
  public(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
7
7
  }
8
- export declare function createEdgeAuth(rt: GatewayRuntime, obs: Observability, verifier?: JwtVerifier): EdgeAuth;
8
+ /**
9
+ * HOST-AWARE verifier selection — a SHARED multi-workspace daemon fronts several
10
+ * IdPs (each workspace's `auth.<apex>`), so the verifier for a request is chosen
11
+ * by the request's host rather than fixed at boot: a foodguard token is verified
12
+ * against the foodguard IdP's JWKS, never the tetros keys. A single-workspace
13
+ * gateway resolves to its one IdP exactly as before.
14
+ */
15
+ export interface EdgeVerifierPool {
16
+ for(host: string): JwtVerifier | undefined;
17
+ stop(): void;
18
+ }
19
+ export declare function createEdgeAuth(rt: GatewayRuntime, obs: Observability, verifiers?: EdgeVerifierPool): EdgeAuth;
9
20
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/auth.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IAC1G,MAAM,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;CACzG;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,WAAW,GAAG,QAAQ,CA8CvG"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/auth.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IAC1G,MAAM,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;CACzG;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;IAC3C,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,SAAS,CAAC,EAAE,gBAAgB,GAAG,QAAQ,CAkD7G"}
@@ -14,7 +14,7 @@ exports.createEdgeAuth = createEdgeAuth;
14
14
  */
15
15
  const manifest_1 = require("../manifest");
16
16
  const dev_auth_1 = require("./dev-auth");
17
- function createEdgeAuth(rt, obs, verifier) {
17
+ function createEdgeAuth(rt, obs, verifiers) {
18
18
  const { traceStep } = obs;
19
19
  const internal = (req, _route, trace) => {
20
20
  // service-to-service: validate the CALLER's api key at the boundary
@@ -37,7 +37,11 @@ function createEdgeAuth(rt, obs, verifier) {
37
37
  const method = (req.method ?? 'GET').toUpperCase();
38
38
  const resolved = (0, manifest_1.authRuleFor)(policy, url0, method);
39
39
  if (resolved.mode === 'jwt') {
40
- const claims = (0, dev_auth_1.verifyDevJwt)(req, verifier);
40
+ // The verifier for THIS request's host (per-apex IdP resolution in a shared
41
+ // multi-workspace daemon; the host is the ROUTED host — trusted exactly the
42
+ // way routing itself trusts it, never taken from the token).
43
+ const reqHost = String(req.headers.host ?? '').split(':')[0].toLowerCase();
44
+ const claims = (0, dev_auth_1.verifyDevJwt)(req, verifiers?.for(reqHost));
41
45
  if (!claims) {
42
46
  traceStep(trace, 'auth', { mode: 'jwt', ok: false });
43
47
  return { ok: false, status: 401, reason: 'unauthorized', detail: 'Unauthorized: invalid or missing token' };
@@ -1 +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"}
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,CAwEzE"}
@@ -70,6 +70,19 @@ function startControlServer(deps) {
70
70
  }),
71
71
  });
72
72
  }
73
+ // A RICHER status than /list: the daemon's own identity + every registered workspace (flagged if it
74
+ // OWNS the daemon) + per-service live state (down|starting|up). The IDE plugin + `gateway status --json`
75
+ // consume this to render the active gateway, its workspaces, and each service's health at a glance.
76
+ if (req.method === 'GET' && url === '/status') {
77
+ return send(res, 200, {
78
+ daemon: { pid: process.pid, wsKey: rt.wsKey },
79
+ workspaces: rt.registry.keys().map((key) => {
80
+ const f = rt.registry.get(key);
81
+ return { key, root: f?.root, routes: f?.routes.length ?? 0, services: f?.services.length ?? 0, isDaemon: key === rt.wsKey };
82
+ }),
83
+ services: rt.registry.allServices().map((s) => ({ project: s.project, wsKey: s.wsKey, host: s.decl.host, state: s.state })),
84
+ });
85
+ }
73
86
  if (req.method === 'POST' && url === '/register') {
74
87
  const body = (await readJsonBody(req));
75
88
  if (!body.wsKey || !body.root)
@@ -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,CAwQvF"}
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,EAAY,MAAM,iBAAiB,CAAC;AA4C/D;;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,CAkRvF"}
@@ -140,6 +140,14 @@ function lensFrontendStale(spawnedAt, lastRecycleAt, sourceSetChangedAt, now, cf
140
140
  }
141
141
  function createServiceLayer(rt, obs) {
142
142
  const { emit } = obs;
143
+ // Route a service's lifecycle events (starting/up/down/cold-start/…) to ITS OWNING workspace's bus —
144
+ // mirrors the pod runtime-event routing below (svc.wsKey === rt.wsKey ? rt.eventFile : <root>/.lensmcp).
145
+ // The daemon's own services stay on rt.eventFile; a REGISTERED workspace's go to its own bus, so each
146
+ // hosted dashboard's cluster view (/<key>/api/cluster) is scoped to ITS workspace, not the daemon's
147
+ // (foodguard/cluster was empty because every service event went to the daemon's tetros bus). Two
148
+ // workspaces can share a service NAME (both have `auth`), so this MUST key off svc.wsKey, not the project.
149
+ const serviceBus = (svc) => svc.wsKey === rt.wsKey ? undefined : path.join(svc.root, '.lensmcp', 'events.jsonl');
150
+ const emitSvc = (svc, severity, title, fingerprint, raw) => emit(severity, title, fingerprint, { wsKey: svc.wsKey, ...raw }, undefined, serviceBus(svc));
143
151
  const scanNow = (pool) => { pool.scannedAt = 0; return (0, discovery_1.pickSock)(pool, rt.scanTtlMs) ? pool.socks.length : 0; };
144
152
  const spawnService = (svc) => {
145
153
  if (rt.stopped())
@@ -147,7 +155,7 @@ function createServiceLayer(rt, obs) {
147
155
  const target = `${svc.project}:${svc.decl.serveTarget ?? 'serve-hmr'}`;
148
156
  const min = Math.max(1, svc.decl.minPods ?? 1);
149
157
  console.log(`[gateway] starting ${target} (pods=${min})…`);
150
- emit('info', `service starting: ${svc.project} (${min} pod${min > 1 ? 's' : ''})`, `cluster-service:${svc.project}`, { kind: 'service-starting', project: svc.project, pods: min });
158
+ emitSvc(svc, 'info', `service starting: ${svc.project} (${min} pod${min > 1 ? 's' : ''})`, `cluster-service:${svc.project}`, { kind: 'service-starting', project: svc.project, pods: min });
151
159
  svc.state = 'starting';
152
160
  svc.spawnAt = Date.now();
153
161
  // childCount goes as a CLI option, NOT env: nx executes tasks via its
@@ -197,7 +205,7 @@ function createServiceLayer(rt, obs) {
197
205
  if (clean !== svc.lastErrorEmitSig || now - (svc.lastErrorEmitAt ?? 0) >= types_1.SERVICE_ERROR_THROTTLE_MS) {
198
206
  svc.lastErrorEmitSig = clean;
199
207
  svc.lastErrorEmitAt = now;
200
- emit('error', `service error: ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-error', project: svc.project, line: clean });
208
+ emitSvc(svc, 'error', `service error: ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-error', project: svc.project, line: clean });
201
209
  }
202
210
  }
203
211
  if (/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(line))
@@ -214,7 +222,7 @@ function createServiceLayer(rt, obs) {
214
222
  if (rt.stopped())
215
223
  return; // gateway is tearing down — its dying services' exit chatter is noise
216
224
  console.log(`[gateway] ${target} exited (${code})`);
217
- emit(code === 0 ? 'info' : 'warning', `service down: ${svc.project} (exit ${code})`, `cluster-service:${svc.project}`, { kind: 'service-down', project: svc.project, code });
225
+ emitSvc(svc, code === 0 ? 'info' : 'warning', `service down: ${svc.project} (exit ${code})`, `cluster-service:${svc.project}`, { kind: 'service-down', project: svc.project, code });
218
226
  // SELF-HEAL: respawn an UNEXPECTEDLY-crashed pod so a transient failure recovers with no manual
219
227
  // restart. A clean exit (code 0) or a sniffed BUILD failure (a code error → a fix, not a transient
220
228
  // crash) is left down — scale-from-zero (ensureUp) still spawns it on the next request. A crash-loop
@@ -225,7 +233,7 @@ function createServiceLayer(rt, obs) {
225
233
  const now = Date.now();
226
234
  svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
227
235
  if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
228
- emit('error', `service crash-looping: ${svc.project} — ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
236
+ emitSvc(svc, 'error', `service crash-looping: ${svc.project} — ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
229
237
  return;
230
238
  }
231
239
  svc.restarts.push(now);
@@ -234,7 +242,7 @@ function createServiceLayer(rt, obs) {
234
242
  if (rt.stopped() || svc.proc || svc.state !== 'down')
235
243
  return; // a request already respawned it, or teardown
236
244
  console.log(`[gateway] self-heal: respawning ${svc.project} (restart ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
237
- emit('info', `self-heal: respawning ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-respawn', project: svc.project, attempt: svc.restarts.length });
245
+ emitSvc(svc, 'info', `self-heal: respawning ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-respawn', project: svc.project, attempt: svc.restarts.length });
238
246
  spawnService(svc);
239
247
  }, backoffMs);
240
248
  });
@@ -243,7 +251,7 @@ function createServiceLayer(rt, obs) {
243
251
  const ensureUp = async (svc) => {
244
252
  if (scanNow(svc.pool) > 0) {
245
253
  if (svc.state !== 'up') {
246
- 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 });
254
+ emitSvc(svc, '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 });
247
255
  }
248
256
  svc.state = 'up';
249
257
  return true;
@@ -263,7 +271,7 @@ function createServiceLayer(rt, obs) {
263
271
  svc.buildFailed = false;
264
272
  const durationMs = svc.spawnAt ? Date.now() - svc.spawnAt : undefined;
265
273
  console.log(`[gateway] ${svc.project} up (${svc.pool.socks.length} pods${durationMs ? `, cold start ${durationMs}ms` : ''})`);
266
- 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 });
274
+ emitSvc(svc, '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 });
267
275
  return true;
268
276
  }
269
277
  if (svc.state === 'down')
@@ -288,14 +296,14 @@ function createServiceLayer(rt, obs) {
288
296
  r.on('error', () => undefined);
289
297
  r.end(JSON.stringify({ pods: cur + 1 }));
290
298
  console.log(`[gateway] autoscale ${svc.project}: ${cur} → ${cur + 1} pods (inflight ${svc.inflight})`);
291
- 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 });
299
+ emitSvc(svc, '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 });
292
300
  };
293
301
  // Idle sweeper: kill gateway-spawned lazy services after idleKillSec.
294
302
  const killSpawned = (svc, why) => {
295
303
  if (!svc.proc)
296
304
  return;
297
305
  console.log(`[gateway] ${svc.project} ${why} — scaling to zero.`);
298
- emit('info', `idle kill: ${svc.project} (${why})`, `cluster-service:${svc.project}`, { kind: 'idle-kill', project: svc.project, why });
306
+ emitSvc(svc, 'info', `idle kill: ${svc.project} (${why})`, `cluster-service:${svc.project}`, { kind: 'idle-kill', project: svc.project, why });
299
307
  const dying = svc.proc;
300
308
  // Detach the corpse's handlers BEFORE killing: a dying webpack/nx flushes
301
309
  // buffered stdout/stderr as it exits, and those `sniff` callbacks would
@@ -341,13 +349,13 @@ function createServiceLayer(rt, obs) {
341
349
  const now = Date.now();
342
350
  svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
343
351
  if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
344
- emit('error', `service crash-looping: ${svc.project} — wedged ${types_1.SERVICE_MAX_RESTARTS}+×/min, not auto-recycling (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
352
+ emitSvc(svc, 'error', `service crash-looping: ${svc.project} — wedged ${types_1.SERVICE_MAX_RESTARTS}+×/min, not auto-recycling (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
345
353
  killSpawned(svc, 'wedged (crash-loop cap) — leaving down for the next request'); // svc.proc cleared → scale-from-zero can retry
346
354
  return;
347
355
  }
348
356
  svc.restarts.push(now);
349
357
  console.log(`[gateway] self-heal: recycling WEDGED ${svc.project} (live parent, 0 live pods; ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
350
- emit('warning', `self-heal: recycling wedged ${svc.project} (0 live pods)`, `cluster-service:${svc.project}`, { kind: 'service-recycle', project: svc.project, attempt: svc.restarts.length });
358
+ emitSvc(svc, 'warning', `self-heal: recycling wedged ${svc.project} (0 live pods)`, `cluster-service:${svc.project}`, { kind: 'service-recycle', project: svc.project, attempt: svc.restarts.length });
351
359
  killSpawned(svc, 'wedged: 0 live pods — recycling'); // kills the tree + unlinks stale socks (+1200ms) + svc.proc=undefined + state='down'
352
360
  // A wedge is unexpected — bring it straight back (don't wait for a request). Respawn AFTER killSpawned's
353
361
  // deferred sock cleanup (1200ms) so the fresh child binds a clean socket path.
@@ -388,7 +396,7 @@ function createServiceLayer(rt, obs) {
388
396
  const recyclePod = (svc, reason) => {
389
397
  svc.lastStaleRecycleAt = Date.now();
390
398
  console.log(`[gateway] zombie recycle: ${svc.project} (${reason}) — stale fs view; fresh pod incoming`);
391
- emit('warning', `zombie recycle: ${svc.project} (${reason})`, `cluster-service:${svc.project}`, { kind: 'pod-recycle', project: svc.project, reason });
399
+ emitSvc(svc, 'warning', `zombie recycle: ${svc.project} (${reason})`, `cluster-service:${svc.project}`, { kind: 'pod-recycle', project: svc.project, reason });
392
400
  const eager = !!svc.decl.eager;
393
401
  killSpawned(svc, `${reason} — recycling stale pod`); // clears proc, state='down', unlinks socks (+1200ms)
394
402
  if (eager)
@@ -11,7 +11,10 @@ export type Severity = 'info' | 'warning' | 'error';
11
11
  */
12
12
  export declare const LENS_DASHBOARD_PROJECT = "(lens-dashboard)";
13
13
  export interface Observability {
14
- emit(severity: Severity, title: string, fingerprint: string, raw?: Record<string, unknown>, flowCtx?: Record<string, unknown>): void;
14
+ /** Append a cluster event to the bus. `busFile` overrides the default daemon bus for THIS event — used to
15
+ * route a REGISTERED workspace's service-lifecycle events to ITS OWN bus, so each hosted dashboard's
16
+ * cluster view is scoped to its own workspace (not the daemon's). Omit for the daemon's own events. */
17
+ emit(severity: Severity, title: string, fingerprint: string, raw?: Record<string, unknown>, flowCtx?: Record<string, unknown>, busFile?: string): void;
15
18
  eid(): string;
16
19
  beginTrace(req: http.IncomingMessage): GwTrace | undefined;
17
20
  traceStep(t: GwTrace | undefined, stepName: string, info?: Record<string, unknown>): void;
@@ -1 +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;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAEzD,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,CAmGtG"}
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;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B;;4GAEwG;IACxG,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,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvJ,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,CAoGtG"}
@@ -29,10 +29,11 @@ exports.LENS_DASHBOARD_PROJECT = '(lens-dashboard)';
29
29
  const hdr = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
30
30
  function createObservability(eventFile, opts) {
31
31
  const eid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
32
- const emit = (severity, title, fingerprint, raw, flowCtx) => {
32
+ const emit = (severity, title, fingerprint, raw, flowCtx, busFile) => {
33
+ const target = busFile ?? eventFile; // a registered workspace's service events go to ITS bus, not the daemon's
33
34
  try {
34
- fs.mkdirSync(path.dirname(eventFile), { recursive: true });
35
- fs.appendFileSync(eventFile, JSON.stringify({
35
+ fs.mkdirSync(path.dirname(target), { recursive: true });
36
+ fs.appendFileSync(target, JSON.stringify({
36
37
  id: eid(), sessionId: 'pending', timestamp: Date.now(),
37
38
  source: 'gateway', category: 'cluster', severity,
38
39
  context: { sessionId: 'pending', ...flowCtx }, fingerprint, title, raw,
@@ -1,3 +1,16 @@
1
- import type { GatewayContext, GatewayHandle, GatewayRuntimeOptions } from './types';
1
+ import { type EdgeVerifierPool } from './auth';
2
+ import type { GatewayContext, GatewayHandle, GatewayRuntimeOptions, Route } from './types';
3
+ /**
4
+ * The dev edge's JWT verifiers — matching the prod gateway's RS256/JWKS posture so the LOCAL edge rejects
5
+ * forged tokens instead of decode-accepting them, and MULTI-ISSUER for the shared daemon: every
6
+ * `auth.<apex>` host in the LIVE route table is an IdP for its apex, and `for(host)` picks the verifier
7
+ * whose apex the request host belongs to (longest apex wins) — so a foodguard token is verified against
8
+ * `auth.foodguard.local/jwks.json`, a tetros token against `auth.tetros.ai.local/jwks.json`, on the SAME
9
+ * :443 daemon. `LENSMCP_GW_JWKS_URL` (explicit, same env as prod) keeps its old meaning: one verifier for
10
+ * everything. Auth hosts come from project.json (trusted), never from a token — no SSRF/attacker-chosen
11
+ * JWKS. `for()` returns undefined when the host matches no IdP apex (→ non-HS256 tokens fail closed
12
+ * unless the insecure opt-in is set).
13
+ */
14
+ export declare function buildEdgeVerifierPool(getRoutes: () => Route[]): EdgeVerifierPool;
2
15
  export declare function startGateway(options: GatewayRuntimeOptions, context: GatewayContext): Promise<GatewayHandle>;
3
16
  //# sourceMappingURL=server.d.ts.map
@@ -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,CAmPxB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAK/D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AA0BxH;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,KAAK,EAAE,GAAG,gBAAgB,CA4ChF;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAkPxB"}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildEdgeVerifierPool = buildEdgeVerifierPool;
3
4
  exports.startGateway = startGateway;
4
5
  const tslib_1 = require("tslib");
5
6
  /**
@@ -53,31 +54,63 @@ function insecureLocalFetch(url, init) {
53
54
  });
54
55
  }
55
56
  /**
56
- * The dev edge's JWT verifiermatches the prod gateway's RS256/JWKS posture so the LOCAL edge rejects
57
- * forged tokens instead of decode-accepting them. The JWKS URL is `LENSMCP_GW_JWKS_URL` (explicit, same
58
- * env as prod) or, absent that, is AUTO-DERIVED from the trusted `auth.*` host in the gateway's own route
59
- * table (`https://<auth-host>/jwks.json`) so a standard `auth.<apex>` IdP needs zero config. The host is
60
- * taken from project.json (trusted), never from a token, so there's no SSRF/attacker-chosen JWKS. Returns
61
- * undefined when no source is found (→ non-HS256 tokens fail closed unless the insecure opt-in is set).
57
+ * The dev edge's JWT verifiersmatching the prod gateway's RS256/JWKS posture so the LOCAL edge rejects
58
+ * forged tokens instead of decode-accepting them, and MULTI-ISSUER for the shared daemon: every
59
+ * `auth.<apex>` host in the LIVE route table is an IdP for its apex, and `for(host)` picks the verifier
60
+ * whose apex the request host belongs to (longest apex wins) so a foodguard token is verified against
61
+ * `auth.foodguard.local/jwks.json`, a tetros token against `auth.tetros.ai.local/jwks.json`, on the SAME
62
+ * :443 daemon. `LENSMCP_GW_JWKS_URL` (explicit, same env as prod) keeps its old meaning: one verifier for
63
+ * everything. Auth hosts come from project.json (trusted), never from a token — no SSRF/attacker-chosen
64
+ * JWKS. `for()` returns undefined when the host matches no IdP apex (→ non-HS256 tokens fail closed
65
+ * unless the insecure opt-in is set).
62
66
  */
63
- function buildEdgeJwtVerifier(routes) {
67
+ function buildEdgeVerifierPool(getRoutes) {
64
68
  const explicit = process.env['LENSMCP_GW_JWKS_URL'];
65
- let jwksUrl = explicit;
66
- let derived = false;
67
- if (!jwksUrl) {
68
- const authHost = routes
69
- .map((r) => r.host)
70
- .find((h) => typeof h === 'string' && /^auth\./.test(h) && !h.startsWith('internal.'));
71
- if (authHost) {
72
- jwksUrl = `https://${authHost}/jwks.json`;
73
- derived = true;
74
- }
75
- }
76
- if (!jwksUrl)
77
- return undefined;
78
69
  const issuer = process.env['LENSMCP_GW_JWT_ISS'];
79
- console.log(`[gateway] edge JWT: verifying via JWKS ${jwksUrl}${derived ? ' (auto-derived from the auth route)' : ''}`);
80
- return (0, jwks_verify_1.createJwksVerifier)({ jwksUrl, ...(issuer ? { issuer } : {}), fetchImpl: insecureLocalFetch });
70
+ const byApex = new Map();
71
+ let explicitVerifier;
72
+ const make = (jwksUrl, label) => {
73
+ console.log(`[gateway] edge JWT: verifying via JWKS ${jwksUrl}${label}`);
74
+ const v = (0, jwks_verify_1.createJwksVerifier)({ jwksUrl, ...(issuer ? { issuer } : {}), fetchImpl: insecureLocalFetch });
75
+ void v.refresh().catch((e) => console.warn('[gateway] edge JWKS warm failed (will retry):', e.message));
76
+ return v;
77
+ };
78
+ return {
79
+ // PER-APEX resolution over the LIVE route table: every `auth.<apex>` host is an
80
+ // IdP for its apex, so a SHARED multi-workspace daemon verifies each workspace's
81
+ // tokens against ITS OWN IdP's JWKS. Reading the routes lazily means a workspace
82
+ // REGISTERED LATER (control-plane /register) gets its verifier with no rebuild
83
+ // hook — and `LENSMCP_GW_JWKS_URL` keeps its old meaning (one verifier for all).
84
+ // The auth host comes from project.json (trusted), never from a token — no
85
+ // attacker-chosen JWKS.
86
+ for(host) {
87
+ if (explicit)
88
+ return (explicitVerifier ??= make(explicit, ''));
89
+ const h = host.split(':')[0].toLowerCase();
90
+ let best;
91
+ for (const r of getRoutes()) {
92
+ const rh = r.host;
93
+ if (typeof rh !== 'string' || !/^auth\./.test(rh) || rh.startsWith('internal.'))
94
+ continue;
95
+ const apex = rh.slice('auth.'.length);
96
+ if ((h === apex || h.endsWith(`.${apex}`)) && (!best || apex.length > best.length))
97
+ best = apex;
98
+ }
99
+ if (!best)
100
+ return undefined;
101
+ let v = byApex.get(best);
102
+ if (!v) {
103
+ v = make(`https://auth.${best}/jwks.json`, ` (auto-derived for apex ${best})`);
104
+ byApex.set(best, v);
105
+ }
106
+ return v;
107
+ },
108
+ stop() {
109
+ explicitVerifier?.stop();
110
+ for (const v of byApex.values())
111
+ v.stop();
112
+ },
113
+ };
81
114
  }
82
115
  async function startGateway(options, context) {
83
116
  // NB: this local `https` (TLS-on flag) deliberately SHADOWS the `node:https`
@@ -178,12 +211,10 @@ async function startGateway(options, context) {
178
211
  // Edge JWT verification (mirrors the prod gateway): when a JWKS source is configured/derivable, the dev
179
212
  // edge verifies RS256 tokens by `kid` against the IdP's published keys and REJECTS anything it can't
180
213
  // verify (rogue kid / bad sig / alg:none / HS256-confusion / expired) — closing the dev decode-only bypass.
181
- const jwtVerifier = buildEdgeJwtVerifier(routes);
182
- const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs, jwtVerifier);
214
+ const edgeVerifiers = buildEdgeVerifierPool(() => rt.routes);
215
+ const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs, edgeVerifiers);
183
216
  // Warm the key cache (non-fatal, non-blocking): the IdP pod may still be booting, so a failure here just
184
217
  // means the first tokens fail closed until the on-miss/periodic refresh lands the keys (matches prod).
185
- if (jwtVerifier)
186
- void jwtVerifier.refresh().catch((e) => console.warn('[gateway] edge JWKS warm failed (will retry):', e.message));
187
218
  // Custom user middleware (the JWT seam; internal.* routes skip it) is wired as
188
219
  // the BUILT-IN FIRST `afterAuth` hook — so "custom middleware runs AFTER core
189
220
  // auth" and the `{ mode:'middleware' }` trace step are preserved.
@@ -300,7 +331,7 @@ async function startGateway(options, context) {
300
331
  clearInterval(edgeFlusher);
301
332
  clearInterval(sweeper);
302
333
  obs.emit('info', 'gateway down', 'cluster-gateway', { kind: 'gateway-down' });
303
- jwtVerifier?.stop(); // clear the JWKS periodic-refresh timer
334
+ edgeVerifiers.stop(); // clear every JWKS periodic-refresh timer
304
335
  controlServer?.close(); // tear down the control plane + its socket
305
336
  (0, workspace_registry_1.unregisterWorkspace)(wsKey); // drop this workspace from the global chooser registry (graceful stop)
306
337
  for (const svc of services)
@@ -1 +1 @@
1
- {"version":3,"file":"trust.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/trust/trust.impl.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAEpD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QACvB,QAAQ,EAAE,MAAM,CACd,MAAM,EACN;YACE,IAAI,EAAE,MAAM,CAAC;YACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;gBAAE,OAAO,CAAC,EAAE;oBAAE,OAAO,CAAC,EAAE;wBAAE,MAAM,CAAC,EAAE,KAAK,CAAC;4BAAE,IAAI,CAAC,EAAE,MAAM,CAAA;yBAAE,CAAC,CAAA;qBAAE,CAAA;iBAAE,CAAA;aAAE,CAAC,CAAC;SAC7F,CACF,CAAC;KACH,CAAC;CACH;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAA8B,aAAa,CACzC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAwE/B;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,EAAE,CAmC7F"}
1
+ {"version":3,"file":"trust.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/trust/trust.impl.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAEpD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QACvB,QAAQ,EAAE,MAAM,CACd,MAAM,EACN;YACE,IAAI,EAAE,MAAM,CAAC;YACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;gBAAE,OAAO,CAAC,EAAE;oBAAE,OAAO,CAAC,EAAE;wBAAE,MAAM,CAAC,EAAE,KAAK,CAAC;4BAAE,IAAI,CAAC,EAAE,MAAM,CAAA;yBAAE,CAAC,CAAA;qBAAE,CAAA;iBAAE,CAAA;aAAE,CAAC,CAAC;SAC7F,CACF,CAAC;KACH,CAAC;CACH;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAA8B,aAAa,CACzC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAqF/B;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,EAAE,CAmC7F"}
@@ -27,6 +27,11 @@ const basic_ssl_1 = require("../../basic-ssl");
27
27
  async function trustExecutor(options, context) {
28
28
  const cacheDir = path.join(context.root, 'node_modules', '.cache', 'davnx-webpack');
29
29
  const caPath = (0, basic_ssl_1.ensureCaSync)(cacheDir);
30
+ // HOSTS-ONLY mode (set by the CLI when this workspace is a GUEST of a shared daemon): the daemon serves
31
+ // TLS with ITS own already-trusted CA, so this workspace's CA is unused — skip the keychain step + the
32
+ // (misleading) file-based CA verify, and set up /etc/hosts + DNS only. The CLI reports the REAL served-cert
33
+ // trust afterward.
34
+ const hostsOnly = process.env['LENSMCP_TRUST_HOSTS_ONLY'] === '1';
30
35
  const hosts = collectHosts(options, context);
31
36
  if (hosts.length === 0) {
32
37
  console.log('[trust] no concrete hostnames found (only wildcards?) — keychain step only.');
@@ -38,11 +43,15 @@ async function trustExecutor(options, context) {
38
43
  console.log(` • /etc/hosts: "127.0.0.1 ${h}" and "::1 ${h}"`);
39
44
  return { success: true };
40
45
  }
41
- // --- 1) CA into the System keychain (skip when already there) -----------
42
- const present = (0, node_child_process_1.spawnSync)('security', ['find-certificate', '-c', 'lensmcp local dev CA', '/Library/Keychains/System.keychain'], {
43
- stdio: 'ignore',
44
- }).status === 0;
45
- if (present) {
46
+ // --- 1) CA into the System keychain (skip when already there, or in hosts-only/guest mode) -----------
47
+ const present = hostsOnly ||
48
+ (0, node_child_process_1.spawnSync)('security', ['find-certificate', '-c', 'lensmcp local dev CA', '/Library/Keychains/System.keychain'], {
49
+ stdio: 'ignore',
50
+ }).status === 0;
51
+ if (hostsOnly) {
52
+ console.log('[trust] hosts-only: a shared daemon serves TLS with its own CA — skipping the keychain step.');
53
+ }
54
+ else if (present) {
46
55
  console.log('[trust] CA already in the System keychain — skipping.');
47
56
  }
48
57
  else {
@@ -85,8 +94,12 @@ async function trustExecutor(options, context) {
85
94
  (0, node_child_process_1.spawnSync)('sudo', ['sh', '-c', 'dscacheutil -flushcache; killall -HUP mDNSResponder'], { stdio: 'inherit' });
86
95
  }
87
96
  // --- 4) verify ------------------------------------------------------------
88
- const verify = (0, node_child_process_1.spawnSync)('security', ['verify-cert', '-c', caPath, '-p', 'basic'], { stdio: 'ignore' });
89
- console.log(`[trust] CA trust check: ${verify.status === 0 ? 'OK' : 'NOT TRUSTED YET (browser restart may be needed)'}`);
97
+ // Skip in hosts-only mode: verifying THIS workspace's (unused) CA file would falsely report "NOT TRUSTED"
98
+ // the daemon serves with its own CA, and the CLI probes the real served cert's trust after this returns.
99
+ if (!hostsOnly) {
100
+ const verify = (0, node_child_process_1.spawnSync)('security', ['verify-cert', '-c', caPath, '-p', 'basic'], { stdio: 'ignore' });
101
+ console.log(`[trust] CA trust check: ${verify.status === 0 ? 'OK' : 'NOT TRUSTED YET (browser restart may be needed)'}`);
102
+ }
90
103
  for (const h of hosts) {
91
104
  console.log(`[trust] ${h} → https ready (hosts + cert SAN ride the gateway config)`);
92
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.16.19",
3
+ "version": "1.16.21",
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",
@@ -33,10 +33,10 @@
33
33
  "@fastify/reply-from": "^12.0.0",
34
34
  "ioredis": "^5.0.0",
35
35
  "@opentelemetry/api": "^1.9.0",
36
- "@opentelemetry/core": "^1.30.0",
37
- "@opentelemetry/resources": "^1.30.0",
38
- "@opentelemetry/sdk-trace-base": "^1.30.0",
39
- "@opentelemetry/exporter-trace-otlp-http": "^0.57.0"
36
+ "@opentelemetry/core": "^2.9.0",
37
+ "@opentelemetry/resources": "^2.9.0",
38
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
39
+ "@opentelemetry/exporter-trace-otlp-http": "^0.220.0"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "fastify": {
@@ -65,8 +65,8 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@lensmcp/node-instrumentation": "1.16.19",
69
- "@lensmcp/nx-plugin": "1.16.19",
68
+ "@lensmcp/node-instrumentation": "1.16.21",
69
+ "@lensmcp/nx-plugin": "1.16.21",
70
70
  "fork-ts-checker-webpack-plugin": "^9.0.0",
71
71
  "glob": "^11.0.0",
72
72
  "http-proxy": "^1.18.0",