@lensmcp/cluster 1.11.0 → 1.12.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.
@@ -0,0 +1,36 @@
1
+ import type * as http from 'node:http';
2
+ /**
3
+ * The gateway's EDGE error contract — shared by the dev (`gateway.lib.ts`) and prod
4
+ * (`prod-gateway.lib.ts`) gateways so an error the gateway generates ITSELF (before/instead of an
5
+ * upstream) has the SAME shape as an error a service returns.
6
+ *
7
+ * Two properties the gateway guarantees for every request:
8
+ * 1. **JSON, not `text/plain`.** A gateway-generated 4xx/5xx is the JSON envelope
9
+ * `{ key, status, traceId, message? }` — the same `{ key, status }` localized contract the house
10
+ * `ApiError` uses, so a browser app renders `t(key)` (a localized message) AND can read the trace id.
11
+ * `key` is a framework-NEUTRAL `gateway.*` slug; the consumer ships the `gateway.*` translations.
12
+ * `message` is a developer-facing hint (never shown to users), preserving the old plain-text detail.
13
+ * 2. **A trace id on EVERY request.** {@link ensureRequestId} mints `x-request-id` at the edge if the
14
+ * client didn't send one, so the SAME id is forwarded to the upstream (the pod adopts it as its
15
+ * trace id) AND returned to the client — making any request traceable end-to-end, not just lensmcp
16
+ * flow-traced ones.
17
+ *
18
+ * This keeps the gateway generic (no consumer-specific keys baked in beyond the neutral `gateway.*`
19
+ * namespace) while integrating cleanly with a localized-error system.
20
+ */
21
+ export type GatewayErrorReason = 'bad_request' | 'unauthorized' | 'forbidden' | 'not_found' | 'rate_limited' | 'unavailable' | 'internal';
22
+ /** Reason → stable, neutral localized key (the consumer translates the `gateway.*` namespace). */
23
+ export declare const GATEWAY_ERROR_KEY: Record<GatewayErrorReason, string>;
24
+ /**
25
+ * Ensure the request carries an `x-request-id` (mint one at the edge if absent) and return it. Because
26
+ * the gateway forwards `req.headers` to the upstream, the pod's request-context adopts THIS id as its
27
+ * `traceId`, and the gateway returns it on the response — one id, end-to-end, for ANY request.
28
+ */
29
+ export declare function ensureRequestId(req: http.IncomingMessage): string;
30
+ /**
31
+ * Build the JSON edge-error body `{ key, status, traceId, message? }`. `detail` becomes the dev-facing
32
+ * `message` (e.g. the old plain-text reason), never surfaced to end users.
33
+ */
34
+ export declare function gatewayErrorBody(status: number, reason: GatewayErrorReason, traceId: string, detail?: string): string;
35
+ export declare const GATEWAY_ERROR_CONTENT_TYPE = "application/json; charset=utf-8";
36
+ //# sourceMappingURL=gateway-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-errors.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/gateway-errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAEvC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,kBAAkB,GAC1B,aAAa,GACb,cAAc,GACd,WAAW,GACX,WAAW,GACX,cAAc,GACd,aAAa,GACb,UAAU,CAAC;AAEf,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAQhE,CAAC;AAOF;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,MAAM,CAMjE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAOR;AAED,eAAO,MAAM,0BAA0B,oCAAoC,CAAC"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GATEWAY_ERROR_CONTENT_TYPE = exports.GATEWAY_ERROR_KEY = void 0;
4
+ exports.ensureRequestId = ensureRequestId;
5
+ exports.gatewayErrorBody = gatewayErrorBody;
6
+ /** Reason → stable, neutral localized key (the consumer translates the `gateway.*` namespace). */
7
+ exports.GATEWAY_ERROR_KEY = {
8
+ bad_request: 'gateway.badRequest',
9
+ unauthorized: 'gateway.unauthorized',
10
+ forbidden: 'gateway.forbidden',
11
+ not_found: 'gateway.notFound',
12
+ rate_limited: 'gateway.rateLimited',
13
+ unavailable: 'gateway.unavailable',
14
+ internal: 'gateway.internal',
15
+ };
16
+ const headerValue = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
17
+ const mintId = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
18
+ /**
19
+ * Ensure the request carries an `x-request-id` (mint one at the edge if absent) and return it. Because
20
+ * the gateway forwards `req.headers` to the upstream, the pod's request-context adopts THIS id as its
21
+ * `traceId`, and the gateway returns it on the response — one id, end-to-end, for ANY request.
22
+ */
23
+ function ensureRequestId(req) {
24
+ const existing = headerValue(req.headers['x-request-id']);
25
+ if (existing)
26
+ return existing;
27
+ const rid = mintId();
28
+ req.headers['x-request-id'] = rid;
29
+ return rid;
30
+ }
31
+ /**
32
+ * Build the JSON edge-error body `{ key, status, traceId, message? }`. `detail` becomes the dev-facing
33
+ * `message` (e.g. the old plain-text reason), never surfaced to end users.
34
+ */
35
+ function gatewayErrorBody(status, reason, traceId, detail) {
36
+ return JSON.stringify({
37
+ key: exports.GATEWAY_ERROR_KEY[reason],
38
+ status,
39
+ traceId,
40
+ ...(detail ? { message: detail } : {}),
41
+ });
42
+ }
43
+ exports.GATEWAY_ERROR_CONTENT_TYPE = 'application/json; charset=utf-8';
@@ -1 +1 @@
1
- {"version":3,"file":"gateway.lib.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/gateway.lib.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gateway.lib.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/gateway.lib.ts"],"names":[],"mappings":"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"}
@@ -21,9 +21,21 @@ const http = tslib_1.__importStar(require("node:http"));
21
21
  const os = tslib_1.__importStar(require("node:os"));
22
22
  const path = tslib_1.__importStar(require("node:path"));
23
23
  const manifest_1 = require("./manifest");
24
+ const gateway_errors_1 = require("./gateway-errors");
24
25
  const lens_frontend_1 = require("@lensmcp/nx-plugin/lens-frontend");
25
26
  var manifest_2 = require("./manifest"); // back-compat re-export
26
27
  Object.defineProperty(exports, "hostMatches", { enumerable: true, get: function () { return manifest_2.hostMatches; } });
28
+ /**
29
+ * Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
30
+ * `text/plain`) + an `x-request-id` header — the SAME localized contract a service returns, so a browser
31
+ * app renders `t(key)` and surfaces the trace id. Mirrors the prod gateway's `sendError`.
32
+ */
33
+ const sendError = (res, req, status, reason, detail) => {
34
+ const traceId = (0, gateway_errors_1.ensureRequestId)(req);
35
+ if (!res.headersSent)
36
+ res.writeHead(status, { 'content-type': gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE, 'x-request-id': traceId });
37
+ res.end((0, gateway_errors_1.gatewayErrorBody)(status, reason, traceId, detail));
38
+ };
27
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
28
40
  const httpProxy = require('http-proxy');
29
41
  /**
@@ -710,9 +722,8 @@ async function startGateway(options, context) {
710
722
  if (svc)
711
723
  scaleUp(svc);
712
724
  if (!sock) {
713
- res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
714
- const errs = svc?.lastErrors?.length ? '\n\nService errors (from build/runtime):\n ' + svc.lastErrors.join('\n ') : '';
715
- res.end(`Service ${route.project} is not reachable (no pods in ${route.pool.dir}).${errs}\nFix the error — the watcher recovers and the next request retries automatically.`);
725
+ const errs = svc?.lastErrors?.length ? ` Service errors: ${svc.lastErrors.join('; ')}` : '';
726
+ 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.`);
716
727
  return;
717
728
  }
718
729
  req.__lensmcpPod = path.basename(sock, '.sock');
@@ -730,8 +741,7 @@ async function startGateway(options, context) {
730
741
  }
731
742
  catch { /* already gone */ } // a refused sock is a GHOST file — remove it so rescans stay truthful
732
743
  console.error(`[gateway] pod ${path.basename(sock)} of ${route.project}:`, err.message);
733
- res.statusCode = 502;
734
- res.end(`Pod ${path.basename(sock)} of ${route.project} unavailable — retry.`);
744
+ sendError(res, req, 502, 'unavailable', `Pod ${path.basename(sock)} of ${route.project} unavailable — retry.`);
735
745
  });
736
746
  return;
737
747
  }
@@ -739,8 +749,7 @@ async function startGateway(options, context) {
739
749
  // setRedirectHostRewrite's url.parse(options.target) on a 3xx (see the socketPath case above).
740
750
  proxy.web(req, res, { target: route.target, autoRewrite: typeof route.target === 'string', changeOrigin: true, agent: agentForTarget(route.target) }, (err) => {
741
751
  console.error(`[gateway] upstream ${route.project} (${route.target}):`, err.message);
742
- res.statusCode = 502;
743
- res.end(`Upstream ${route.project} unavailable.`);
752
+ sendError(res, req, 502, 'unavailable', `Upstream ${route.project} unavailable.`);
744
753
  });
745
754
  };
746
755
  // The gateway OWNS these headers — strip any client-supplied copy at ingress so identity
@@ -761,6 +770,9 @@ async function startGateway(options, context) {
761
770
  // any public route would forward a spoofed caller id to downstream audit.
762
771
  for (const h of GATEWAY_OWNED_HEADERS)
763
772
  delete req.headers[h];
773
+ // Mint a trace id for EVERY request (not just lens-traced ones): it's forwarded to the pod (adopted as
774
+ // its traceId) and returned on the response — so any request is traceable end-to-end.
775
+ (0, gateway_errors_1.ensureRequestId)(req);
764
776
  const trace = beginTrace(req);
765
777
  if (trace) {
766
778
  traceStep(trace, 'received', { host: trace.host, method: trace.method, url: trace.url });
@@ -803,8 +815,7 @@ async function startGateway(options, context) {
803
815
  res.end(JSON.stringify({ host, services: mounted.map((r) => ({ prefix: r.prefix, project: r.project, internal: !!r.internal })) }, null, 2));
804
816
  return;
805
817
  }
806
- res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
807
- res.end(mounted.length > 0
818
+ sendError(res, req, 404, 'not_found', mounted.length > 0
808
819
  ? `No service mounted at ${url}. Mounted on ${host}: ${[...new Set(mounted.map((r) => r.prefix))].join(', ')} (+ /health, /.well-known/services)`
809
820
  : 'No gateway route for this hostname.');
810
821
  return;
@@ -842,8 +853,7 @@ async function startGateway(options, context) {
842
853
  if (!caller) {
843
854
  traceStep(trace, 'auth', { mode: 'x-api-key', ok: false });
844
855
  finishTrace(trace, route.project, 401);
845
- res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8' });
846
- res.end('Invalid or missing x-api-key for internal route.');
856
+ sendError(res, req, 401, 'unauthorized', 'Invalid or missing x-api-key for internal route.');
847
857
  return;
848
858
  }
849
859
  req.headers['x-api-key-id'] = caller; // audit + lens attribution
@@ -865,15 +875,13 @@ async function startGateway(options, context) {
865
875
  if (!claims) {
866
876
  traceStep(trace, 'auth', { mode: 'jwt', ok: false });
867
877
  finishTrace(trace, route.project, 401);
868
- res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8' });
869
- res.end('Unauthorized: invalid or missing token');
878
+ sendError(res, req, 401, 'unauthorized', 'Unauthorized: invalid or missing token');
870
879
  return;
871
880
  }
872
881
  if (resolved.rule && !(0, manifest_1.evalRule)(resolved.rule, devAttrs(req, claims))) {
873
882
  traceStep(trace, 'authz', { ok: false });
874
883
  finishTrace(trace, route.project, 403);
875
- res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
876
- res.end('Forbidden: insufficient permission');
884
+ sendError(res, req, 403, 'forbidden', 'Forbidden: insufficient permission');
877
885
  return;
878
886
  }
879
887
  stampIdentity(req, claims);
@@ -894,8 +902,7 @@ async function startGateway(options, context) {
894
902
  catch (e) {
895
903
  traceStep(trace, 'auth', { mode: 'middleware', ok: false, reason: e.message });
896
904
  finishTrace(trace, route.project, 401);
897
- res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8' });
898
- res.end(`Gateway middleware rejected the request: ${e.message}`);
905
+ sendError(res, req, 401, 'unauthorized', `Gateway middleware rejected the request: ${e.message}`);
899
906
  return;
900
907
  }
901
908
  }
@@ -114,6 +114,14 @@ export interface ProdGatewayOptions {
114
114
  /** Opt-in one-JSON-line-per-request access log to stdout, shaped for Cloud
115
115
  * Logging (severity + time + message). Default off (zero overhead). */
116
116
  accessLog?: boolean;
117
+ /** Browser origins allowed to make CREDENTIALED cross-origin requests (cookies /
118
+ * `credentials: 'include'`). For an origin in this allowlist the gateway answers preflights — and
119
+ * stamps gateway-generated responses — with `Access-Control-Allow-Origin: <exact origin>` +
120
+ * `Access-Control-Allow-Credentials: true` (NEVER `*`: a credentialed `*` is forbidden by the Fetch
121
+ * spec and a cross-site read hole). Needed when a first-party browser app POSTs to a routed service
122
+ * that sets a cookie (e.g. an IdP SSO cookie set across `login.example.com → auth.example.com`).
123
+ * Exact-matched, case-insensitive. Omit (default) → no credentialed CORS (the Bearer-only edge). */
124
+ corsCredentialOrigins?: string[];
117
125
  /** Return verified subject attributes (e.g. JWT claims) for ABAC routing.
118
126
  * MUST verify (signature/exp) — forged claims must not reach the rules. */
119
127
  identify?: (req: http.IncomingMessage) => Record<string, unknown> | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"prod-gateway.lib.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/prod-gateway.lib.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prod-gateway.lib.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/prod-gateway.lib.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAIlC,OAAO,EAEL,KAAK,QAAQ,EAAc,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAqB,KAAK,KAAK,EAClG,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD;;+EAE+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC;oEACgE;IAChE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC;qFACiF;IACjF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,CAAC;IACnE,GAAG,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACtD,8EAA8E;IAC9E,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAChD,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;qFACiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;2EAQuE;IACvE,MAAM,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvP;;;;oFAIgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;sDAIkD;IAClD,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC;;oFAEgF;IAChF,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;6FACyF;IACzF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;0FACsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+FAA+F;IAC/F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;gFAE4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;IAC/C;;iFAE6E;IAC7E,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;+FAG2F;IAC3F,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;gGAC4F;IAC5F,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;uFAGmF;IACnF,MAAM,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrH;4EACwE;IACxE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;yGAMqG;IACrG,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IAGjC;gFAC4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC9E,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B;2DACuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;4EACwE;IACxE,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9E;sEACkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,KAAK,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,aAAa,EAAE,CAAC;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AA6DD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAynB3F"}
@@ -33,8 +33,23 @@ const net = tslib_1.__importStar(require("node:net"));
33
33
  const fastify_1 = tslib_1.__importDefault(require("fastify"));
34
34
  const reply_from_1 = tslib_1.__importDefault(require("@fastify/reply-from"));
35
35
  const manifest_1 = require("./manifest");
36
+ const gateway_errors_1 = require("./gateway-errors");
36
37
  const eid = () => Date.now().toString(36) + (0, node_crypto_1.randomBytes)(6).toString('hex');
37
38
  const hdr = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
39
+ /**
40
+ * Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
41
+ * `text/plain`), stamping `x-request-id` so the trace id is on the response. The SAME `{ key, status }`
42
+ * localized contract a service returns, so a browser app renders `t(key)` and surfaces the trace id.
43
+ * `detail` is the dev-facing hint (the old plain-text reason), never shown to end users.
44
+ */
45
+ const sendError = (reply, req, status, reason, detail) => {
46
+ const traceId = (0, gateway_errors_1.ensureRequestId)(req);
47
+ return reply
48
+ .code(status)
49
+ .header('x-request-id', traceId)
50
+ .type(gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE)
51
+ .send((0, gateway_errors_1.gatewayErrorBody)(status, reason, traceId, detail));
52
+ };
38
53
  // Path-canonicalization guard (CWE-288): a reverse proxy and its backend must agree
39
54
  // on the request path, or auth gets evaluated against a DIFFERENT path than the one
40
55
  // served (AWS API Gateway trailing-slash bypass, IBM API Connect CVE-2025-13915,
@@ -104,6 +119,8 @@ async function startProdGateway(opts) {
104
119
  const keyToProject = new Map(Object.entries(opts.serviceKeys ?? {}).map(([p, k]) => [k, p]));
105
120
  const versionHeader = (opts.versionHeader ?? 'x-lensmcp-version').toLowerCase();
106
121
  const accessLog = !!opts.accessLog;
122
+ // Origins allowed to make CREDENTIALED CORS requests (cookies). Exact-matched, lower-cased.
123
+ const corsCredentialOrigins = new Set((opts.corsCredentialOrigins ?? []).map((o) => o.toLowerCase()));
107
124
  const clientIpHeader = opts.clientIpHeader?.toLowerCase();
108
125
  // `clientIpHeader` is only trusted from a known front proxy. Default: loopback
109
126
  // only (a co-located cloudflared/sidecar) — an off-proxy attacker hitting the
@@ -347,10 +364,20 @@ async function startProdGateway(opts) {
347
364
  const spanByReq = new WeakMap(); // OTel span per in-flight request (ended in onResponse)
348
365
  // ---- no-route fallbacks: CORS preflight, health, discovery, 404 ----
349
366
  const setCors = (reply, req) => {
350
- // These are UNAUTHENTICATED gateway responses (404 / health / route discovery /
351
- // preflight). Reflect the Origin for convenience but DO NOT allow credentials
352
- // a reflected origin + credentials would let any site read these cross-origin.
353
- reply.header('access-control-allow-origin', String(req.headers.origin ?? '*'));
367
+ const origin = req.headers.origin;
368
+ // Credentialed allowlist: echo the EXACT origin + allow credentials (cookies). NEVER `*` with
369
+ // credentials (forbidden by the Fetch spec; a cross-site read hole). Needed for a first-party
370
+ // browser app that POSTs to a cookie-setting routed service (e.g. an IdP SSO cookie).
371
+ if (origin && corsCredentialOrigins.has(origin.toLowerCase())) {
372
+ reply.header('access-control-allow-origin', origin);
373
+ reply.header('access-control-allow-credentials', 'true');
374
+ reply.header('vary', 'Origin');
375
+ return;
376
+ }
377
+ // Otherwise — UNAUTHENTICATED gateway responses (404 / health / route discovery / preflight):
378
+ // reflect the Origin for convenience but DO NOT allow credentials — a reflected origin +
379
+ // credentials would let any site read these cross-origin.
380
+ reply.header('access-control-allow-origin', String(origin ?? '*'));
354
381
  reply.header('vary', 'Origin');
355
382
  };
356
383
  const handleNoRoute = (request, reply, host, url) => {
@@ -373,7 +400,7 @@ async function startProdGateway(opts) {
373
400
  reply.code(200).type('application/json').send(JSON.stringify({ host, routes: routes.filter((r) => !r.host || (0, manifest_1.hostMatches)(r.host, host)).map((r) => ({ host: r.host ?? '(default)', service: r.service, prefix: r.prefix, internal: !!r.internal, auth: r.auth })) }));
374
401
  return;
375
402
  }
376
- reply.code(404).type('text/plain; charset=utf-8').send(mounted.length > 0 ? `No service mounted at ${url} on ${host}.` : 'No gateway route for this hostname.');
403
+ sendError(reply, req, 404, 'not_found', mounted.length > 0 ? `No service mounted at ${url} on ${host}.` : 'No gateway route for this hostname.');
377
404
  };
378
405
  // ---- the request handler (catch-all): route → trust → auth → forward ----
379
406
  const handler = async (request, reply) => {
@@ -381,20 +408,21 @@ async function startProdGateway(opts) {
381
408
  const host = (req.headers.host || '').split(':')[0];
382
409
  const url = req.url ?? '/';
383
410
  const trace = beginTrace(req);
411
+ // Mint a trace id for EVERY request (not just lens-traced ones): it's forwarded to the upstream (the
412
+ // pod adopts it as its traceId) and returned on the response — so any request is traceable end-to-end.
413
+ (0, gateway_errors_1.ensureRequestId)(req);
384
414
  step(trace, 'received', { host, method: req.method, url });
385
415
  // Reject HTTP/0.9 — a header-less simple request has no Host, defeating host-keyed routing/policy.
386
416
  if (req.httpVersionMajor < 1) {
387
417
  finish(trace, 'gateway', 400);
388
- reply.code(400).type('text/plain').send('Bad Request');
389
- return reply;
418
+ return sendError(reply, req, 400, 'bad_request', 'Bad Request');
390
419
  }
391
420
  // Reject path-canonicalization attacks BEFORE routing/auth (proxy↔backend mismatch).
392
421
  const cpath = canonicalPath(url);
393
422
  if (cpath === undefined) {
394
423
  step(trace, 'bad-path', {});
395
424
  finish(trace, 'gateway', 400);
396
- reply.code(400).type('text/plain').send('Bad Request');
397
- return reply;
425
+ return sendError(reply, req, 400, 'bad_request', 'Bad Request');
398
426
  }
399
427
  // Rate limit (opt-in) BEFORE routing/auth, per real client IP — defense-in-depth.
400
428
  if (opts.rateLimiter) {
@@ -405,8 +433,7 @@ async function startProdGateway(opts) {
405
433
  reply.header('retry-after', String(Math.ceil(d.retryAfterMs / 1000)));
406
434
  step(trace, 'rate-limited', {});
407
435
  finish(trace, 'gateway', 429);
408
- reply.code(429).type('text/plain').send('Too Many Requests');
409
- return reply;
436
+ return sendError(reply, req, 429, 'rate_limited', 'Too Many Requests');
410
437
  }
411
438
  }
412
439
  const route = (0, manifest_1.matchRoute)(routes, host, url);
@@ -447,8 +474,7 @@ async function startProdGateway(opts) {
447
474
  if (!caller) {
448
475
  step(trace, 'auth', { mode: 'x-api-key', ok: false });
449
476
  finish(trace, route.service, 401);
450
- reply.code(401).type('text/plain').send('Invalid or missing x-api-key for internal route.');
451
- return reply;
477
+ return sendError(reply, req, 401, 'unauthorized', 'Invalid or missing x-api-key for internal route.');
452
478
  }
453
479
  req.headers['x-api-key-id'] = caller;
454
480
  delete req.headers['x-api-key'];
@@ -462,8 +488,7 @@ async function startProdGateway(opts) {
462
488
  if (!claims) {
463
489
  step(trace, 'auth', { mode: 'jwt', ok: false });
464
490
  finish(trace, route.service, 401);
465
- reply.code(401).type('text/plain').send('Unauthorized: invalid or missing token');
466
- return reply;
491
+ return sendError(reply, req, 401, 'unauthorized', 'Unauthorized: invalid or missing token');
467
492
  }
468
493
  }
469
494
  else {
@@ -473,8 +498,7 @@ async function startProdGateway(opts) {
473
498
  catch (e) {
474
499
  step(trace, 'auth', { mode: 'jwt', ok: false });
475
500
  finish(trace, route.service, 401);
476
- reply.code(401).type('text/plain').send(`Unauthorized: ${e.message}`);
477
- return reply;
501
+ return sendError(reply, req, 401, 'unauthorized', `Unauthorized: ${e.message}`);
478
502
  }
479
503
  }
480
504
  // edge ABAC: the required permission/rule must hold against the VERIFIED claims.
@@ -482,8 +506,7 @@ async function startProdGateway(opts) {
482
506
  if (resolved.rule && (!claims || !(0, manifest_1.evalRule)(resolved.rule, buildAttrs(req, request.ip, claims, undefined)))) {
483
507
  step(trace, 'authz', { ok: false });
484
508
  finish(trace, route.service, 403);
485
- reply.code(403).type('text/plain').send('Forbidden: insufficient permission');
486
- return reply;
509
+ return sendError(reply, req, 403, 'forbidden', 'Forbidden: insufficient permission');
487
510
  }
488
511
  step(trace, 'auth', { mode: 'jwt', ok: true });
489
512
  if (claims)
@@ -527,13 +550,11 @@ async function startProdGateway(opts) {
527
550
  const up = await resolveUpstream(route, trace, pinned);
528
551
  if (!up) {
529
552
  finish(trace, route.service, 503);
530
- reply.code(503).type('text/plain').send(`Service ${route.service} has no available upstream.`);
531
- return reply;
553
+ return sendError(reply, req, 503, 'unavailable', `Service ${route.service} has no available upstream.`);
532
554
  }
533
555
  if (!('url' in up)) {
534
556
  finish(trace, route.service, 502);
535
- reply.code(502).type('text/plain').send(`Service ${route.service}: socket upstreams are not supported by the prod gateway.`);
536
- return reply;
557
+ return sendError(reply, req, 502, 'unavailable', `Service ${route.service}: socket upstreams are not supported by the prod gateway.`);
537
558
  }
538
559
  const origin = new URL(up.url).origin;
539
560
  const upLabel = up.url;
@@ -547,6 +568,8 @@ async function startProdGateway(opts) {
547
568
  getUpstream: () => origin,
548
569
  rewriteHeaders: (headers) => {
549
570
  headers['x-lensmcp-upstream'] = upLabel;
571
+ if (!headers['x-request-id'])
572
+ headers['x-request-id'] = (0, gateway_errors_1.ensureRequestId)(req); // trace id on success too (if the upstream didn't echo it)
550
573
  if (up.version)
551
574
  headers['x-lensmcp-version'] = up.version; // which rollout cohort served
552
575
  // The gateway owns the edge response: drop the upstream framework leak
@@ -566,7 +589,7 @@ async function startProdGateway(opts) {
566
589
  onError: (rep, { error }) => {
567
590
  opts.pods?.drop?.(route.service, up);
568
591
  void error; // don't reflect the raw upstream error (host:port / errno) to the client
569
- rep.code(502).type('text/plain').send(`Upstream ${route.service} unavailable.`);
592
+ sendError(rep, req, 502, 'unavailable', `Upstream ${route.service} unavailable.`);
570
593
  },
571
594
  });
572
595
  };
@@ -776,7 +799,16 @@ async function startProdGateway(opts) {
776
799
  app.addHook('onSend', (request, reply, payload, done) => {
777
800
  const origin = request.headers.origin;
778
801
  if (origin && !reply.getHeader('access-control-allow-origin')) {
779
- reply.header('access-control-allow-origin', String(origin));
802
+ // A credentialed-allowlist origin gets the EXACT origin + ACAC:true (so a gateway-generated
803
+ // 401/403/5xx on a credentialed first-party origin still passes the browser's CORS check);
804
+ // everyone else gets the reflected origin WITHOUT credentials (matches setCors).
805
+ if (corsCredentialOrigins.has(origin.toLowerCase())) {
806
+ reply.header('access-control-allow-origin', origin);
807
+ reply.header('access-control-allow-credentials', 'true');
808
+ }
809
+ else {
810
+ reply.header('access-control-allow-origin', String(origin));
811
+ }
780
812
  reply.header('vary', 'Origin');
781
813
  }
782
814
  done(null, payload);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
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.11.0",
69
- "@lensmcp/nx-plugin": "1.11.0",
68
+ "@lensmcp/node-instrumentation": "1.12.0",
69
+ "@lensmcp/nx-plugin": "1.12.0",
70
70
  "fork-ts-checker-webpack-plugin": "^9.0.0",
71
71
  "glob": "^11.0.0",
72
72
  "http-proxy": "^1.18.0",