@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.
@@ -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;AAErD,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,GAAG,QAAQ,CA8C/E"}
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
- /** Verify the end-user JWT and return its claims, or undefined. */
4
- export declare function verifyDevJwt(req: http.IncomingMessage): Record<string, unknown> | undefined;
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":"AAMA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,mEAAmE;AACnE,wBAAgB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAiC3F;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"}
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 (HS256), mirroring the prod gateway: verify the end-user token, then stamp the
8
- * trusted identity headers the services read (via GatewayTrustGuard). RS256/JWKS is prod's job;
9
- * dev shares the `AUTH_JWT_SECRET` the auth host signs with. Pure functions.
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
- /** Verify the end-user JWT and return its claims, or undefined. */
13
- function verifyDevJwt(req) {
14
- const secret = process.env['AUTH_JWT_SECRET'] ?? 'tetros-dev-secret';
15
- const auth = String((Array.isArray(req.headers['authorization']) ? req.headers['authorization'][0] : req.headers['authorization']) ?? '');
16
- if (!auth.startsWith('Bearer '))
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] = auth.slice(7).split('.');
38
+ const [h, p, s] = token.split('.');
19
39
  if (!h || !p || !s)
20
40
  return undefined;
21
- // An IdP that signs ASYMMETRIC (RS256 + JWKS — e.g. tetros's auth host) does NOT use the HS256 dev
22
- // shared-secret this dev edge assumed. The PROD gateway verifies RS256 via jwks-verify; in dev we
23
- // DECODE + exp-check the claims (no signature check) — the gateway is the only local ingress, the token
24
- // came from the trusted local IdP, and a dev IdP's ephemeral per-pod keys make in-cluster JWKS
25
- // verification unreliable. (The HS256 path below stays for services using the shared-secret convention.)
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":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAe,MAAM,SAAS,CAAC;AAEjH,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAkKxB"}
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
- const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs);
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.6",
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.6",
69
- "@lensmcp/nx-plugin": "1.16.6",
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",