@lensmcp/cluster 1.0.0 → 1.1.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.lib.d.ts.map +1 -1
- package/executors/gateway/gateway.lib.js +1 -0
- package/executors/gateway/health-check.d.ts +53 -0
- package/executors/gateway/health-check.d.ts.map +1 -0
- package/executors/gateway/health-check.js +66 -0
- package/executors/gateway/main.prod-gateway.js +219 -8
- package/executors/gateway/manifest.d.ts +7 -2
- package/executors/gateway/manifest.d.ts.map +1 -1
- package/executors/gateway/manifest.js +22 -9
- package/executors/gateway/metrics.d.ts +37 -0
- package/executors/gateway/metrics.d.ts.map +1 -0
- package/executors/gateway/metrics.js +56 -0
- package/executors/gateway/otel-tracing.d.ts +47 -0
- package/executors/gateway/otel-tracing.d.ts.map +1 -0
- package/executors/gateway/otel-tracing.js +73 -0
- package/executors/gateway/prod-gateway.lib.d.ts +87 -1
- package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
- package/executors/gateway/prod-gateway.lib.js +309 -31
- package/executors/gateway/providers-prod.d.ts.map +1 -1
- package/executors/gateway/providers-prod.js +46 -13
- package/executors/gateway/rate-limit.d.ts +66 -0
- package/executors/gateway/rate-limit.d.ts.map +1 -0
- package/executors/gateway/rate-limit.js +91 -0
- package/executors.json +8 -8
- package/main.devserver.js +5 -4
- package/package.json +23 -2
|
@@ -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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"}
|
|
@@ -253,6 +253,7 @@ async function startGateway(options, context) {
|
|
|
253
253
|
const text = d.toString();
|
|
254
254
|
for (const line of text.split('\n')) {
|
|
255
255
|
if (/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(line)) {
|
|
256
|
+
// eslint-disable-next-line no-control-regex -- strip ANSI color codes from child output
|
|
256
257
|
svc.lastErrors.push(line.replace(/\x1b\[[0-9;]*m/g, '').trim());
|
|
257
258
|
if (svc.lastErrors.length > 12)
|
|
258
259
|
svc.lastErrors.shift();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active upstream health-checking — opt-in. Periodically probes every known
|
|
3
|
+
* upstream URL and tracks healthy/unhealthy per endpoint, so the gateway routes
|
|
4
|
+
* a pooled service AROUND dead endpoints (instead of only discovering death
|
|
5
|
+
* passively, one failed request at a time) and the /statusz rollup reflects true
|
|
6
|
+
* liveness even with zero traffic.
|
|
7
|
+
*
|
|
8
|
+
* Liveness, not readiness: a probe is "healthy" when the endpoint ANSWERS with
|
|
9
|
+
* status < 500 — so a service without the health path still reads up on a 404;
|
|
10
|
+
* only a 5xx or a connection failure/timeout is down. New endpoints start
|
|
11
|
+
* optimistically healthy, so a not-yet-probed endpoint is never black-holed.
|
|
12
|
+
*
|
|
13
|
+
* Fail-open by design (see providers-prod `pick`): only multi-endpoint POOLS are
|
|
14
|
+
* health-filtered, and even then the gateway falls back to an unhealthy endpoint
|
|
15
|
+
* rather than 503 if a whole pool looks down — a checker false-positive must
|
|
16
|
+
* never take a service fully offline.
|
|
17
|
+
*/
|
|
18
|
+
export interface HealthCheckerOptions {
|
|
19
|
+
/** Probe path appended to each upstream URL. Default `/healthz`. */
|
|
20
|
+
path?: string;
|
|
21
|
+
/** Probe period (ms). Default 5000. */
|
|
22
|
+
intervalMs?: number;
|
|
23
|
+
/** Per-probe timeout (ms). Default 2000. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Consecutive failures before an endpoint is marked unhealthy. Default 2. */
|
|
26
|
+
unhealthyThreshold?: number;
|
|
27
|
+
/** Consecutive successes before a down endpoint is marked healthy again. Default 1. */
|
|
28
|
+
healthyThreshold?: number;
|
|
29
|
+
/** Injectable fetch (tests). Defaults to global fetch. */
|
|
30
|
+
fetchImpl?: typeof fetch;
|
|
31
|
+
/** Notified when an endpoint flips health. */
|
|
32
|
+
onChange?: (url: string, healthy: boolean) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface EndpointHealth {
|
|
35
|
+
url: string;
|
|
36
|
+
healthy: boolean;
|
|
37
|
+
fails: number;
|
|
38
|
+
lastCheckedAt: number | null;
|
|
39
|
+
}
|
|
40
|
+
export interface HealthChecker {
|
|
41
|
+
/** Set the current URL set to monitor (called on every route rebuild). */
|
|
42
|
+
track(urls: string[]): void;
|
|
43
|
+
/** Current health (default true for an untracked / not-yet-probed URL). */
|
|
44
|
+
isHealthy(url: string): boolean;
|
|
45
|
+
/** Per-endpoint health for the status page. */
|
|
46
|
+
snapshot(): EndpointHealth[];
|
|
47
|
+
/** Probe every tracked URL once (used by start()'s loop and by tests). */
|
|
48
|
+
probeOnce(): Promise<void>;
|
|
49
|
+
start(): void;
|
|
50
|
+
stop(): void;
|
|
51
|
+
}
|
|
52
|
+
export declare function createHealthChecker(opts?: HealthCheckerOptions): HealthChecker;
|
|
53
|
+
//# sourceMappingURL=health-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-check.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/health-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AAE9G,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5B,2EAA2E;IAC3E,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,+CAA+C;IAC/C,QAAQ,IAAI,cAAc,EAAE,CAAC;IAC7B,0EAA0E;IAC1E,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;CACd;AAID,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,oBAAyB,GAAG,aAAa,CAyClF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHealthChecker = createHealthChecker;
|
|
4
|
+
function createHealthChecker(opts = {}) {
|
|
5
|
+
const path = opts.path ?? '/healthz';
|
|
6
|
+
const intervalMs = opts.intervalMs ?? 5000;
|
|
7
|
+
const timeoutMs = opts.timeoutMs ?? 2000;
|
|
8
|
+
const downAt = Math.max(1, opts.unhealthyThreshold ?? 2);
|
|
9
|
+
const upAt = Math.max(1, opts.healthyThreshold ?? 1);
|
|
10
|
+
const doFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
11
|
+
const state = new Map();
|
|
12
|
+
let timer;
|
|
13
|
+
const probeUrl = (url) => url.replace(/\/+$/, '') + (path.startsWith('/') ? path : '/' + path);
|
|
14
|
+
const probe = async (url) => {
|
|
15
|
+
const st = state.get(url);
|
|
16
|
+
if (!st)
|
|
17
|
+
return;
|
|
18
|
+
let ok = false;
|
|
19
|
+
const ctl = new AbortController();
|
|
20
|
+
const t = setTimeout(() => ctl.abort(), timeoutMs);
|
|
21
|
+
try {
|
|
22
|
+
const res = await doFetch(probeUrl(url), { method: 'GET', signal: ctl.signal, redirect: 'manual' });
|
|
23
|
+
ok = res.status < 500; // answered → up (even 404); only 5xx counts as down
|
|
24
|
+
}
|
|
25
|
+
catch { /* network error / timeout → unhealthy (ok stays false) */ }
|
|
26
|
+
finally {
|
|
27
|
+
clearTimeout(t);
|
|
28
|
+
}
|
|
29
|
+
st.lastCheckedAt = Date.now();
|
|
30
|
+
if (ok) {
|
|
31
|
+
st.oks += 1;
|
|
32
|
+
st.fails = 0;
|
|
33
|
+
if (!st.healthy && st.oks >= upAt) {
|
|
34
|
+
st.healthy = true;
|
|
35
|
+
opts.onChange?.(url, true);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
st.fails += 1;
|
|
40
|
+
st.oks = 0;
|
|
41
|
+
if (st.healthy && st.fails >= downAt) {
|
|
42
|
+
st.healthy = false;
|
|
43
|
+
opts.onChange?.(url, false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const probeOnce = () => Promise.allSettled([...state.keys()].map(probe)).then(() => undefined);
|
|
48
|
+
return {
|
|
49
|
+
track(urls) {
|
|
50
|
+
const next = new Set(urls);
|
|
51
|
+
for (const u of urls)
|
|
52
|
+
if (!state.has(u))
|
|
53
|
+
state.set(u, { healthy: true, fails: 0, oks: 0, lastCheckedAt: null });
|
|
54
|
+
for (const u of [...state.keys()])
|
|
55
|
+
if (!next.has(u))
|
|
56
|
+
state.delete(u);
|
|
57
|
+
},
|
|
58
|
+
isHealthy: (url) => state.get(url)?.healthy ?? true,
|
|
59
|
+
snapshot: () => [...state.entries()].map(([url, s]) => ({ url, healthy: s.healthy, fails: s.fails, lastCheckedAt: s.lastCheckedAt })),
|
|
60
|
+
probeOnce,
|
|
61
|
+
start() { if (timer)
|
|
62
|
+
return; timer = setInterval(() => void probeOnce(), intervalMs); timer.unref?.(); void probeOnce(); },
|
|
63
|
+
stop() { if (timer)
|
|
64
|
+
clearInterval(timer); timer = undefined; },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -18,8 +18,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
* LENSMCP_GW_KEYS path to a {service: x-internal-token} JSON (the trust registry)
|
|
19
19
|
* LENSMCP_GW_TLS_KEY path to TLS private key PEM (enables https)
|
|
20
20
|
* LENSMCP_GW_TLS_CERT path to TLS cert PEM
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* jwt routes ALWAYS require auth: validated against the verifier (JWKS/HS256) when
|
|
22
|
+
* configured, else FAIL CLOSED (401). LENSMCP_GW_JWT_PRESENCE_ONLY=1 opts into a
|
|
23
|
+
* weak bearer-presence-only edge guard when no verifier is wired (not recommended).
|
|
23
24
|
* LENSMCP_EVENT_FILE set → enable the lens bus (OPT-IN; off by default)
|
|
24
25
|
*
|
|
25
26
|
* Targeted rollout (attribute-based routing; docs/design/targeted-rollout.md):
|
|
@@ -35,14 +36,68 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
35
36
|
* LENSMCP_GW_COOKIE_DOMAIN cookie Domain (e.g. ".tetros.ai" to span subdomains)
|
|
36
37
|
* LENSMCP_GW_COOKIE_MAXAGE cookie Max-Age seconds (default 31536000)
|
|
37
38
|
*
|
|
39
|
+
* Edge tuning (all optional — lib defaults are production-shaped):
|
|
40
|
+
* PORT platform-injected bind port (used when LENSMCP_GW_PORTS is unset)
|
|
41
|
+
* LENSMCP_GW_ACCESS_LOG "1" → one Cloud-Logging-shaped JSON line per request to stdout
|
|
42
|
+
* LENSMCP_GW_UPSTREAM_TLS_VERIFY "1" → verify upstream TLS certs (set for east-west on GCP;
|
|
43
|
+
* default off keeps local-CA / self-signed upstreams working)
|
|
44
|
+
* LENSMCP_GW_UPSTREAM_H2 "1" → HTTP/2 to upstreams (multiplex many MCP streams; Cloud Run speaks h2).
|
|
45
|
+
* Auto-on when mTLS is configured; set "0" to force h1.1.
|
|
46
|
+
* mTLS to upstreams (optional client cert; rides on h2). Server validation is the
|
|
47
|
+
* LENSMCP_GW_UPSTREAM_TLS_VERIFY toggle above ("option to not validate" = leave it off):
|
|
48
|
+
* LENSMCP_GW_MTLS_FROM_REDIS "1" → load cert/key/ca from shared Redis keys
|
|
49
|
+
* <prefix>:mtls:{cert,key,ca} (rotate by writing Redis; rolling-restart to apply)
|
|
50
|
+
* LENSMCP_GW_MTLS_CERT / _KEY / _CA PEM file paths (used when not loading from Redis)
|
|
51
|
+
* LENSMCP_GW_CLIENT_IP_HEADER header with the authoritative client IP from the front
|
|
52
|
+
* proxy (set `cf-connecting-ip` behind Cloudflare; feeds IP-based ABAC)
|
|
53
|
+
* LENSMCP_GW_CLIENT_IP_TRUSTED_PROXIES csv of CIDRs allowed to set that header
|
|
54
|
+
* (default loopback only — prevents off-proxy IP spoofing)
|
|
55
|
+
* LENSMCP_GW_UNDICI_CONNECTIONS per-origin connection pool size (default 256)
|
|
56
|
+
* LENSMCP_GW_UNDICI_BODY_TIMEOUT_MS undici response-body timeout; default 0 = unlimited
|
|
57
|
+
* (keeps long-lived SSE / streaming MCP responses open)
|
|
58
|
+
* LENSMCP_GW_KEEPALIVE_MS server keep-alive timeout; default 620000, kept ABOVE the GCP
|
|
59
|
+
* external ALB's 600s backend keep-alive (avoids intermittent 502s)
|
|
60
|
+
* LENSMCP_GW_REQUEST_TIMEOUT_MS inbound request timeout; default 0 = disabled (streaming)
|
|
61
|
+
*
|
|
62
|
+
* Distributed tracing (OpenTelemetry — on only when the OTLP endpoint is set):
|
|
63
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT OTLP/HTTP endpoint (Cloud Trace / Collector / X-Ray / Tempo / Jaeger).
|
|
64
|
+
* Set → gateway emits a SERVER span per request + propagates W3C traceparent downstream.
|
|
65
|
+
* OTEL_SERVICE_NAME resource service.name (default "lensmcp-gateway")
|
|
66
|
+
* OTEL_EXPORTER_OTLP_HEADERS exporter auth headers (standard OTLP env, read by the exporter)
|
|
67
|
+
* LENSMCP_GW_OTEL_SAMPLE head sample ratio 0..1 for root spans (default 1)
|
|
68
|
+
*
|
|
69
|
+
* Status rollup (for a status page — observed health, not the routing registry):
|
|
70
|
+
* LENSMCP_GW_STATUS "1" → expose GET /statusz + (with Redis) fan health to status:<svc>:<instance>
|
|
71
|
+
* LENSMCP_GW_STATUS_TOKEN if set, /statusz (and /metricsz) require ?token= or x-status-token to match
|
|
72
|
+
* LENSMCP_GW_METRICS "1" → expose token-gated /metricsz (event-loop lag / rss / cpu / req rates) for load tests
|
|
73
|
+
*
|
|
74
|
+
* Active health-checking (probe upstreams; route pools around dead endpoints):
|
|
75
|
+
* LENSMCP_GW_HEALTH_CHECK "1" → probe every upstream and steer pooled picks to healthy ones
|
|
76
|
+
* LENSMCP_GW_HEALTH_PATH probe path (default /healthz); healthy = answers with status < 500
|
|
77
|
+
* LENSMCP_GW_HEALTH_INTERVAL_MS / LENSMCP_GW_HEALTH_TIMEOUT_MS probe period / timeout
|
|
78
|
+
*
|
|
79
|
+
* Rate limit (opt-in, per client IP; defense-in-depth behind the edge throttle):
|
|
80
|
+
* LENSMCP_GW_RATE_LIMIT requests allowed per window (enables the limiter)
|
|
81
|
+
* LENSMCP_GW_RATE_WINDOW_MS window the limit refills over (default 60000)
|
|
82
|
+
* LENSMCP_GW_RATE_LIMIT_REDIS "1" → ONE GLOBAL budget across all containers via the
|
|
83
|
+
* shared registry Redis (atomic Lua token-bucket); else per-instance
|
|
84
|
+
* LENSMCP_GW_RATE_LIMIT_REDIS_URL dedicated Redis for the limiter (else reuse the registry one)
|
|
85
|
+
* LENSMCP_GW_RATE_LIMIT_TIMEOUT_MS per-request Redis budget before the fail policy (default 100)
|
|
86
|
+
* LENSMCP_GW_RATE_LIMIT_FAIL_CLOSED "1" → deny on a Redis error/timeout (default: fail OPEN)
|
|
87
|
+
*
|
|
38
88
|
* Manifests + endpoints + keys are FILE-backed here; a registry-backed
|
|
39
89
|
* ManifestProvider (Redis/ConfigMap) drops in behind the same interface.
|
|
40
90
|
*/
|
|
41
91
|
const node_fs_1 = require("node:fs");
|
|
42
92
|
const node_crypto_1 = require("node:crypto");
|
|
43
93
|
const node_path_1 = require("node:path");
|
|
94
|
+
const node_os_1 = require("node:os");
|
|
44
95
|
const prod_gateway_lib_1 = require("./prod-gateway.lib");
|
|
45
96
|
const jwks_verify_1 = require("./jwks-verify");
|
|
97
|
+
const otel_tracing_1 = require("./otel-tracing");
|
|
98
|
+
const health_check_1 = require("./health-check");
|
|
99
|
+
const rate_limit_1 = require("./rate-limit");
|
|
100
|
+
const metrics_1 = require("./metrics");
|
|
46
101
|
const registry_source_1 = require("./registry-source");
|
|
47
102
|
const providers_prod_1 = require("./providers-prod");
|
|
48
103
|
function readJson(path, fallback) {
|
|
@@ -95,7 +150,9 @@ function watchJsonFile(path, onChange, pollMs = 1000) {
|
|
|
95
150
|
}
|
|
96
151
|
async function main() {
|
|
97
152
|
const env = process.env;
|
|
98
|
-
|
|
153
|
+
// Bind ports: explicit LENSMCP_GW_PORTS wins; else honor the platform `PORT`
|
|
154
|
+
// contract (Cloud Run / buildpacks inject it); else the lib default (8080/8443).
|
|
155
|
+
const ports = (env['LENSMCP_GW_PORTS'] ?? env['PORT'] ?? '').split(',').map((s) => Number(s.trim())).filter(Boolean);
|
|
99
156
|
// Process-level safety net: a single uncaught async error must not kill the
|
|
100
157
|
// pod and 503 every in-flight request — log and keep serving.
|
|
101
158
|
process.on('uncaughtException', (err) => console.error('[gateway] uncaughtException:', err));
|
|
@@ -108,10 +165,13 @@ async function main() {
|
|
|
108
165
|
let pods;
|
|
109
166
|
let stopEndpointsWatch;
|
|
110
167
|
let registry;
|
|
168
|
+
// Reused (when present) for the status:* fan-out and shared mTLS material reads.
|
|
169
|
+
let redisClient;
|
|
111
170
|
if (redisUrl) {
|
|
112
171
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
113
172
|
const Redis = require('ioredis');
|
|
114
173
|
const client = new Redis(redisUrl);
|
|
174
|
+
redisClient = client;
|
|
115
175
|
const reg = (0, registry_source_1.createRegistryProviders)((0, registry_source_1.redisRegistrySource)(client, env['LENSMCP_GW_REDIS_PREFIX'] ? { prefix: env['LENSMCP_GW_REDIS_PREFIX'] } : {}), { onError: (e) => console.error('[gateway] registry:', e instanceof Error ? e.message : e) });
|
|
116
176
|
await reg.start(); // warm the route table before serving
|
|
117
177
|
manifests = reg.manifests;
|
|
@@ -160,10 +220,10 @@ async function main() {
|
|
|
160
220
|
: undefined;
|
|
161
221
|
// ── JWT verification: JWKS (RS256, prod) preferred, HS256 secret as fallback ──
|
|
162
222
|
// One verifier powers BOTH identify() (claims → ABAC attributes) and the edge
|
|
163
|
-
// authenticate()
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
const
|
|
223
|
+
// authenticate(). A `jwt` route with NO verifier configured FAILS CLOSED (401),
|
|
224
|
+
// unless LENSMCP_GW_JWT_PRESENCE_ONLY=1 explicitly opts into a weak bearer-presence
|
|
225
|
+
// edge guard (no signature check). Default is fail-closed, not pass-through.
|
|
226
|
+
const jwtPresenceOnly = env['LENSMCP_GW_JWT_PRESENCE_ONLY'] === '1';
|
|
167
227
|
const jwksUrl = env['LENSMCP_GW_JWKS_URL'];
|
|
168
228
|
const jwtSecret = env['LENSMCP_GW_JWT_SECRET'];
|
|
169
229
|
const jwks = jwksUrl
|
|
@@ -202,13 +262,16 @@ async function main() {
|
|
|
202
262
|
};
|
|
203
263
|
const identify = (jwks || jwtSecret) ? bearerClaims : undefined;
|
|
204
264
|
const authenticate = (mode, req) => {
|
|
205
|
-
if (mode !== 'jwt'
|
|
265
|
+
if (mode !== 'jwt')
|
|
206
266
|
return;
|
|
207
267
|
if (jwks || jwtSecret) {
|
|
208
268
|
if (!bearerClaims(req))
|
|
209
269
|
throw new Error('invalid or missing token');
|
|
210
270
|
return;
|
|
211
271
|
} // real verification
|
|
272
|
+
// No verifier configured → FAIL CLOSED, unless presence-only is explicitly opted in.
|
|
273
|
+
if (!jwtPresenceOnly)
|
|
274
|
+
throw new Error('jwt route but no verifier configured (set LENSMCP_GW_JWKS_URL or LENSMCP_GW_JWT_SECRET)');
|
|
212
275
|
if (!String(req.headers['authorization'] ?? '').startsWith('Bearer '))
|
|
213
276
|
throw new Error('missing bearer token');
|
|
214
277
|
};
|
|
@@ -248,6 +311,133 @@ async function main() {
|
|
|
248
311
|
console.error('[gateway] initial JWKS load failed (will retry in background):', e.message);
|
|
249
312
|
}
|
|
250
313
|
}
|
|
314
|
+
// ── edge tuning (all optional; sensible prod defaults live in the lib) ──
|
|
315
|
+
const accessLog = env['LENSMCP_GW_ACCESS_LOG'] === '1';
|
|
316
|
+
const clientIpHeader = env['LENSMCP_GW_CLIENT_IP_HEADER']; // e.g. cf-connecting-ip behind Cloudflare
|
|
317
|
+
// CIDRs of trusted front proxies that may set the client-IP header (default: loopback only).
|
|
318
|
+
const clientIpTrustedProxies = (env['LENSMCP_GW_CLIENT_IP_TRUSTED_PROXIES'] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
319
|
+
const undici = {};
|
|
320
|
+
if (env['LENSMCP_GW_UPSTREAM_TLS_VERIFY'] === '1')
|
|
321
|
+
undici.rejectUnauthorized = true; // "option to not validate" = leave unset
|
|
322
|
+
if (env['LENSMCP_GW_UPSTREAM_H2'] === '1')
|
|
323
|
+
undici.allowH2 = true;
|
|
324
|
+
if (env['LENSMCP_GW_UNDICI_CONNECTIONS'])
|
|
325
|
+
undici.connections = Number(env['LENSMCP_GW_UNDICI_CONNECTIONS']);
|
|
326
|
+
if (env['LENSMCP_GW_UNDICI_BODY_TIMEOUT_MS'])
|
|
327
|
+
undici.bodyTimeout = Number(env['LENSMCP_GW_UNDICI_BODY_TIMEOUT_MS']);
|
|
328
|
+
// ── mTLS client materials (optional): from the shared registry Redis, or files ──
|
|
329
|
+
if (env['LENSMCP_GW_MTLS_FROM_REDIS'] === '1' && redisClient) {
|
|
330
|
+
const p = env['LENSMCP_GW_REDIS_PREFIX'] ?? 'lensmcp:gw';
|
|
331
|
+
const [cert, key, ca] = await redisClient.mget(`${p}:mtls:cert`, `${p}:mtls:key`, `${p}:mtls:ca`);
|
|
332
|
+
if (cert)
|
|
333
|
+
undici.cert = cert;
|
|
334
|
+
if (key)
|
|
335
|
+
undici.key = key;
|
|
336
|
+
if (ca)
|
|
337
|
+
undici.ca = ca;
|
|
338
|
+
if (cert || key)
|
|
339
|
+
console.log('[gateway] mTLS materials loaded from Redis');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
if (env['LENSMCP_GW_MTLS_CERT'])
|
|
343
|
+
undici.cert = (0, node_fs_1.readFileSync)(env['LENSMCP_GW_MTLS_CERT']);
|
|
344
|
+
if (env['LENSMCP_GW_MTLS_KEY'])
|
|
345
|
+
undici.key = (0, node_fs_1.readFileSync)(env['LENSMCP_GW_MTLS_KEY']);
|
|
346
|
+
if (env['LENSMCP_GW_MTLS_CA'])
|
|
347
|
+
undici.ca = (0, node_fs_1.readFileSync)(env['LENSMCP_GW_MTLS_CA']);
|
|
348
|
+
}
|
|
349
|
+
// mTLS rides on HTTP/2 by default (unless explicitly disabled with LENSMCP_GW_UPSTREAM_H2=0).
|
|
350
|
+
if (undici.cert && undici.key) {
|
|
351
|
+
if (env['LENSMCP_GW_UPSTREAM_H2'] !== '0')
|
|
352
|
+
undici.allowH2 = true;
|
|
353
|
+
console.log(`[gateway] mTLS to upstreams ON over ${undici.allowH2 ? 'h2' : 'h1.1'} (server validation ${undici.rejectUnauthorized ? 'enabled' : 'OFF'})`);
|
|
354
|
+
}
|
|
355
|
+
const server = {};
|
|
356
|
+
if (env['LENSMCP_GW_KEEPALIVE_MS'])
|
|
357
|
+
server.keepAliveTimeout = Number(env['LENSMCP_GW_KEEPALIVE_MS']);
|
|
358
|
+
if (env['LENSMCP_GW_REQUEST_TIMEOUT_MS'])
|
|
359
|
+
server.requestTimeout = Number(env['LENSMCP_GW_REQUEST_TIMEOUT_MS']);
|
|
360
|
+
// OTel distributed tracing — on only when OTEL_EXPORTER_OTLP_ENDPOINT is set.
|
|
361
|
+
const tracing = (0, otel_tracing_1.setupTracing)({
|
|
362
|
+
...(env['OTEL_SERVICE_NAME'] ? { serviceName: env['OTEL_SERVICE_NAME'] } : {}),
|
|
363
|
+
...(env['LENSMCP_GW_OTEL_SAMPLE'] ? { sampleRatio: Number(env['LENSMCP_GW_OTEL_SAMPLE']) } : {}),
|
|
364
|
+
});
|
|
365
|
+
if (tracing)
|
|
366
|
+
console.log(`[gateway] OTLP tracing on → ${env['OTEL_EXPORTER_OTLP_ENDPOINT']}`);
|
|
367
|
+
// Status rollup: /statusz endpoint (token-guarded) + a per-instance Redis
|
|
368
|
+
// `status:<service>:<host>` fan-out (TTL'd) that a status page reads with a
|
|
369
|
+
// read-only ACL scoped to `status:*` — never the routing control-plane keys.
|
|
370
|
+
const statusEnabled = env['LENSMCP_GW_STATUS'] === '1';
|
|
371
|
+
const statusToken = env['LENSMCP_GW_STATUS_TOKEN'];
|
|
372
|
+
const instance = (0, node_os_1.hostname)();
|
|
373
|
+
const statusTtl = Math.max(3, Math.ceil((Number(env['LENSMCP_GW_TRAFFIC_FLUSH_MS'] ?? 5000) / 1000) * 3));
|
|
374
|
+
const statusRedis = redisClient;
|
|
375
|
+
const onStatus = (statusEnabled && statusRedis)
|
|
376
|
+
? (snapshot) => {
|
|
377
|
+
for (const s of snapshot) {
|
|
378
|
+
void statusRedis.set(`status:${s.service}:${instance}`, JSON.stringify({ ...s, instance, at: Date.now() }), 'EX', statusTtl).catch(() => undefined);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
: undefined;
|
|
382
|
+
if (statusEnabled && !statusToken)
|
|
383
|
+
console.warn('[gateway] LENSMCP_GW_STATUS=1 but no LENSMCP_GW_STATUS_TOKEN — /statusz stays DISABLED (404) until a token is set (fail-closed). The Redis status:* fan-out still runs.');
|
|
384
|
+
if (statusEnabled)
|
|
385
|
+
console.log(`[gateway] status on — /statusz${statusToken ? '' : ' (endpoint disabled: no token)'}${onStatus ? ' + Redis status:* fan-out' : ''}`);
|
|
386
|
+
// Active upstream health-checking — probes upstreams, routes pools around dead
|
|
387
|
+
// endpoints, and feeds true liveness into /statusz. Default off (passive only).
|
|
388
|
+
const healthChecker = env['LENSMCP_GW_HEALTH_CHECK'] === '1'
|
|
389
|
+
? (0, health_check_1.createHealthChecker)({
|
|
390
|
+
...(env['LENSMCP_GW_HEALTH_PATH'] ? { path: env['LENSMCP_GW_HEALTH_PATH'] } : {}),
|
|
391
|
+
...(env['LENSMCP_GW_HEALTH_INTERVAL_MS'] ? { intervalMs: Number(env['LENSMCP_GW_HEALTH_INTERVAL_MS']) } : {}),
|
|
392
|
+
...(env['LENSMCP_GW_HEALTH_TIMEOUT_MS'] ? { timeoutMs: Number(env['LENSMCP_GW_HEALTH_TIMEOUT_MS']) } : {}),
|
|
393
|
+
})
|
|
394
|
+
: undefined;
|
|
395
|
+
if (healthChecker)
|
|
396
|
+
console.log(`[gateway] active health-checking on (path ${env['LENSMCP_GW_HEALTH_PATH'] ?? '/healthz'})`);
|
|
397
|
+
// Per-client-IP rate limit (opt-in; defense-in-depth behind Cloudflare/Cloud Armor).
|
|
398
|
+
// LENSMCP_GW_RATE_LIMIT_REDIS=1 makes it ONE GLOBAL budget shared by every gateway
|
|
399
|
+
// container (atomic Lua token-bucket on the shared registry Redis — or a dedicated
|
|
400
|
+
// LENSMCP_GW_RATE_LIMIT_REDIS_URL); fail-open on a Redis blip. Otherwise it's the
|
|
401
|
+
// per-instance in-memory limiter (whose effective limit multiplies by container count).
|
|
402
|
+
let rateLimiter;
|
|
403
|
+
let rlRedisClient; // a dedicated client we own and must quit
|
|
404
|
+
if (env['LENSMCP_GW_RATE_LIMIT']) {
|
|
405
|
+
const limit = Number(env['LENSMCP_GW_RATE_LIMIT']);
|
|
406
|
+
const windowMs = Number(env['LENSMCP_GW_RATE_WINDOW_MS'] ?? 60_000);
|
|
407
|
+
const wantRedis = env['LENSMCP_GW_RATE_LIMIT_REDIS'] === '1' || !!env['LENSMCP_GW_RATE_LIMIT_REDIS_URL'];
|
|
408
|
+
if (wantRedis) {
|
|
409
|
+
const dedicatedUrl = env['LENSMCP_GW_RATE_LIMIT_REDIS_URL'];
|
|
410
|
+
let client;
|
|
411
|
+
if (dedicatedUrl) {
|
|
412
|
+
const Redis = require('ioredis');
|
|
413
|
+
rlRedisClient = new Redis(dedicatedUrl);
|
|
414
|
+
client = rlRedisClient;
|
|
415
|
+
}
|
|
416
|
+
else if (redisClient) {
|
|
417
|
+
client = redisClient; // reuse the registry Redis
|
|
418
|
+
}
|
|
419
|
+
if (client) {
|
|
420
|
+
const failClosed = env['LENSMCP_GW_RATE_LIMIT_FAIL_CLOSED'] === '1';
|
|
421
|
+
rateLimiter = (0, rate_limit_1.createRedisRateLimiter)(client, {
|
|
422
|
+
limit, windowMs,
|
|
423
|
+
...(env['LENSMCP_GW_RATE_LIMIT_TIMEOUT_MS'] ? { timeoutMs: Number(env['LENSMCP_GW_RATE_LIMIT_TIMEOUT_MS']) } : {}),
|
|
424
|
+
...(failClosed ? { failOpen: false } : {}),
|
|
425
|
+
});
|
|
426
|
+
console.log(`[gateway] rate limit on — ${limit} req / ${windowMs}ms GLOBAL (Redis: ${dedicatedUrl ? 'dedicated' : 'shared registry'}, fail-${failClosed ? 'closed' : 'open'}) per client IP`);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
console.warn('[gateway] LENSMCP_GW_RATE_LIMIT_REDIS set but no Redis configured (set LENSMCP_GW_REDIS_URL or LENSMCP_GW_RATE_LIMIT_REDIS_URL) — falling back to PER-INSTANCE in-memory limiting.');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (!rateLimiter) {
|
|
433
|
+
rateLimiter = (0, rate_limit_1.createRateLimiter)({ limit, windowMs });
|
|
434
|
+
console.log(`[gateway] rate limit on — ${limit} req / ${windowMs}ms per client IP (in-memory, per-instance)`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Process self-metrics (event-loop lag / rss / cpu / req rates) for load tests + monitoring.
|
|
438
|
+
const metrics = env['LENSMCP_GW_METRICS'] === '1' ? (0, metrics_1.createProcessMetrics)() : undefined;
|
|
439
|
+
if (metrics)
|
|
440
|
+
console.log(`[gateway] process metrics on — /metricsz${statusToken ? ' (token-gated)' : ' (DISABLED: needs LENSMCP_GW_STATUS_TOKEN)'}`);
|
|
251
441
|
const handle = await (0, prod_gateway_lib_1.startProdGateway)({
|
|
252
442
|
...(ports.length ? { ports } : {}),
|
|
253
443
|
manifests,
|
|
@@ -261,6 +451,19 @@ async function main() {
|
|
|
261
451
|
...(trustProxyIp ? { trustProxyIp } : {}),
|
|
262
452
|
...(cookie ? { cookie } : {}),
|
|
263
453
|
...(uidHeader ? { uidHeader } : {}),
|
|
454
|
+
...(accessLog ? { accessLog: true } : {}),
|
|
455
|
+
...(clientIpHeader ? { clientIpHeader } : {}),
|
|
456
|
+
...(clientIpTrustedProxies.length ? { clientIpTrustedProxies } : {}),
|
|
457
|
+
...(tracing ? { tracing } : {}),
|
|
458
|
+
...(statusEnabled ? { statusEndpoint: true } : {}),
|
|
459
|
+
...(statusToken ? { statusToken } : {}),
|
|
460
|
+
...(onStatus ? { onStatus } : {}),
|
|
461
|
+
...(healthChecker ? { healthChecker } : {}),
|
|
462
|
+
...(rateLimiter ? { rateLimiter } : {}),
|
|
463
|
+
...(metrics ? { metrics } : {}),
|
|
464
|
+
...(env['LENSMCP_GW_TRAFFIC_FLUSH_MS'] ? { trafficFlushMs: Number(env['LENSMCP_GW_TRAFFIC_FLUSH_MS']) } : {}),
|
|
465
|
+
...(Object.keys(undici).length ? { undici } : {}),
|
|
466
|
+
...(Object.keys(server).length ? { server } : {}),
|
|
264
467
|
});
|
|
265
468
|
console.log(`[gateway] production gateway up on ${handle.ports.join(', ')} (${handle.routes().length} routes${emit ? ', lens ON' : ''})`);
|
|
266
469
|
const shutdown = async (sig) => {
|
|
@@ -277,11 +480,19 @@ async function main() {
|
|
|
277
480
|
registry?.stop();
|
|
278
481
|
}
|
|
279
482
|
catch { /* ignore */ }
|
|
483
|
+
try {
|
|
484
|
+
await rlRedisClient?.quit();
|
|
485
|
+
}
|
|
486
|
+
catch { /* ignore */ } // the dedicated rate-limit client we created (handle.stop() stops the limiter itself)
|
|
280
487
|
try {
|
|
281
488
|
jwks?.stop();
|
|
282
489
|
}
|
|
283
490
|
catch { /* ignore */ }
|
|
284
491
|
await handle.stop();
|
|
492
|
+
try {
|
|
493
|
+
await tracing?.shutdown();
|
|
494
|
+
}
|
|
495
|
+
catch { /* ignore */ } // flush buffered spans before exit
|
|
285
496
|
process.exit(0);
|
|
286
497
|
};
|
|
287
498
|
for (const sig of ['SIGTERM', 'SIGINT'])
|
|
@@ -114,10 +114,15 @@ export interface ManifestProvider {
|
|
|
114
114
|
/** Resolves a service to a concrete upstream, with optional scale-from-zero. */
|
|
115
115
|
export interface PodProvider {
|
|
116
116
|
/** Pick an upstream for this service. `opts.version` pins a rollout cohort
|
|
117
|
-
* (sticky canary / debugging); otherwise traffic is split by cohort weight.
|
|
117
|
+
* (sticky canary / debugging); otherwise traffic is split by cohort weight.
|
|
118
|
+
* `opts.isHealthy` (when given) steers a pooled pick toward healthy endpoints
|
|
119
|
+
* — fail-open: if a whole pool looks down it still returns one. */
|
|
118
120
|
pick(service: string, opts?: {
|
|
119
121
|
version?: string;
|
|
122
|
+
isHealthy?: (url: string) => boolean;
|
|
120
123
|
}): Upstream | undefined;
|
|
124
|
+
/** All upstream URLs this provider could route to — for active health-checking. */
|
|
125
|
+
endpoints?(): string[];
|
|
121
126
|
/** Bring a scaled-to-zero service up (dev). Prod returns true (always on). */
|
|
122
127
|
ensureUp?(service: string): Promise<boolean>;
|
|
123
128
|
/** Drop a dead upstream so the next pick avoids it (refused socket / 502). */
|
|
@@ -235,7 +240,7 @@ export declare function manifestFromClusterDecl(project: string, decl: {
|
|
|
235
240
|
/** A flat, read-only attribute bag assembled per request: verified JWT claims,
|
|
236
241
|
* `ip`, `header.<name>`, `device` (sticky key), `path`, `method`. */
|
|
237
242
|
export type Attrs = Record<string, unknown>;
|
|
238
|
-
export type RuleOp = 'eq' | 'ne' | 'in' | 'nin' | 'prefix' | 'suffix' | 'contains' | 'regex' | 'exists' | 'gt' | 'lt' | 'cidr';
|
|
243
|
+
export type RuleOp = 'eq' | 'ne' | 'in' | 'nin' | 'prefix' | 'suffix' | 'contains' | 'hastoken' | 'regex' | 'exists' | 'gt' | 'lt' | 'cidr';
|
|
239
244
|
export interface Condition {
|
|
240
245
|
attr: string;
|
|
241
246
|
op: RuleOp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;6BAE6B;AAC7B,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,UAAU,CAAC;AAErD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AACD,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED;qEACqE;AACrE,MAAM,WAAW,SAAS;IACxB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,uFAAuF;IACvF,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC9B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;0EACsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;CAC1B;AAED;;oEAEoE;AACpE,MAAM,MAAM,QAAQ,GAAG,CAAC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzF;;yBAEyB;AACzB,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,IAAI,IAAI,aAAa,EAAE,CAAC;IACxB,+EAA+E;IAC/E,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC9D;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B;
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -138,8 +138,11 @@ function authRuleFor(policy, path, method) {
|
|
|
138
138
|
continue;
|
|
139
139
|
if (o.methods && !o.methods.some((x) => x.toUpperCase() === m))
|
|
140
140
|
continue;
|
|
141
|
-
const rule = o.rule ?? (o.permission ? { attr: 'perms', op: '
|
|
142
|
-
|
|
141
|
+
const rule = o.rule ?? (o.permission ? { attr: 'perms', op: 'hastoken', value: o.permission } : undefined);
|
|
142
|
+
// An override that omits `mode` INHERITS the service default — otherwise a
|
|
143
|
+
// permission-only override would resolve to mode=undefined and the gateway
|
|
144
|
+
// would skip the jwt branch entirely, silently NOT enforcing the permission.
|
|
145
|
+
return { mode: o.mode ?? policy?.default ?? 'jwt', ...(rule ? { rule } : {}) };
|
|
143
146
|
}
|
|
144
147
|
return { mode: policy?.default ?? 'jwt' };
|
|
145
148
|
}
|
|
@@ -281,7 +284,13 @@ function evalCondition(c, attrs) {
|
|
|
281
284
|
case 'nin': return !(Array.isArray(c.value) && c.value.some((x) => Array.isArray(v) ? v.some((vv) => eq(vv, x)) : eq(v, x)));
|
|
282
285
|
case 'prefix': return str(v).startsWith(str(c.value));
|
|
283
286
|
case 'suffix': return str(v).endsWith(str(c.value));
|
|
284
|
-
case 'contains': return Array.isArray(v) ? v.some((vv) => eq(vv, c.value)) : str(v).includes(str(c.value));
|
|
287
|
+
case 'contains': return Array.isArray(v) ? v.some((vv) => eq(vv, c.value)) : str(v).includes(str(c.value)); // substring / array element (general op)
|
|
288
|
+
// EXACT token in a list — a `perms`/scope STRING is delimited, so "superadmin"/"billing,adminx"
|
|
289
|
+
// must NOT satisfy hastoken:"admin". This is what `permission` compiles to (authz, no substring bypass).
|
|
290
|
+
case 'hastoken': {
|
|
291
|
+
const cv = str(c.value);
|
|
292
|
+
return Array.isArray(v) ? v.some((vv) => eq(vv, c.value)) : str(v).split(/[\s,]+/).filter(Boolean).includes(cv);
|
|
293
|
+
}
|
|
285
294
|
case 'regex': {
|
|
286
295
|
const re = compileRe(str(c.value));
|
|
287
296
|
return re ? re.test(str(v)) : false;
|
|
@@ -296,14 +305,18 @@ function evalCondition(c, attrs) {
|
|
|
296
305
|
function evalRule(node, attrs) {
|
|
297
306
|
if (!isGroup(node))
|
|
298
307
|
return evalCondition(node, attrs);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
308
|
+
// Only MEANINGFUL clauses count — an empty all:[]/any:[]/not:{} must NOT vacuously admit (fail closed),
|
|
309
|
+
// while a real `not:{cond}`-only rule still grants when its veto passes.
|
|
310
|
+
const all = Array.isArray(node.all) && node.all.length ? node.all : undefined;
|
|
311
|
+
const any = Array.isArray(node.any) && node.any.length ? node.any : undefined;
|
|
312
|
+
const not = node.not && Object.keys(node.not).length ? node.not : undefined;
|
|
313
|
+
if (not && evalRule(not, attrs))
|
|
314
|
+
return false; // veto
|
|
315
|
+
if (all && !all.every((n) => evalRule(n, attrs)))
|
|
302
316
|
return false;
|
|
303
|
-
if (
|
|
317
|
+
if (any && !any.some((n) => evalRule(n, attrs)))
|
|
304
318
|
return false;
|
|
305
|
-
|
|
306
|
-
return !!(node.all || node.any || node.not);
|
|
319
|
+
return !!(all || any || not); // grant only if ≥1 meaningful clause was present (and satisfied)
|
|
307
320
|
}
|
|
308
321
|
/** First matching rule's version (top-to-bottom), or undefined. */
|
|
309
322
|
function pickTargetedVersion(rules, attrs) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface MetricsSnapshot {
|
|
2
|
+
uptimeSec: number;
|
|
3
|
+
rssMB: number;
|
|
4
|
+
heapUsedMB: number;
|
|
5
|
+
heapTotalMB: number;
|
|
6
|
+
externalMB: number;
|
|
7
|
+
/** CPU consumed in the interval since the previous snapshot. `percent` is of one core (single-threaded gateway). */
|
|
8
|
+
cpu: {
|
|
9
|
+
userMs: number;
|
|
10
|
+
systemMs: number;
|
|
11
|
+
percent: number;
|
|
12
|
+
};
|
|
13
|
+
/** Event-loop delay over the interval (ms). p99/max are the latency-killers to watch. */
|
|
14
|
+
loopLagMs: {
|
|
15
|
+
mean: number;
|
|
16
|
+
p50: number;
|
|
17
|
+
p90: number;
|
|
18
|
+
p99: number;
|
|
19
|
+
max: number;
|
|
20
|
+
};
|
|
21
|
+
requests: {
|
|
22
|
+
total: number;
|
|
23
|
+
sinceLast: number;
|
|
24
|
+
perSec: number;
|
|
25
|
+
s2xx: number;
|
|
26
|
+
s4xx: number;
|
|
27
|
+
s5xx: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface ProcessMetrics {
|
|
31
|
+
recordRequest(status: number): void;
|
|
32
|
+
/** Read-and-reset: returns interval deltas and resets the CPU/req/loop-lag windows. */
|
|
33
|
+
snapshot(): MetricsSnapshot;
|
|
34
|
+
stop(): void;
|
|
35
|
+
}
|
|
36
|
+
export declare function createProcessMetrics(): ProcessMetrics;
|
|
37
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/metrics.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAC3E,oHAAoH;IACpH,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,yFAAyF;IACzF,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAChF,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1G;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,uFAAuF;IACvF,QAAQ,IAAI,eAAe,CAAC;IAC5B,IAAI,IAAI,IAAI,CAAC;CACd;AAKD,wBAAgB,oBAAoB,IAAI,cAAc,CAkCrD"}
|