@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.
- package/executors/gateway/gateway-errors.d.ts +36 -0
- package/executors/gateway/gateway-errors.d.ts.map +1 -0
- package/executors/gateway/gateway-errors.js +43 -0
- package/executors/gateway/gateway.lib.d.ts.map +1 -1
- package/executors/gateway/gateway.lib.js +24 -17
- package/executors/gateway/prod-gateway.lib.d.ts +8 -0
- package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
- package/executors/gateway/prod-gateway.lib.js +57 -25
- package/package.json +3 -3
|
@@ -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":"AAAA;;;;GAIG;AACH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG9D,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAqE,KAAK,UAAU,EAAc,MAAM,YAAY,CAAC;
|
|
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
|
-
|
|
714
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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":"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;
|
|
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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"}
|
|
@@ -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
|
-
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
69
|
-
"@lensmcp/nx-plugin": "1.
|
|
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",
|