@lensmcp/cluster 1.16.6 → 1.16.8
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/runtime/auth.d.ts +2 -1
- package/executors/gateway/runtime/auth.d.ts.map +1 -1
- package/executors/gateway/runtime/auth.js +2 -2
- package/executors/gateway/runtime/dev-auth.d.ts +9 -2
- package/executors/gateway/runtime/dev-auth.d.ts.map +1 -1
- package/executors/gateway/runtime/dev-auth.js +43 -14
- package/executors/gateway/runtime/server.d.ts.map +1 -1
- package/executors/gateway/runtime/server.js +61 -1
- package/package.json +3 -3
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { AuthVerdict, GatewayRuntime, GwTrace, Route } from './types';
|
|
2
2
|
import type { Observability } from './observability';
|
|
3
|
+
import type { JwtVerifier } from '../jwks-verify';
|
|
3
4
|
export interface EdgeAuth {
|
|
4
5
|
internal(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
|
|
5
6
|
public(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
|
|
6
7
|
}
|
|
7
|
-
export declare function createEdgeAuth(rt: GatewayRuntime, obs: Observability): EdgeAuth;
|
|
8
|
+
export declare function createEdgeAuth(rt: GatewayRuntime, obs: Observability, verifier?: JwtVerifier): EdgeAuth;
|
|
8
9
|
//# 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;
|
|
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"}
|
|
@@ -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) {
|
|
17
|
+
function createEdgeAuth(rt, obs, verifier) {
|
|
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,7 @@ function createEdgeAuth(rt, obs) {
|
|
|
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);
|
|
40
|
+
const claims = (0, dev_auth_1.verifyDevJwt)(req, verifier);
|
|
41
41
|
if (!claims) {
|
|
42
42
|
traceStep(trace, 'auth', { mode: 'jwt', ok: false });
|
|
43
43
|
return { ok: false, status: 401, reason: 'unauthorized', detail: 'Unauthorized: invalid or missing token' };
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type * as http from 'node:http';
|
|
2
2
|
import type { Attrs } from './types';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import type { JwtVerifier } from '../jwks-verify';
|
|
4
|
+
/**
|
|
5
|
+
* Verify the end-user JWT and return its claims, or undefined.
|
|
6
|
+
*
|
|
7
|
+
* @param verifier when provided (the RS256/JWKS verifier wired by the composition root), it is the ONLY
|
|
8
|
+
* acceptor — the token is verified against the issuer's published keys by `kid`, so a rogue key / unknown
|
|
9
|
+
* kid / bad signature / `alg:none` / HS256-confusion / expired token all return undefined (fail closed).
|
|
10
|
+
*/
|
|
11
|
+
export declare function verifyDevJwt(req: http.IncomingMessage, verifier?: JwtVerifier): Record<string, unknown> | undefined;
|
|
5
12
|
/** Stamp verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
|
|
6
13
|
export declare function stampIdentity(req: http.IncomingMessage, claims: Record<string, unknown>): void;
|
|
7
14
|
/** Build the ABAC attribute bag from the verified claims + request envelope. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-auth.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/dev-auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev-auth.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/dev-auth.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AASlD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAyCnH;AAED,+GAA+G;AAC/G,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAQ9F;AAED,gFAAgF;AAChF,eAAO,MAAM,QAAQ,GAAI,KAAK,IAAI,CAAC,eAAe,EAAE,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,KAKpF,CAAC"}
|
|
@@ -4,25 +4,45 @@ exports.devAttrs = void 0;
|
|
|
4
4
|
exports.verifyDevJwt = verifyDevJwt;
|
|
5
5
|
exports.stampIdentity = stampIdentity;
|
|
6
6
|
/**
|
|
7
|
-
* Dev edge auth
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Dev edge auth: verify the end-user token, then stamp the trusted identity headers the services read
|
|
8
|
+
* (via GatewayTrustGuard). It MIRRORS the prod gateway's posture — a real JWKS/RS256 verifier when one is
|
|
9
|
+
* wired (see `../jwks-verify` + the composition in `server.ts`), an HS256 shared-secret path as the legacy
|
|
10
|
+
* fallback. Pure functions (the verifier is injected, so unit tests supply their own JWKS).
|
|
11
|
+
*
|
|
12
|
+
* SECURITY (the invariant this file must hold): a `jwt` route MUST NOT accept a token whose signature the
|
|
13
|
+
* edge cannot verify. When a JWKS verifier is wired it is the SOLE acceptor — every alg goes through it, so
|
|
14
|
+
* an unknown `kid`, a bad signature, `alg:none`, an HS256-confusion token, and an expired token ALL fail
|
|
15
|
+
* closed (rejected). Without a verifier, an asymmetric (RS256/ES*) token cannot be checked (we don't hold
|
|
16
|
+
* the issuer's public keys), so it also fails closed — unless an operator EXPLICITLY opts into the old
|
|
17
|
+
* insecure decode-only path (`LENSMCP_GW_DEV_JWT_INSECURE_DECODE=1`) for local bring-up. The HS256
|
|
18
|
+
* shared-secret path stays for services using that convention (and is itself bypassed once JWKS is wired).
|
|
10
19
|
*/
|
|
11
20
|
const node_crypto_1 = require("node:crypto");
|
|
12
|
-
/**
|
|
13
|
-
function
|
|
14
|
-
const
|
|
15
|
-
const auth = String(
|
|
16
|
-
|
|
21
|
+
/** The compact JWS from the `Authorization: Bearer …` header, or undefined. */
|
|
22
|
+
function bearerToken(req) {
|
|
23
|
+
const raw = Array.isArray(req.headers['authorization']) ? req.headers['authorization'][0] : req.headers['authorization'];
|
|
24
|
+
const auth = String(raw ?? '');
|
|
25
|
+
return auth.startsWith('Bearer ') ? auth.slice(7) : undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Verify the end-user JWT and return its claims, or undefined.
|
|
29
|
+
*
|
|
30
|
+
* @param verifier when provided (the RS256/JWKS verifier wired by the composition root), it is the ONLY
|
|
31
|
+
* acceptor — the token is verified against the issuer's published keys by `kid`, so a rogue key / unknown
|
|
32
|
+
* kid / bad signature / `alg:none` / HS256-confusion / expired token all return undefined (fail closed).
|
|
33
|
+
*/
|
|
34
|
+
function verifyDevJwt(req, verifier) {
|
|
35
|
+
const token = bearerToken(req);
|
|
36
|
+
if (!token)
|
|
17
37
|
return undefined;
|
|
18
|
-
const [h, p, s] =
|
|
38
|
+
const [h, p, s] = token.split('.');
|
|
19
39
|
if (!h || !p || !s)
|
|
20
40
|
return undefined;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
41
|
+
// Preferred: a real JWKS/RS256 verifier (mirrors the prod gateway's `jwks ? jwks.verify(tok) : hs256`).
|
|
42
|
+
// When wired it is the SOLE acceptor, so every forgery vector below is closed at once.
|
|
43
|
+
if (verifier)
|
|
44
|
+
return verifier.verify(token);
|
|
45
|
+
// ── No verifier wired (legacy dev) ──────────────────────────────────────────────────────────────────
|
|
26
46
|
let alg;
|
|
27
47
|
try {
|
|
28
48
|
alg = JSON.parse(Buffer.from(h, 'base64url').toString())['alg'];
|
|
@@ -31,6 +51,13 @@ function verifyDevJwt(req) {
|
|
|
31
51
|
return undefined;
|
|
32
52
|
}
|
|
33
53
|
if (typeof alg === 'string' && alg !== 'HS256') {
|
|
54
|
+
// An asymmetric IdP (RS256 + JWKS) does NOT use the HS256 dev shared secret. Without the issuer's public
|
|
55
|
+
// keys we CANNOT verify the signature, so — unlike the old code, which decode-accepted any RS256 token
|
|
56
|
+
// (a full edge auth bypass: a rogue key or a forged `alg:none` sailed through) — we FAIL CLOSED. Wire a
|
|
57
|
+
// JWKS verifier (LENSMCP_GW_JWKS_URL, or the auto-derived auth-host JWKS) to accept RS256 tokens.
|
|
58
|
+
if (process.env['LENSMCP_GW_DEV_JWT_INSECURE_DECODE'] !== '1')
|
|
59
|
+
return undefined; // fail closed
|
|
60
|
+
// Explicit, dev-only opt-in to the legacy decode + exp-check (NO signature check) — bring-up escape hatch.
|
|
34
61
|
try {
|
|
35
62
|
const rsClaims = JSON.parse(Buffer.from(p, 'base64url').toString());
|
|
36
63
|
if (typeof rsClaims['exp'] === 'number' && Date.now() / 1000 > rsClaims['exp'])
|
|
@@ -41,6 +68,8 @@ function verifyDevJwt(req) {
|
|
|
41
68
|
return undefined;
|
|
42
69
|
}
|
|
43
70
|
}
|
|
71
|
+
// HS256 shared-secret convention (only reachable when NO JWKS verifier is wired).
|
|
72
|
+
const secret = process.env['AUTH_JWT_SECRET'] ?? 'tetros-dev-secret';
|
|
44
73
|
const expected = (0, node_crypto_1.createHmac)('sha256', secret).update(`${h}.${p}`).digest('base64url');
|
|
45
74
|
const a = Buffer.from(s);
|
|
46
75
|
const b = Buffer.from(expected);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAuBA,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,CA0KxB"}
|
|
@@ -24,6 +24,57 @@ const auth_1 = require("./auth");
|
|
|
24
24
|
const hooks_1 = require("./hooks");
|
|
25
25
|
const handler_1 = require("./handler");
|
|
26
26
|
const upgrade_1 = require("./upgrade");
|
|
27
|
+
const jwks_verify_1 = require("../jwks-verify");
|
|
28
|
+
/**
|
|
29
|
+
* A `fetch`-shaped shim for the JWKS pull that accepts the dev gateway's OWN self-signed cert. The dev
|
|
30
|
+
* IdP's JWKS is served behind the same local HTTPS front door (e.g. `https://auth.tetros.ai.local`), whose
|
|
31
|
+
* cert chains to the local basic-ssl CA — global `fetch` would reject it. Read-only GET of a public,
|
|
32
|
+
* gateway-served JWKS document; the SIGNATURE trust comes from verifying tokens against those keys, not
|
|
33
|
+
* from the transport. Falls back to plain `http` for an http URL. Dev-only (only the dev executor uses it).
|
|
34
|
+
*/
|
|
35
|
+
function insecureLocalFetch(url, init) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const mod = url.startsWith('https:') ? require('node:https') : require('node:http');
|
|
38
|
+
const req = mod.request(url, { method: 'GET', headers: init?.headers, rejectUnauthorized: false }, (res) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
res.on('data', (c) => chunks.push(c));
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
const status = res.statusCode ?? 0;
|
|
43
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
44
|
+
resolve({ ok: status >= 200 && status < 300, status, json: async () => JSON.parse(text) });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
req.on('error', reject);
|
|
48
|
+
req.end();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The dev edge's JWT verifier — matches the prod gateway's RS256/JWKS posture so the LOCAL edge rejects
|
|
53
|
+
* forged tokens instead of decode-accepting them. The JWKS URL is `LENSMCP_GW_JWKS_URL` (explicit, same
|
|
54
|
+
* env as prod) or, absent that, is AUTO-DERIVED from the trusted `auth.*` host in the gateway's own route
|
|
55
|
+
* table (`https://<auth-host>/jwks.json`) — so a standard `auth.<apex>` IdP needs zero config. The host is
|
|
56
|
+
* taken from project.json (trusted), never from a token, so there's no SSRF/attacker-chosen JWKS. Returns
|
|
57
|
+
* undefined when no source is found (→ non-HS256 tokens fail closed unless the insecure opt-in is set).
|
|
58
|
+
*/
|
|
59
|
+
function buildEdgeJwtVerifier(routes) {
|
|
60
|
+
const explicit = process.env['LENSMCP_GW_JWKS_URL'];
|
|
61
|
+
let jwksUrl = explicit;
|
|
62
|
+
let derived = false;
|
|
63
|
+
if (!jwksUrl) {
|
|
64
|
+
const authHost = routes
|
|
65
|
+
.map((r) => r.host)
|
|
66
|
+
.find((h) => typeof h === 'string' && /^auth\./.test(h) && !h.startsWith('internal.'));
|
|
67
|
+
if (authHost) {
|
|
68
|
+
jwksUrl = `https://${authHost}/jwks.json`;
|
|
69
|
+
derived = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!jwksUrl)
|
|
73
|
+
return undefined;
|
|
74
|
+
const issuer = process.env['LENSMCP_GW_JWT_ISS'];
|
|
75
|
+
console.log(`[gateway] edge JWT: verifying via JWKS ${jwksUrl}${derived ? ' (auto-derived from the auth route)' : ''}`);
|
|
76
|
+
return (0, jwks_verify_1.createJwksVerifier)({ jwksUrl, ...(issuer ? { issuer } : {}), fetchImpl: insecureLocalFetch });
|
|
77
|
+
}
|
|
27
78
|
async function startGateway(options, context) {
|
|
28
79
|
// NB: this local `https` (TLS-on flag) deliberately SHADOWS the `node:https`
|
|
29
80
|
// module — which is why TLS/agents are reached via `require('node:https')`.
|
|
@@ -65,7 +116,15 @@ async function startGateway(options, context) {
|
|
|
65
116
|
const svcLayer = (0, lifecycle_1.createServiceLayer)(rt, obs);
|
|
66
117
|
const lens = (0, lens_children_1.createLensChildren)(rt, obs, options);
|
|
67
118
|
const proxyLayer = (0, proxy_1.createProxy)(rt, obs, svcLayer);
|
|
68
|
-
|
|
119
|
+
// Edge JWT verification (mirrors the prod gateway): when a JWKS source is configured/derivable, the dev
|
|
120
|
+
// edge verifies RS256 tokens by `kid` against the IdP's published keys and REJECTS anything it can't
|
|
121
|
+
// verify (rogue kid / bad sig / alg:none / HS256-confusion / expired) — closing the dev decode-only bypass.
|
|
122
|
+
const jwtVerifier = buildEdgeJwtVerifier(routes);
|
|
123
|
+
const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs, jwtVerifier);
|
|
124
|
+
// Warm the key cache (non-fatal, non-blocking): the IdP pod may still be booting, so a failure here just
|
|
125
|
+
// means the first tokens fail closed until the on-miss/periodic refresh lands the keys (matches prod).
|
|
126
|
+
if (jwtVerifier)
|
|
127
|
+
void jwtVerifier.refresh().catch((e) => console.warn('[gateway] edge JWKS warm failed (will retry):', e.message));
|
|
69
128
|
// Custom user middleware (the JWT seam; internal.* routes skip it) is wired as
|
|
70
129
|
// the BUILT-IN FIRST `afterAuth` hook — so "custom middleware runs AFTER core
|
|
71
130
|
// auth" and the `{ mode:'middleware' }` trace step are preserved.
|
|
@@ -167,6 +226,7 @@ async function startGateway(options, context) {
|
|
|
167
226
|
clearInterval(edgeFlusher);
|
|
168
227
|
clearInterval(sweeper);
|
|
169
228
|
obs.emit('info', 'gateway down', 'cluster-gateway', { kind: 'gateway-down' });
|
|
229
|
+
jwtVerifier?.stop(); // clear the JWKS periodic-refresh timer
|
|
170
230
|
for (const svc of services)
|
|
171
231
|
svcLayer.killSpawned(svc, 'gateway shutdown');
|
|
172
232
|
// Reap the lens-mode managed children (dashboard + MCP singletons + each
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.8",
|
|
4
4
|
"description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@lensmcp/node-instrumentation": "1.16.
|
|
69
|
-
"@lensmcp/nx-plugin": "1.16.
|
|
68
|
+
"@lensmcp/node-instrumentation": "1.16.8",
|
|
69
|
+
"@lensmcp/nx-plugin": "1.16.8",
|
|
70
70
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
71
71
|
"glob": "^11.0.0",
|
|
72
72
|
"http-proxy": "^1.18.0",
|