@lensmcp/cluster 1.16.20 → 1.16.22
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/rate-limit.d.ts +8 -1
- package/executors/gateway/rate-limit.d.ts.map +1 -1
- package/executors/gateway/runtime/auth.d.ts +12 -1
- package/executors/gateway/runtime/auth.d.ts.map +1 -1
- package/executors/gateway/runtime/auth.js +6 -2
- package/executors/gateway/runtime/lens-children.d.ts.map +1 -1
- package/executors/gateway/runtime/lens-children.js +8 -1
- package/executors/gateway/runtime/server.d.ts +14 -1
- package/executors/gateway/runtime/server.d.ts.map +1 -1
- package/executors/gateway/runtime/server.js +58 -27
- package/package.json +3 -3
|
@@ -30,7 +30,14 @@ export interface RateLimiter {
|
|
|
30
30
|
check(key: string): RateDecision | Promise<RateDecision>;
|
|
31
31
|
stop(): void;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
/** The in-memory limiter — its `check` is SYNCHRONOUS (no round-trip). Assignable to {@link RateLimiter}
|
|
34
|
+
* (a `RateDecision` widens to `RateDecision | Promise<RateDecision>`), so it drops in wherever the async
|
|
35
|
+
* interface is expected while callers/tests can read the result without an `await`. */
|
|
36
|
+
export interface SyncRateLimiter {
|
|
37
|
+
check(key: string): RateDecision;
|
|
38
|
+
stop(): void;
|
|
39
|
+
}
|
|
40
|
+
export declare function createRateLimiter(opts: RateLimiterOptions): SyncRateLimiter;
|
|
34
41
|
/**
|
|
35
42
|
* Redis-backed token-bucket rate limiter — ONE GLOBAL budget shared by every
|
|
36
43
|
* gateway container/instance (so a per-IP limit is N-container-independent, unlike
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAAG,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE;AAE3F,MAAM,WAAW,WAAW;IAC1B;oFACgF;IAChF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACzD,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAAG,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE;AAE3F,MAAM,WAAW,WAAW;IAC1B;oFACgF;IAChF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACzD,IAAI,IAAI,IAAI,CAAC;CACd;AAED;;wFAEwF;AACxF,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAAC;IACjC,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,eAAe,CA6B3E;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,uBAAuB;IACtC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvF;AA2BD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,uBAAuB,GAAG,WAAW,CAgCzG"}
|
|
@@ -5,5 +5,16 @@ export interface EdgeAuth {
|
|
|
5
5
|
internal(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
|
|
6
6
|
public(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* HOST-AWARE verifier selection — a SHARED multi-workspace daemon fronts several
|
|
10
|
+
* IdPs (each workspace's `auth.<apex>`), so the verifier for a request is chosen
|
|
11
|
+
* by the request's host rather than fixed at boot: a foodguard token is verified
|
|
12
|
+
* against the foodguard IdP's JWKS, never the tetros keys. A single-workspace
|
|
13
|
+
* gateway resolves to its one IdP exactly as before.
|
|
14
|
+
*/
|
|
15
|
+
export interface EdgeVerifierPool {
|
|
16
|
+
for(host: string): JwtVerifier | undefined;
|
|
17
|
+
stop(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createEdgeAuth(rt: GatewayRuntime, obs: Observability, verifiers?: EdgeVerifierPool): EdgeAuth;
|
|
9
20
|
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/auth.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IAC1G,MAAM,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;CACzG;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/auth.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IAC1G,MAAM,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;CACzG;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;IAC3C,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,SAAS,CAAC,EAAE,gBAAgB,GAAG,QAAQ,CAkD7G"}
|
|
@@ -14,7 +14,7 @@ exports.createEdgeAuth = createEdgeAuth;
|
|
|
14
14
|
*/
|
|
15
15
|
const manifest_1 = require("../manifest");
|
|
16
16
|
const dev_auth_1 = require("./dev-auth");
|
|
17
|
-
function createEdgeAuth(rt, obs,
|
|
17
|
+
function createEdgeAuth(rt, obs, verifiers) {
|
|
18
18
|
const { traceStep } = obs;
|
|
19
19
|
const internal = (req, _route, trace) => {
|
|
20
20
|
// service-to-service: validate the CALLER's api key at the boundary
|
|
@@ -37,7 +37,11 @@ function createEdgeAuth(rt, obs, verifier) {
|
|
|
37
37
|
const method = (req.method ?? 'GET').toUpperCase();
|
|
38
38
|
const resolved = (0, manifest_1.authRuleFor)(policy, url0, method);
|
|
39
39
|
if (resolved.mode === 'jwt') {
|
|
40
|
-
|
|
40
|
+
// The verifier for THIS request's host (per-apex IdP resolution in a shared
|
|
41
|
+
// multi-workspace daemon; the host is the ROUTED host — trusted exactly the
|
|
42
|
+
// way routing itself trusts it, never taken from the token).
|
|
43
|
+
const reqHost = String(req.headers.host ?? '').split(':')[0].toLowerCase();
|
|
44
|
+
const claims = (0, dev_auth_1.verifyDevJwt)(req, verifiers?.for(reqHost));
|
|
41
45
|
if (!claims) {
|
|
42
46
|
traceStep(trace, 'auth', { mode: 'jwt', ok: false });
|
|
43
47
|
return { ok: false, status: 401, reason: 'unauthorized', detail: 'Unauthorized: invalid or missing token' };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAuB,MAAM,SAAS,CAAC;AAQ1F,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAM7E,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC9E;;yGAEqG;IACrG,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAAC;IAC3D;gHAC4G;IAC5G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,
|
|
1
|
+
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAuB,MAAM,SAAS,CAAC;AAQ1F,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAM7E,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC9E;;yGAEqG;IACrG,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAAC;IAC3D;gHAC4G;IAC5G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CAiUvH"}
|
|
@@ -223,7 +223,14 @@ function createLensChildren(rt, obs, options) {
|
|
|
223
223
|
LENSMCP_EVENT_FILE: eventFile,
|
|
224
224
|
LENSMCP_DASHBOARD_PORT: String(port),
|
|
225
225
|
LENSMCP_DASHBOARD_BASE: lensBase,
|
|
226
|
-
|
|
226
|
+
LENSMCP_WS_KEY: fragment.wsKey,
|
|
227
|
+
}, false, undefined,
|
|
228
|
+
// The guest dashboard's Cluster view derives its service DECLS + service-keys from `process.cwd()`
|
|
229
|
+
// (dashboard bundle: `const root = process.cwd()`), so the child MUST run in the REGISTERED
|
|
230
|
+
// workspace's root — NOT the default `rt.root` (the daemon owner's). Without this a hosted workspace's
|
|
231
|
+
// dashboard renders the DAEMON's `project.json` services (the "foodguard page shows tetros" bug); the
|
|
232
|
+
// sibling `hostWorkspaceApps` already threads `fragment.root` this way.
|
|
233
|
+
fragment.root);
|
|
227
234
|
return () => {
|
|
228
235
|
removedLabels.add(label); // stop auto-heal from resurrecting it
|
|
229
236
|
usedDashboardPorts.delete(port);
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type EdgeVerifierPool } from './auth';
|
|
2
|
+
import type { GatewayContext, GatewayHandle, GatewayRuntimeOptions, Route } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* The dev edge's JWT verifiers — matching the prod gateway's RS256/JWKS posture so the LOCAL edge rejects
|
|
5
|
+
* forged tokens instead of decode-accepting them, and MULTI-ISSUER for the shared daemon: every
|
|
6
|
+
* `auth.<apex>` host in the LIVE route table is an IdP for its apex, and `for(host)` picks the verifier
|
|
7
|
+
* whose apex the request host belongs to (longest apex wins) — so a foodguard token is verified against
|
|
8
|
+
* `auth.foodguard.local/jwks.json`, a tetros token against `auth.tetros.ai.local/jwks.json`, on the SAME
|
|
9
|
+
* :443 daemon. `LENSMCP_GW_JWKS_URL` (explicit, same env as prod) keeps its old meaning: one verifier for
|
|
10
|
+
* everything. Auth hosts come from project.json (trusted), never from a token — no SSRF/attacker-chosen
|
|
11
|
+
* JWKS. `for()` returns undefined when the host matches no IdP apex (→ non-HS256 tokens fail closed
|
|
12
|
+
* unless the insecure opt-in is set).
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildEdgeVerifierPool(getRoutes: () => Route[]): EdgeVerifierPool;
|
|
2
15
|
export declare function startGateway(options: GatewayRuntimeOptions, context: GatewayContext): Promise<GatewayHandle>;
|
|
3
16
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAK/D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AA0BxH;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,KAAK,EAAE,GAAG,gBAAgB,CA4ChF;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAkPxB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildEdgeVerifierPool = buildEdgeVerifierPool;
|
|
3
4
|
exports.startGateway = startGateway;
|
|
4
5
|
const tslib_1 = require("tslib");
|
|
5
6
|
/**
|
|
@@ -53,31 +54,63 @@ function insecureLocalFetch(url, init) {
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
|
-
* The dev edge's JWT
|
|
57
|
-
* forged tokens instead of decode-accepting them
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
57
|
+
* The dev edge's JWT verifiers — matching the prod gateway's RS256/JWKS posture so the LOCAL edge rejects
|
|
58
|
+
* forged tokens instead of decode-accepting them, and MULTI-ISSUER for the shared daemon: every
|
|
59
|
+
* `auth.<apex>` host in the LIVE route table is an IdP for its apex, and `for(host)` picks the verifier
|
|
60
|
+
* whose apex the request host belongs to (longest apex wins) — so a foodguard token is verified against
|
|
61
|
+
* `auth.foodguard.local/jwks.json`, a tetros token against `auth.tetros.ai.local/jwks.json`, on the SAME
|
|
62
|
+
* :443 daemon. `LENSMCP_GW_JWKS_URL` (explicit, same env as prod) keeps its old meaning: one verifier for
|
|
63
|
+
* everything. Auth hosts come from project.json (trusted), never from a token — no SSRF/attacker-chosen
|
|
64
|
+
* JWKS. `for()` returns undefined when the host matches no IdP apex (→ non-HS256 tokens fail closed
|
|
65
|
+
* unless the insecure opt-in is set).
|
|
62
66
|
*/
|
|
63
|
-
function
|
|
67
|
+
function buildEdgeVerifierPool(getRoutes) {
|
|
64
68
|
const explicit = process.env['LENSMCP_GW_JWKS_URL'];
|
|
65
|
-
let jwksUrl = explicit;
|
|
66
|
-
let derived = false;
|
|
67
|
-
if (!jwksUrl) {
|
|
68
|
-
const authHost = routes
|
|
69
|
-
.map((r) => r.host)
|
|
70
|
-
.find((h) => typeof h === 'string' && /^auth\./.test(h) && !h.startsWith('internal.'));
|
|
71
|
-
if (authHost) {
|
|
72
|
-
jwksUrl = `https://${authHost}/jwks.json`;
|
|
73
|
-
derived = true;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (!jwksUrl)
|
|
77
|
-
return undefined;
|
|
78
69
|
const issuer = process.env['LENSMCP_GW_JWT_ISS'];
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
const byApex = new Map();
|
|
71
|
+
let explicitVerifier;
|
|
72
|
+
const make = (jwksUrl, label) => {
|
|
73
|
+
console.log(`[gateway] edge JWT: verifying via JWKS ${jwksUrl}${label}`);
|
|
74
|
+
const v = (0, jwks_verify_1.createJwksVerifier)({ jwksUrl, ...(issuer ? { issuer } : {}), fetchImpl: insecureLocalFetch });
|
|
75
|
+
void v.refresh().catch((e) => console.warn('[gateway] edge JWKS warm failed (will retry):', e.message));
|
|
76
|
+
return v;
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
// PER-APEX resolution over the LIVE route table: every `auth.<apex>` host is an
|
|
80
|
+
// IdP for its apex, so a SHARED multi-workspace daemon verifies each workspace's
|
|
81
|
+
// tokens against ITS OWN IdP's JWKS. Reading the routes lazily means a workspace
|
|
82
|
+
// REGISTERED LATER (control-plane /register) gets its verifier with no rebuild
|
|
83
|
+
// hook — and `LENSMCP_GW_JWKS_URL` keeps its old meaning (one verifier for all).
|
|
84
|
+
// The auth host comes from project.json (trusted), never from a token — no
|
|
85
|
+
// attacker-chosen JWKS.
|
|
86
|
+
for(host) {
|
|
87
|
+
if (explicit)
|
|
88
|
+
return (explicitVerifier ??= make(explicit, ''));
|
|
89
|
+
const h = host.split(':')[0].toLowerCase();
|
|
90
|
+
let best;
|
|
91
|
+
for (const r of getRoutes()) {
|
|
92
|
+
const rh = r.host;
|
|
93
|
+
if (typeof rh !== 'string' || !/^auth\./.test(rh) || rh.startsWith('internal.'))
|
|
94
|
+
continue;
|
|
95
|
+
const apex = rh.slice('auth.'.length);
|
|
96
|
+
if ((h === apex || h.endsWith(`.${apex}`)) && (!best || apex.length > best.length))
|
|
97
|
+
best = apex;
|
|
98
|
+
}
|
|
99
|
+
if (!best)
|
|
100
|
+
return undefined;
|
|
101
|
+
let v = byApex.get(best);
|
|
102
|
+
if (!v) {
|
|
103
|
+
v = make(`https://auth.${best}/jwks.json`, ` (auto-derived for apex ${best})`);
|
|
104
|
+
byApex.set(best, v);
|
|
105
|
+
}
|
|
106
|
+
return v;
|
|
107
|
+
},
|
|
108
|
+
stop() {
|
|
109
|
+
explicitVerifier?.stop();
|
|
110
|
+
for (const v of byApex.values())
|
|
111
|
+
v.stop();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
81
114
|
}
|
|
82
115
|
async function startGateway(options, context) {
|
|
83
116
|
// NB: this local `https` (TLS-on flag) deliberately SHADOWS the `node:https`
|
|
@@ -178,12 +211,10 @@ async function startGateway(options, context) {
|
|
|
178
211
|
// Edge JWT verification (mirrors the prod gateway): when a JWKS source is configured/derivable, the dev
|
|
179
212
|
// edge verifies RS256 tokens by `kid` against the IdP's published keys and REJECTS anything it can't
|
|
180
213
|
// verify (rogue kid / bad sig / alg:none / HS256-confusion / expired) — closing the dev decode-only bypass.
|
|
181
|
-
const
|
|
182
|
-
const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs,
|
|
214
|
+
const edgeVerifiers = buildEdgeVerifierPool(() => rt.routes);
|
|
215
|
+
const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs, edgeVerifiers);
|
|
183
216
|
// Warm the key cache (non-fatal, non-blocking): the IdP pod may still be booting, so a failure here just
|
|
184
217
|
// means the first tokens fail closed until the on-miss/periodic refresh lands the keys (matches prod).
|
|
185
|
-
if (jwtVerifier)
|
|
186
|
-
void jwtVerifier.refresh().catch((e) => console.warn('[gateway] edge JWKS warm failed (will retry):', e.message));
|
|
187
218
|
// Custom user middleware (the JWT seam; internal.* routes skip it) is wired as
|
|
188
219
|
// the BUILT-IN FIRST `afterAuth` hook — so "custom middleware runs AFTER core
|
|
189
220
|
// auth" and the `{ mode:'middleware' }` trace step are preserved.
|
|
@@ -300,7 +331,7 @@ async function startGateway(options, context) {
|
|
|
300
331
|
clearInterval(edgeFlusher);
|
|
301
332
|
clearInterval(sweeper);
|
|
302
333
|
obs.emit('info', 'gateway down', 'cluster-gateway', { kind: 'gateway-down' });
|
|
303
|
-
|
|
334
|
+
edgeVerifiers.stop(); // clear every JWKS periodic-refresh timer
|
|
304
335
|
controlServer?.close(); // tear down the control plane + its socket
|
|
305
336
|
(0, workspace_registry_1.unregisterWorkspace)(wsKey); // drop this workspace from the global chooser registry (graceful stop)
|
|
306
337
|
for (const svc of services)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.22",
|
|
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.22",
|
|
69
|
+
"@lensmcp/nx-plugin": "1.16.22",
|
|
70
70
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
71
71
|
"glob": "^11.0.0",
|
|
72
72
|
"http-proxy": "^1.18.0",
|