@lensmcp/cluster 1.12.0 → 1.13.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.
Files changed (98) hide show
  1. package/executors/gateway/gateway.lib.d.ts +15 -167
  2. package/executors/gateway/gateway.lib.d.ts.map +1 -1
  3. package/executors/gateway/gateway.lib.js +24 -1041
  4. package/executors/gateway/main.prod-gateway.js +74 -2
  5. package/executors/gateway/prod-gateway.lib.d.ts +19 -151
  6. package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
  7. package/executors/gateway/prod-gateway.lib.js +16 -863
  8. package/executors/gateway/prod-runtime/access-log.d.ts +9 -0
  9. package/executors/gateway/prod-runtime/access-log.d.ts.map +1 -0
  10. package/executors/gateway/prod-runtime/access-log.js +24 -0
  11. package/executors/gateway/prod-runtime/app.d.ts +23 -0
  12. package/executors/gateway/prod-runtime/app.d.ts.map +1 -0
  13. package/executors/gateway/prod-runtime/app.js +123 -0
  14. package/executors/gateway/prod-runtime/auth.d.ts +30 -0
  15. package/executors/gateway/prod-runtime/auth.d.ts.map +1 -0
  16. package/executors/gateway/prod-runtime/auth.js +51 -0
  17. package/executors/gateway/prod-runtime/cors.d.ts +19 -0
  18. package/executors/gateway/prod-runtime/cors.d.ts.map +1 -0
  19. package/executors/gateway/prod-runtime/cors.js +40 -0
  20. package/executors/gateway/prod-runtime/edge.d.ts +16 -0
  21. package/executors/gateway/prod-runtime/edge.d.ts.map +1 -0
  22. package/executors/gateway/prod-runtime/edge.js +65 -0
  23. package/executors/gateway/prod-runtime/handler.d.ts +32 -0
  24. package/executors/gateway/prod-runtime/handler.d.ts.map +1 -0
  25. package/executors/gateway/prod-runtime/handler.js +226 -0
  26. package/executors/gateway/prod-runtime/hooks.d.ts +20 -0
  27. package/executors/gateway/prod-runtime/hooks.d.ts.map +1 -0
  28. package/executors/gateway/prod-runtime/hooks.js +42 -0
  29. package/executors/gateway/prod-runtime/observability.d.ts +23 -0
  30. package/executors/gateway/prod-runtime/observability.d.ts.map +1 -0
  31. package/executors/gateway/prod-runtime/observability.js +125 -0
  32. package/executors/gateway/prod-runtime/rollout.d.ts +18 -0
  33. package/executors/gateway/prod-runtime/rollout.d.ts.map +1 -0
  34. package/executors/gateway/prod-runtime/rollout.js +103 -0
  35. package/executors/gateway/prod-runtime/routing.d.ts +4 -0
  36. package/executors/gateway/prod-runtime/routing.d.ts.map +1 -0
  37. package/executors/gateway/prod-runtime/routing.js +40 -0
  38. package/executors/gateway/prod-runtime/server.d.ts +3 -0
  39. package/executors/gateway/prod-runtime/server.d.ts.map +1 -0
  40. package/executors/gateway/prod-runtime/server.js +79 -0
  41. package/executors/gateway/prod-runtime/trust.d.ts +12 -0
  42. package/executors/gateway/prod-runtime/trust.d.ts.map +1 -0
  43. package/executors/gateway/prod-runtime/trust.js +32 -0
  44. package/executors/gateway/prod-runtime/types.d.ts +241 -0
  45. package/executors/gateway/prod-runtime/types.d.ts.map +1 -0
  46. package/executors/gateway/prod-runtime/types.js +2 -0
  47. package/executors/gateway/prod-runtime/upgrade.d.ts +21 -0
  48. package/executors/gateway/prod-runtime/upgrade.d.ts.map +1 -0
  49. package/executors/gateway/prod-runtime/upgrade.js +116 -0
  50. package/executors/gateway/prod-runtime/upstream.d.ts +13 -0
  51. package/executors/gateway/prod-runtime/upstream.d.ts.map +1 -0
  52. package/executors/gateway/prod-runtime/upstream.js +21 -0
  53. package/executors/gateway/runtime/auth.d.ts +8 -0
  54. package/executors/gateway/runtime/auth.d.ts.map +1 -0
  55. package/executors/gateway/runtime/auth.js +60 -0
  56. package/executors/gateway/runtime/dev-auth.d.ts +9 -0
  57. package/executors/gateway/runtime/dev-auth.d.ts.map +1 -0
  58. package/executors/gateway/runtime/dev-auth.js +79 -0
  59. package/executors/gateway/runtime/discovery.d.ts +17 -0
  60. package/executors/gateway/runtime/discovery.d.ts.map +1 -0
  61. package/executors/gateway/runtime/discovery.js +111 -0
  62. package/executors/gateway/runtime/edge.d.ts +31 -0
  63. package/executors/gateway/runtime/edge.d.ts.map +1 -0
  64. package/executors/gateway/runtime/edge.js +47 -0
  65. package/executors/gateway/runtime/handler.d.ts +24 -0
  66. package/executors/gateway/runtime/handler.d.ts.map +1 -0
  67. package/executors/gateway/runtime/handler.js +165 -0
  68. package/executors/gateway/runtime/hooks.d.ts +23 -0
  69. package/executors/gateway/runtime/hooks.d.ts.map +1 -0
  70. package/executors/gateway/runtime/hooks.js +55 -0
  71. package/executors/gateway/runtime/lens-children.d.ts +13 -0
  72. package/executors/gateway/runtime/lens-children.d.ts.map +1 -0
  73. package/executors/gateway/runtime/lens-children.js +136 -0
  74. package/executors/gateway/runtime/lifecycle.d.ts +13 -0
  75. package/executors/gateway/runtime/lifecycle.d.ts.map +1 -0
  76. package/executors/gateway/runtime/lifecycle.js +179 -0
  77. package/executors/gateway/runtime/observability.d.ts +17 -0
  78. package/executors/gateway/runtime/observability.d.ts.map +1 -0
  79. package/executors/gateway/runtime/observability.js +107 -0
  80. package/executors/gateway/runtime/proxy.d.ts +20 -0
  81. package/executors/gateway/runtime/proxy.d.ts.map +1 -0
  82. package/executors/gateway/runtime/proxy.js +113 -0
  83. package/executors/gateway/runtime/scope.d.ts +16 -0
  84. package/executors/gateway/runtime/scope.d.ts.map +1 -0
  85. package/executors/gateway/runtime/scope.js +68 -0
  86. package/executors/gateway/runtime/server.d.ts +3 -0
  87. package/executors/gateway/runtime/server.d.ts.map +1 -0
  88. package/executors/gateway/runtime/server.js +168 -0
  89. package/executors/gateway/runtime/service-keys.d.ts +11 -0
  90. package/executors/gateway/runtime/service-keys.d.ts.map +1 -0
  91. package/executors/gateway/runtime/service-keys.js +49 -0
  92. package/executors/gateway/runtime/types.d.ts +234 -0
  93. package/executors/gateway/runtime/types.d.ts.map +1 -0
  94. package/executors/gateway/runtime/types.js +2 -0
  95. package/executors/gateway/runtime/upgrade.d.ts +22 -0
  96. package/executors/gateway/runtime/upgrade.d.ts.map +1 -0
  97. package/executors/gateway/runtime/upgrade.js +63 -0
  98. package/package.json +3 -3
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startProdGateway = startProdGateway;
4
+ const observability_1 = require("./observability");
5
+ const routing_1 = require("./routing");
6
+ const rollout_1 = require("./rollout");
7
+ const upstream_1 = require("./upstream");
8
+ const auth_1 = require("./auth");
9
+ const hooks_1 = require("./hooks");
10
+ const cors_1 = require("./cors");
11
+ const handler_1 = require("./handler");
12
+ const upgrade_1 = require("./upgrade");
13
+ const app_1 = require("./app");
14
+ async function startProdGateway(opts) {
15
+ const ports = opts.ports?.length ? opts.ports : [opts.tls ? 8443 : 8080];
16
+ // Default authenticator FAILS CLOSED: a jwt route with no authenticator wired is rejected.
17
+ const authenticate = opts.authenticate ?? ((mode) => {
18
+ if (mode === 'jwt')
19
+ throw new Error('jwt route but no authenticator configured');
20
+ });
21
+ // ---- option-derived config (resolved once) ----
22
+ const versionHeader = (opts.versionHeader ?? 'x-lensmcp-version').toLowerCase();
23
+ const accessLog = !!opts.accessLog;
24
+ const corsCredentialOrigins = new Set((opts.corsCredentialOrigins ?? []).map((o) => o.toLowerCase()));
25
+ const keyToProject = new Map(Object.entries(opts.serviceKeys ?? {}).map(([p, k]) => [k, p]));
26
+ // ---- route table + observability (lazy getRoutes breaks the table↔obs cycle) ----
27
+ // A holder, so obs can read the routes lazily while `table` stays a const (assigned once,
28
+ // before any request — obs's getter only runs at request time).
29
+ const tableRef = {};
30
+ const obs = (0, observability_1.createObservability)(opts, () => tableRef.current.routes);
31
+ const table = (0, routing_1.createRouteTable)(opts, obs);
32
+ tableRef.current = table;
33
+ const rt = { opts, table, keyToProject, versionHeader, accessLog, corsCredentialOrigins };
34
+ // ---- layers ----
35
+ const rollout = (0, rollout_1.createRollout)(opts);
36
+ const upstream = (0, upstream_1.createUpstream)(opts, obs);
37
+ const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs, rollout, authenticate);
38
+ const hooks = (0, hooks_1.createHooks)(opts.hooks ?? {});
39
+ const reqState = new WeakMap();
40
+ const spanByReq = new WeakMap();
41
+ const ctxByReq = new WeakMap();
42
+ const cors = (0, cors_1.createCors)(rt, reqState);
43
+ const handler = (0, handler_1.createHandler)({ rt, obs, cors, auth: edgeAuth, upstream, rollout, hooks, reqState, spanByReq, ctxByReq });
44
+ const upgrade = (0, upgrade_1.createUpgrade)({ rt, auth: edgeAuth, upstream, hooks });
45
+ const { buildApp } = (0, app_1.createApp)({ rt, obs, cors, hooks, handler, upgrade, reqState, spanByReq, ctxByReq });
46
+ const flusher = obs.startFlusher();
47
+ table.rebuild(table.manifests()); // emit initial gateway-up (+ healthChecker.track)
48
+ // ---- listeners ----
49
+ const apps = [];
50
+ const boundPorts = [];
51
+ for (const port of ports) {
52
+ const app = await buildApp();
53
+ await app.listen({ port, host: '0.0.0.0' });
54
+ const addr = app.server.address();
55
+ const bound = typeof addr === 'object' && addr ? addr.port : port;
56
+ boundPorts.push(bound);
57
+ console.log(`[gateway] listening on ${opts.tls ? 'https' : 'http'}://0.0.0.0:${bound}`);
58
+ apps.push(app);
59
+ }
60
+ opts.healthChecker?.start(); // begin probing now that the route table (and tracked URLs) is warm
61
+ let stopped = false;
62
+ return {
63
+ ports: boundPorts,
64
+ routes: () => table.routes,
65
+ status: obs.buildStatusSnapshot,
66
+ stop: async () => {
67
+ if (stopped)
68
+ return;
69
+ stopped = true;
70
+ clearInterval(flusher);
71
+ table.stop();
72
+ opts.healthChecker?.stop();
73
+ opts.rateLimiter?.stop();
74
+ opts.metrics?.stop();
75
+ obs.emit('info', 'gateway down', 'gateway-down', { kind: 'gateway-down' });
76
+ await Promise.all(apps.map((a) => a.close())); // reply-from closes its undici pool on close
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The trust boundary (verbatim dev model). The gateway OWNS the identity/trust
3
+ * headers — drop any client-supplied copy at ingress so identity can only
4
+ * originate from a verified token, then re-stamp on forward. Pure.
5
+ */
6
+ import type * as http from 'node:http';
7
+ export declare const TRUST_HEADERS: string[];
8
+ export declare const HOP_BY_HOP: string[];
9
+ export declare const stripTrust: (req: http.IncomingMessage) => void;
10
+ /** Stamp the verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
11
+ export declare const stampIdentity: (req: http.IncomingMessage, claims: Record<string, unknown>) => void;
12
+ //# sourceMappingURL=trust.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/trust.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAGvC,eAAO,MAAM,aAAa,UAAyG,CAAC;AAIpI,eAAO,MAAM,UAAU,UAAmD,CAAC;AAE3E,eAAO,MAAM,UAAU,GAAI,KAAK,IAAI,CAAC,eAAe,KAAG,IAGtD,CAAC;AAEF,mHAAmH;AACnH,eAAO,MAAM,aAAa,GAAI,KAAK,IAAI,CAAC,eAAe,EAAE,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAQ1F,CAAC"}
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stampIdentity = exports.stripTrust = exports.HOP_BY_HOP = exports.TRUST_HEADERS = void 0;
4
+ // Drop any client-supplied copy at ingress so identity can only originate from a verified token.
5
+ exports.TRUST_HEADERS = ['x-internal-token', 'x-api-key-id', 'x-user-id', 'x-tenant-id', 'x-user-roles', 'x-user-permissions'];
6
+ // Also drop client-supplied hop-by-hop control headers: a `Connection: x-internal-token`
7
+ // would otherwise make undici strip the gateway's OWN injected trust headers before the
8
+ // upstream (CVE-2026-33805). The proxy manages its own upstream connection semantics.
9
+ exports.HOP_BY_HOP = ['connection', 'proxy-connection', 'keep-alive'];
10
+ const stripTrust = (req) => {
11
+ for (const h of exports.TRUST_HEADERS)
12
+ delete req.headers[h];
13
+ for (const h of exports.HOP_BY_HOP)
14
+ delete req.headers[h];
15
+ };
16
+ exports.stripTrust = stripTrust;
17
+ /** Stamp the verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
18
+ const stampIdentity = (req, claims) => {
19
+ // Only stamp SCALAR claims — an object/array sub/tid must never coerce to
20
+ // "[object Object]" / a CSV tenant header (claim type-confusion).
21
+ const set = (k, v) => { if ((typeof v === 'string' && v !== '') || typeof v === 'number')
22
+ req.headers[k] = String(v); };
23
+ set('x-user-id', claims['sub']);
24
+ set('x-tenant-id', claims['tid']);
25
+ const roles = claims['roles'];
26
+ if (Array.isArray(roles) && roles.length)
27
+ req.headers['x-user-roles'] = roles.map(String).join(',');
28
+ const perms = claims['perms'];
29
+ if (Array.isArray(perms) && perms.length)
30
+ req.headers['x-user-permissions'] = perms.map(String).join(',');
31
+ };
32
+ exports.stampIdentity = stampIdentity;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Shared types for the layered PROD gateway runtime. TYPES-ONLY (imports only
3
+ * `node:*`, `fastify`, `../manifest`, the provider modules, and `../gateway-errors`
4
+ * — never a runtime layer) so every layer imports it without a cycle. The
5
+ * composition root (`server.ts`) is the only place concrete layers are wired.
6
+ */
7
+ import type * as http from 'node:http';
8
+ import type { FastifyReply, FastifyRequest } from 'fastify';
9
+ import type { AuthMode, Attrs, ManifestProvider, PodProvider, Route, RouteManifest } from '../manifest';
10
+ import type { GatewayTracer } from '../otel-tracing';
11
+ import type { HealthChecker } from '../health-check';
12
+ import type { RateLimiter } from '../rate-limit';
13
+ import type { ProcessMetrics } from '../metrics';
14
+ export type { Attrs };
15
+ /** Per-service health rollup, derived from observed traffic over a rolling window.
16
+ * `status` is inferred from the recent error rate (passive — not an active probe):
17
+ * healthy <5% err · degraded 5–50% · down ≥50% · idle (no recent traffic). */
18
+ export interface ServiceStatus {
19
+ service: string;
20
+ hosts: string[];
21
+ status: 'healthy' | 'degraded' | 'down' | 'idle';
22
+ requests: number;
23
+ errorRate: number;
24
+ avgMs: number;
25
+ versions: string[];
26
+ lastSeen: number | null;
27
+ }
28
+ export interface ProdGatewayOptions {
29
+ /** Ports to bind. Default `[8080]` (or `[8443]` with TLS). One Fastify
30
+ * instance per port; all share the route table + providers. */
31
+ ports?: number[];
32
+ manifests: ManifestProvider;
33
+ /** Resolves a service → upstream when a route has no direct `upstream`. */
34
+ pods?: PodProvider;
35
+ /** service → its `x-internal-token` (from a k8s Secret / Vault in prod). */
36
+ serviceKeys?: Record<string, string>;
37
+ /** Validate a `jwt`-mode request; THROW to reject (→ 401). Default: a
38
+ * pass-through with a one-time warning — wire a real JWKS validator in prod. */
39
+ authenticate?: (mode: AuthMode, req: http.IncomingMessage) => void;
40
+ tls?: {
41
+ key: string | Buffer;
42
+ cert: string | Buffer;
43
+ };
44
+ /** Lens bus sink — OPT-IN. Omit (default) for zero observability overhead. */
45
+ emit?: (event: Record<string, unknown>) => void;
46
+ /** Traffic-edge flush period (ms). Default 5000. */
47
+ trafficFlushMs?: number;
48
+ /** Request header that PINS a rollout cohort (sticky canary / debugging).
49
+ * Default `x-lensmcp-version`. The chosen version is echoed on the response. */
50
+ versionHeader?: string;
51
+ /** undici pool tuning passed to reply-from. `bodyTimeout: 0` (default) keeps
52
+ * long-lived SSE / streamable-HTTP MCP responses open — undici's 300s default
53
+ * would otherwise abort an idle stream. `rejectUnauthorized` defaults false
54
+ * (local-CA / self-signed upstreams); set true for verified east-west on GCP.
55
+ * `allowH2` multiplexes many concurrent streams over few HTTP/2 connections
56
+ * (Cloud Run supports h2 end-to-end) — useful when one upstream fans many MCP streams.
57
+ * `cert`/`key` present a client certificate to upstreams (mTLS); `ca` trusts a
58
+ * private CA for the server side. `rejectUnauthorized:false` keeps mTLS while
59
+ * skipping server-cert validation (self-signed internal services). */
60
+ undici?: {
61
+ connections?: number;
62
+ pipelining?: number;
63
+ keepAliveTimeout?: number;
64
+ bodyTimeout?: number;
65
+ headersTimeout?: number;
66
+ rejectUnauthorized?: boolean;
67
+ allowH2?: boolean;
68
+ cert?: string | Buffer;
69
+ key?: string | Buffer;
70
+ ca?: string | Buffer;
71
+ };
72
+ /** Header carrying the AUTHORITATIVE client IP from a trusted front proxy
73
+ * (e.g. `cf-connecting-ip` behind Cloudflare). When set, the `ip` ABAC
74
+ * attribute comes from this header instead of the (multi-proxy, spoofable)
75
+ * X-Forwarded-For chain. Safe only when the origin is locked to that proxy —
76
+ * which Cloudflare Tunnel guarantees (there is no public origin to bypass). */
77
+ clientIpHeader?: string;
78
+ /** CIDRs of trusted front proxies. `clientIpHeader` is honored ONLY when the
79
+ * immediate peer (socket address) is in this set — otherwise the header is
80
+ * ignored and the socket IP is used (prevents spoofing the `ip` ABAC attribute
81
+ * by hitting the origin directly). Default when omitted: loopback only
82
+ * (matches a co-located cloudflared/sidecar). */
83
+ clientIpTrustedProxies?: string[];
84
+ /** OpenTelemetry tracer (from `setupTracing`). When set, each request gets a
85
+ * SERVER span whose context is propagated downstream (W3C `traceparent`), so
86
+ * the gateway hop stitches into the distributed trace. Omit → no span work. */
87
+ tracing?: GatewayTracer;
88
+ /** Expose `GET /statusz` — the per-service health rollup as JSON. Requires
89
+ * `statusToken` too (fail-closed: served only when both are set). Default off (404). */
90
+ statusEndpoint?: boolean;
91
+ /** Token `/statusz` requires (via `?token=` or `x-status-token`), compared in
92
+ * constant time. REQUIRED for the endpoint to serve — no token → /statusz is 404. */
93
+ statusToken?: string;
94
+ /** How many flush windows the rolling status sums over (default 12 → ~60s at the 5s flush). */
95
+ statusWindows?: number;
96
+ /** Called each flush window with the per-service status snapshot — wire it to a
97
+ * Redis `status:*` fan-out / status page. The gateway is the only thing that
98
+ * sees every request, so this is the natural source of observed health. */
99
+ onStatus?: (snapshot: ServiceStatus[]) => void;
100
+ /** Active upstream health-checker (from `createHealthChecker`). When set, the
101
+ * gateway probes upstreams, steers pooled picks toward healthy endpoints, and
102
+ * reflects true liveness in /statusz. Omit → passive drop-on-error only. */
103
+ healthChecker?: HealthChecker;
104
+ /** Opt-in per-client-IP rate limiter — defense-in-depth behind the edge throttle.
105
+ * In-memory + per-instance (`createRateLimiter`) or ONE global budget across all
106
+ * containers (`createRedisRateLimiter`). The check is `await`ed either way; it
107
+ * runs before routing/auth and applies only to proxied traffic (ops endpoints exempt). */
108
+ rateLimiter?: RateLimiter;
109
+ /** Process self-metrics (event-loop lag / rss / cpu / req rates) from `createProcessMetrics`;
110
+ * exposed at the token-gated `/metricsz` for load/stress tests + production monitoring. */
111
+ metrics?: ProcessMetrics;
112
+ /** Server-side HTTP timeouts (ms). `keepAliveTimeout` default 620000 — kept
113
+ * ABOVE the GCP external ALB's 600s backend keep-alive so the gateway never
114
+ * closes a pooled connection the LB still considers live (the classic
115
+ * intermittent-502 race). `requestTimeout` default 0 (disabled) for streaming. */
116
+ server?: {
117
+ keepAliveTimeout?: number;
118
+ requestTimeout?: number;
119
+ connectionTimeout?: number;
120
+ headersTimeout?: number;
121
+ };
122
+ /** Opt-in one-JSON-line-per-request access log to stdout, shaped for Cloud
123
+ * Logging (severity + time + message). Default off (zero overhead). */
124
+ accessLog?: boolean;
125
+ /** Browser origins allowed to make CREDENTIALED cross-origin requests (cookies /
126
+ * `credentials: 'include'`). For an origin in this allowlist the gateway answers preflights — and
127
+ * stamps gateway-generated responses — with `Access-Control-Allow-Origin: <exact origin>` +
128
+ * `Access-Control-Allow-Credentials: true` (NEVER `*`: a credentialed `*` is forbidden by the Fetch
129
+ * spec and a cross-site read hole). Needed when a first-party browser app POSTs to a routed service
130
+ * that sets a cookie (e.g. an IdP SSO cookie set across `login.example.com → auth.example.com`).
131
+ * Exact-matched, case-insensitive. Omit (default) → no credentialed CORS (the Bearer-only edge). */
132
+ corsCredentialOrigins?: string[];
133
+ /** Return verified subject attributes (e.g. JWT claims) for ABAC routing.
134
+ * MUST verify (signature/exp) — forged claims must not reach the rules. */
135
+ identify?: (req: http.IncomingMessage) => Record<string, unknown> | undefined;
136
+ /** Allowlist of request headers exposed to rules as `header.<name>` attributes. */
137
+ attributeHeaders?: string[];
138
+ /** Take the `ip` attribute from `X-Forwarded-For` (behind a trusted LB) via
139
+ * Fastify `trustProxy`; default false → socket IP. */
140
+ trustProxyIp?: boolean;
141
+ /** Gateway-issued device cookie for sticky bucketing (anonymous-friendly).
142
+ * Omit to disable cookie stickiness. `secret` enables HMAC signing. */
143
+ cookie?: {
144
+ name?: string;
145
+ secret?: string;
146
+ domain?: string;
147
+ maxAge?: number;
148
+ };
149
+ /** Header carrying a client device/user id — the sticky key when no cookie
150
+ * is present (native/mobile/API clients), e.g. `x-device-id`. */
151
+ uidHeader?: string;
152
+ /** Customer-supplied lifecycle hooks layered on the built-in pipeline (see hooks.ts). */
153
+ hooks?: ProdGatewayHooks;
154
+ }
155
+ export interface ProdGatewayHandle {
156
+ ports: number[];
157
+ routes: () => Route[];
158
+ status: () => ServiceStatus[];
159
+ stop: () => Promise<void>;
160
+ }
161
+ /** The reply surface {@link sendError} uses — structural so BOTH the HTTP/1 and HTTP/2 fastify reply
162
+ * generics (e.g. the `@fastify/reply-from` `onError` reply) satisfy it without a cast. */
163
+ export interface ReplyLike {
164
+ code(statusCode: number): ReplyLike;
165
+ header(key: string, value: string): ReplyLike;
166
+ type(contentType: string): ReplyLike;
167
+ send(payload: string): ReplyLike;
168
+ }
169
+ /** Per-request state stashed between the handler and the onResponse hook
170
+ * (edge + trace finish) — keyed by the request object, no decoration needed. */
171
+ export interface ReqState {
172
+ route: Route;
173
+ caller?: string;
174
+ startedAt: number;
175
+ trace?: GwTrace;
176
+ version?: string;
177
+ upLabel: string;
178
+ proxied?: boolean;
179
+ }
180
+ export interface GwTrace {
181
+ startedAt: number;
182
+ method: string;
183
+ host: string;
184
+ url: string;
185
+ steps: Array<Record<string, unknown>>;
186
+ flowId?: string;
187
+ requestId?: string;
188
+ done?: boolean;
189
+ }
190
+ /** The mutable route table, content-replaced atomically on a manifest rebuild
191
+ * (the array + map identities are STABLE so layers can hold the reference). */
192
+ export interface RouteTable {
193
+ routes: Route[];
194
+ manifestByService: Map<string, RouteManifest>;
195
+ rebuild(next: RouteManifest[]): void;
196
+ manifests(): RouteManifest[];
197
+ stop(): void;
198
+ }
199
+ /** Shared state + config for the prod layers (no layer-produced functions → acyclic).
200
+ * `opts` is read pervasively (providers are DI'd through it); `table` holds the live
201
+ * route refs; the rest is option-derived config resolved once in `server.ts`. */
202
+ export interface ProdRuntime {
203
+ readonly opts: ProdGatewayOptions;
204
+ readonly table: RouteTable;
205
+ readonly keyToProject: ReadonlyMap<string, string>;
206
+ readonly versionHeader: string;
207
+ readonly accessLog: boolean;
208
+ readonly corsCredentialOrigins: ReadonlySet<string>;
209
+ }
210
+ export type ProdGatewayPhase = 'onRequest' | 'beforeAuth' | 'afterAuth' | 'beforeForward' | 'onResponse' | 'onError' | 'onUpgrade';
211
+ export type ProdHookHandler = (ctx: ProdHookContext) => void | Promise<void>;
212
+ export interface ProdGatewayHooks {
213
+ onRequest?: ProdHookHandler[];
214
+ beforeAuth?: ProdHookHandler[];
215
+ afterAuth?: ProdHookHandler[];
216
+ beforeForward?: ProdHookHandler[];
217
+ onResponse?: ProdHookHandler[];
218
+ onError?: ProdHookHandler[];
219
+ onUpgrade?: ProdHookHandler[];
220
+ }
221
+ /** Context passed to every prod hook — a typed view over the in-flight request. */
222
+ export interface ProdHookContext {
223
+ /** Raw node request (header mutation flows downstream to the upstream). */
224
+ readonly req: http.IncomingMessage;
225
+ /** The Fastify request (absent on the `onUpgrade` path). */
226
+ readonly request?: FastifyRequest;
227
+ /** The Fastify reply (absent on the `onUpgrade` path). */
228
+ readonly reply?: FastifyReply;
229
+ /** The raw upgrade socket (present only on `onUpgrade`). */
230
+ readonly socket?: import('node:net').Socket;
231
+ route?: Route;
232
+ caller?: string;
233
+ claims?: Record<string, unknown>;
234
+ version?: string;
235
+ readonly trace?: GwTrace;
236
+ error?: Error;
237
+ status?: number;
238
+ sendError(status: number, reason: import('../gateway-errors').GatewayErrorReason, detail?: string): void;
239
+ responded(): boolean;
240
+ }
241
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Raw WebSocket upgrade proxy (same trust model; net-level pipe). The upgrade
3
+ * request is rebuilt from `req.rawHeaders` verbatim — minus the gateway-owned
4
+ * trust headers + the client's hop-by-hop control — plus the validated caller +
5
+ * target token + verified identity. Reuses the shared auth + upstream layers.
6
+ */
7
+ import * as net from 'node:net';
8
+ import type * as http from 'node:http';
9
+ import type { ProdRuntime } from './types';
10
+ import type { EdgeAuth } from './auth';
11
+ import type { UpstreamLayer } from './upstream';
12
+ import type { HooksLayer } from './hooks';
13
+ export interface UpgradeDeps {
14
+ rt: ProdRuntime;
15
+ auth: EdgeAuth;
16
+ upstream: UpstreamLayer;
17
+ hooks: HooksLayer;
18
+ }
19
+ export type UpgradeListener = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
20
+ export declare function createUpgrade(deps: UpgradeDeps): UpgradeListener;
21
+ //# sourceMappingURL=upgrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/upgrade.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAIvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,WAAW,CAAC;IAChB,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpG,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,CA4DhE"}
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createUpgrade = createUpgrade;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * Raw WebSocket upgrade proxy (same trust model; net-level pipe). The upgrade
7
+ * request is rebuilt from `req.rawHeaders` verbatim — minus the gateway-owned
8
+ * trust headers + the client's hop-by-hop control — plus the validated caller +
9
+ * target token + verified identity. Reuses the shared auth + upstream layers.
10
+ */
11
+ const net = tslib_1.__importStar(require("node:net"));
12
+ const manifest_1 = require("../manifest");
13
+ const edge_1 = require("./edge");
14
+ const trust_1 = require("./trust");
15
+ function createUpgrade(deps) {
16
+ const { rt, auth, upstream, hooks } = deps;
17
+ const run = async (req, socket, head) => {
18
+ (0, trust_1.stripTrust)(req);
19
+ const host = (req.headers.host || '').split(':')[0];
20
+ const cpath = (0, edge_1.canonicalPath)(req.url ?? '/');
21
+ if (cpath === undefined) {
22
+ socket.destroy();
23
+ return;
24
+ } // reject path-canonicalization attacks
25
+ const route = (0, manifest_1.matchRoute)(rt.table.routes, host, req.url ?? '/');
26
+ if (!route) {
27
+ socket.destroy();
28
+ return;
29
+ }
30
+ const ctx = hooks.any ? hooks.upgradeContext(req, socket, undefined) : undefined;
31
+ if (ctx)
32
+ ctx.route = route;
33
+ const claims = route.internal ? undefined : rt.opts.identify?.(req);
34
+ let caller;
35
+ if (route.internal) {
36
+ const v = auth.internal(req, route, undefined);
37
+ if (!v.ok) {
38
+ socket.destroy();
39
+ return;
40
+ }
41
+ caller = v.caller;
42
+ if (ctx)
43
+ ctx.caller = caller;
44
+ }
45
+ else {
46
+ // Same fail-closed ABAC + identical attribute bag as the HTTP path (no divergence).
47
+ const v = auth.public(req, route, cpath, claims, req.socket.remoteAddress ?? '', undefined);
48
+ if (!v.ok) {
49
+ socket.destroy();
50
+ return;
51
+ }
52
+ if (ctx)
53
+ ctx.claims = claims;
54
+ if (ctx && hooks.has('afterAuth')) {
55
+ await hooks.run('afterAuth', ctx);
56
+ if (ctx.responded())
57
+ return;
58
+ }
59
+ }
60
+ if (route.prependPrefix) {
61
+ const u = req.url ?? '/';
62
+ const pp = route.prependPrefix;
63
+ if (u !== pp && !u.startsWith(pp + '/'))
64
+ req.url = pp + u;
65
+ } // segment-boundary
66
+ if (ctx && hooks.has('onUpgrade')) {
67
+ await hooks.run('onUpgrade', ctx);
68
+ if (ctx.responded())
69
+ return;
70
+ }
71
+ const pinned = (0, edge_1.hdr)(req.headers[rt.versionHeader]);
72
+ const up = await upstream.resolveUpstream(route, undefined, pinned);
73
+ if (!up || !('url' in up)) {
74
+ socket.destroy();
75
+ return;
76
+ }
77
+ const u = new URL(up.url);
78
+ const upstreamSock = net.connect(Number(u.port) || (u.protocol === 'https:' ? 443 : 80), u.hostname, () => {
79
+ // rebuild the upgrade request from rawHeaders (verbatim), minus the trust headers we own (incl.
80
+ // client identity) + the client's hop-by-hop control (so a `Connection: x-internal-token` can't
81
+ // strip the gateway's injected headers downstream), plus the validated caller + token + identity.
82
+ const strip = new Set(['x-internal-token', 'x-api-key-id', 'x-api-key', 'x-user-id', 'x-tenant-id', 'x-user-roles', 'x-user-permissions', 'connection', 'proxy-connection', 'keep-alive']);
83
+ const lines = [`${req.method} ${req.url} HTTP/1.1`, 'Connection: Upgrade']; // gateway-owned, clean upgrade
84
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
85
+ const k = req.rawHeaders[i];
86
+ if (!strip.has(k.toLowerCase()))
87
+ lines.push(`${k}: ${req.rawHeaders[i + 1]}`);
88
+ }
89
+ if (caller)
90
+ lines.push(`x-api-key-id: ${caller}`);
91
+ const token = rt.opts.serviceKeys?.[route.service];
92
+ if (token)
93
+ lines.push(`x-internal-token: ${token}`);
94
+ if (claims) { // stamp the verified identity (mirrors the HTTP path's stampIdentity)
95
+ if (claims['sub'])
96
+ lines.push(`x-user-id: ${String(claims['sub'])}`);
97
+ if (claims['tid'])
98
+ lines.push(`x-tenant-id: ${String(claims['tid'])}`);
99
+ const roles = claims['roles'];
100
+ if (Array.isArray(roles) && roles.length)
101
+ lines.push(`x-user-roles: ${roles.map(String).join(',')}`);
102
+ const perms = claims['perms'];
103
+ if (Array.isArray(perms) && perms.length)
104
+ lines.push(`x-user-permissions: ${perms.map(String).join(',')}`);
105
+ }
106
+ upstreamSock.write(lines.join('\r\n') + '\r\n\r\n');
107
+ if (head?.length)
108
+ upstreamSock.write(head);
109
+ socket.pipe(upstreamSock);
110
+ upstreamSock.pipe(socket);
111
+ });
112
+ upstreamSock.on('error', () => socket.destroy());
113
+ socket.on('error', () => upstreamSock.destroy());
114
+ };
115
+ return (req, socket, head) => { void run(req, socket, head); };
116
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Per-request upstream resolution: a direct manifest `upstream`, or a pooled
3
+ * pod pick (health-steered, version-pinned) with scale-from-zero via the
4
+ * PodProvider. Shared by the HTTP handler and the WS upgrade.
5
+ */
6
+ import type { Route, Upstream } from '../manifest';
7
+ import type { GwTrace, ProdGatewayOptions } from './types';
8
+ import type { Observability } from './observability';
9
+ export interface UpstreamLayer {
10
+ resolveUpstream(route: Route, trace: GwTrace | undefined, pinned?: string): Promise<Upstream | undefined>;
11
+ }
12
+ export declare function createUpstream(opts: ProdGatewayOptions, obs: Observability): UpstreamLayer;
13
+ //# sourceMappingURL=upstream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/upstream.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;CAC3G;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,aAAa,GAAG,aAAa,CAY1F"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createUpstream = createUpstream;
4
+ function createUpstream(opts, obs) {
5
+ const resolveUpstream = async (route, trace, pinned) => {
6
+ if (route.upstream)
7
+ return { url: route.upstream };
8
+ const opt = {
9
+ ...(pinned ? { version: pinned } : {}),
10
+ ...(opts.healthChecker ? { isHealthy: (u) => opts.healthChecker.isHealthy(u) } : {}),
11
+ };
12
+ let up = opts.pods?.pick(route.service, opt);
13
+ if (!up && opts.pods?.ensureUp) {
14
+ obs.step(trace, 'ensure-up', { project: route.service });
15
+ if (await opts.pods.ensureUp(route.service))
16
+ up = opts.pods.pick(route.service, opt);
17
+ }
18
+ return up;
19
+ };
20
+ return { resolveUpstream };
21
+ }
@@ -0,0 +1,8 @@
1
+ import type { AuthVerdict, GatewayRuntime, GwTrace, Route } from './types';
2
+ import type { Observability } from './observability';
3
+ export interface EdgeAuth {
4
+ internal(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
5
+ public(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
6
+ }
7
+ export declare function createEdgeAuth(rt: GatewayRuntime, obs: Observability): EdgeAuth;
8
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEdgeAuth = createEdgeAuth;
4
+ /**
5
+ * The edge-auth layer (the JWT seam). Two boundaries, ONE source so the HTTP
6
+ * handler and the WS upgrade can't drift:
7
+ * - `internal(...)`: validate the CALLER's `x-api-key` on an `internal.<host>`
8
+ * route, stamp `x-api-key-id` (audit) + `__lensmcpCaller`.
9
+ * - `public(...)`: verify the end-user JWT, enforce per-route rules, inject
10
+ * trusted identity — mirroring the prod gateway. Only enforced when the
11
+ * service declares `cluster.auth` (opt-in; un-migrated services stay open).
12
+ * Each returns a verdict; the caller turns a denial into its own rejection
13
+ * (the handler → `finishTrace` + `sendError`; the upgrade → `socket.destroy()`).
14
+ */
15
+ const manifest_1 = require("../manifest");
16
+ const dev_auth_1 = require("./dev-auth");
17
+ function createEdgeAuth(rt, obs) {
18
+ const { traceStep } = obs;
19
+ const internal = (req, _route, trace) => {
20
+ // service-to-service: validate the CALLER's api key at the boundary
21
+ const presented = String(req.headers['x-api-key'] ?? '');
22
+ const caller = rt.keyToProject.get(presented);
23
+ if (!caller) {
24
+ traceStep(trace, 'auth', { mode: 'x-api-key', ok: false });
25
+ return { ok: false, status: 401, reason: 'unauthorized', detail: 'Invalid or missing x-api-key for internal route.' };
26
+ }
27
+ req.headers['x-api-key-id'] = caller; // audit + lens attribution
28
+ req.__lensmcpCaller = caller;
29
+ delete req.headers['x-api-key'];
30
+ traceStep(trace, 'auth', { mode: 'x-api-key', ok: true, caller });
31
+ return { ok: true, caller };
32
+ };
33
+ const publicAuth = (req, route, trace) => {
34
+ const policy = route.auth;
35
+ if (policy) {
36
+ const url0 = (req.url ?? '/').split('?')[0];
37
+ const method = (req.method ?? 'GET').toUpperCase();
38
+ const resolved = (0, manifest_1.authRuleFor)(policy, url0, method);
39
+ if (resolved.mode === 'jwt') {
40
+ const claims = (0, dev_auth_1.verifyDevJwt)(req);
41
+ if (!claims) {
42
+ traceStep(trace, 'auth', { mode: 'jwt', ok: false });
43
+ return { ok: false, status: 401, reason: 'unauthorized', detail: 'Unauthorized: invalid or missing token' };
44
+ }
45
+ if (resolved.rule && !(0, manifest_1.evalRule)(resolved.rule, (0, dev_auth_1.devAttrs)(req, claims))) {
46
+ traceStep(trace, 'authz', { ok: false });
47
+ return { ok: false, status: 403, reason: 'forbidden', detail: 'Forbidden: insufficient permission' };
48
+ }
49
+ (0, dev_auth_1.stampIdentity)(req, claims);
50
+ traceStep(trace, 'auth', { mode: 'jwt', ok: true });
51
+ return { ok: true, claims };
52
+ }
53
+ traceStep(trace, 'auth', { mode: resolved.mode });
54
+ return { ok: true };
55
+ }
56
+ traceStep(trace, 'auth', { mode: 'public', unconfigured: true });
57
+ return { ok: true };
58
+ };
59
+ return { internal, public: publicAuth };
60
+ }