@lensmcp/cluster 1.8.0 → 1.10.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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * SSRF guard for OUTBOUND fetches of a request- or config-supplied URL.
3
+ *
4
+ * The prod gateway today never fetches an attacker-controlled URL — upstreams
5
+ * are pinned in the manifest, the JWKS URL is operator env, and reply-from /
6
+ * undici do not chase redirects. This module exists so that the MOMENT a sink
7
+ * IS added (a remote-MCP / OpenAPI schema import, a webhook callback, a
8
+ * dynamically-discovered upstream — anything that fetches a URL it did not
9
+ * author) there is a ready, tested guard to wrap it with, instead of a naive
10
+ * `if (host === '127.0.0.1')` string check that the classic bypasses defeat.
11
+ *
12
+ * It defends the three canonical bypasses:
13
+ * 1. DNS name that RESOLVES to an internal IP (`127.0.0.1.nip.io`, rebinding)
14
+ * → we resolve the host and check the RESOLVED addresses, not the string.
15
+ * 2. Redirect chase (`/redirect` 3xx → loopback) → `safeFetch` re-validates
16
+ * EVERY hop with `redirect:'manual'`; it never blindly follows a Location.
17
+ * 3. IPv4-mapped IPv6 loopback (`[::ffff:127.0.0.1]`, `[::ffff:7f00:1]`) →
18
+ * `net.BlockList` normalizes mapped forms against the IPv4 rules.
19
+ *
20
+ * Residual TOCTOU: between the resolve-and-check and the socket connect, a
21
+ * rebinding resolver could flip the record. Close it by pinning the connection
22
+ * to the validated address(es) with `pinnedLookup()` (pass to
23
+ * `new http.Agent({ lookup })` or an undici `connect:{ lookup }` dispatcher).
24
+ *
25
+ * Defaults are operator-overridable but FAIL CLOSED: anything unparseable, any
26
+ * non-http(s) scheme, embedded credentials, an empty host, or a host that
27
+ * resolves to no records is rejected.
28
+ */
29
+ import { BlockList } from 'node:net';
30
+ import type { LookupAddress } from 'node:dns';
31
+ /** Thrown for every blocked URL — carries a machine-readable `reason` and, when
32
+ * the block was an address, the offending `address` (the resolved IP, not the
33
+ * hostname, so logs show what the name actually pointed at). */
34
+ export declare class SsrfBlockedError extends Error {
35
+ readonly reason: string;
36
+ readonly url?: string;
37
+ readonly address?: string;
38
+ constructor(reason: string, url?: string, address?: string, cause?: unknown);
39
+ }
40
+ /** True if `ip` (a literal address) falls in a blocked range. Mapped IPv6 is
41
+ * normalized by BlockList; an unparseable string is treated as blocked. */
42
+ export declare function isBlockedAddress(ip: string, blockList?: BlockList): boolean;
43
+ export interface FetchableUrlOptions {
44
+ /** Allowed URL schemes. Default `['http:', 'https:']`. */
45
+ allowedProtocols?: string[];
46
+ /** DNS resolver (injectable for tests). Default `dns.lookup(host, {all:true})`. */
47
+ resolve?: (host: string) => Promise<string[]>;
48
+ /** Override the blocked-range set. Default = the internal/non-routable set. */
49
+ blockList?: BlockList;
50
+ /** Escape hatch: skip all internal-range checks (e.g. a trusted-loopback dev
51
+ * sink). Off by default — opt in explicitly, never from request input. */
52
+ allowPrivate?: boolean;
53
+ }
54
+ export interface FetchableUrl {
55
+ /** The parsed, validated URL. */
56
+ url: URL;
57
+ /** The host (brackets stripped for IPv6 literals). */
58
+ hostname: string;
59
+ /** Validated addresses: the literal IP, or every resolved DNS record. Feed to
60
+ * `pinnedLookup()` to pin the connection and close the rebinding TOCTOU. */
61
+ addresses: string[];
62
+ }
63
+ /**
64
+ * Validate that `rawUrl` is safe to fetch. Rejects (throws `SsrfBlockedError`)
65
+ * a malformed URL, a disallowed scheme, embedded credentials, a missing host,
66
+ * a literal internal IP, or a host that resolves to ANY internal address.
67
+ * Returns the parsed URL and the validated address set on success.
68
+ */
69
+ export declare function assertFetchableUrl(rawUrl: string | URL, opts?: FetchableUrlOptions): Promise<FetchableUrl>;
70
+ export interface SafeFetchOptions extends FetchableUrlOptions {
71
+ /** Max redirect hops before giving up. Default 5. */
72
+ maxRedirects?: number;
73
+ /** Injectable fetch (tests). Default global `fetch`. */
74
+ fetchImpl?: typeof fetch;
75
+ }
76
+ /**
77
+ * A `fetch` that validates the target — and re-validates every redirect hop —
78
+ * with the SSRF guard. Redirects are handled manually (`redirect:'manual'`) so
79
+ * a 3xx Location to an internal host is checked BEFORE it is followed, never
80
+ * chased blindly. Returns the final non-redirect `Response`.
81
+ *
82
+ * Note: each hop re-issues with the original `init` (method + body preserved —
83
+ * the security-relevant worst case for 307/308). A streamed request body cannot
84
+ * be replayed across hops.
85
+ */
86
+ export declare function safeFetch(rawUrl: string | URL, init?: RequestInit, opts?: SafeFetchOptions): Promise<Response>;
87
+ type LookupCallback = (err: NodeJS.ErrnoException | null, address?: string | LookupAddress[], family?: number) => void;
88
+ /**
89
+ * A `dns.lookup`-compatible function that ONLY ever returns the pre-validated
90
+ * `addresses`. Pass it to `new http.Agent({ lookup })` or an undici
91
+ * `connect:{ lookup }` dispatcher so the socket connects to an address the
92
+ * guard already cleared — closing the DNS-rebinding TOCTOU between check and
93
+ * connect. Use the `addresses` from `assertFetchableUrl()`.
94
+ */
95
+ export declare function pinnedLookup(addresses: readonly string[]): (hostname: string, options: unknown, callback?: LookupCallback) => void;
96
+ export {};
97
+ //# sourceMappingURL=ssrf-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf-guard.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/ssrf-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAQ,SAAS,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C;;iEAEiE;AACjE,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;gBACd,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAU5E;AA6BD;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,GAAE,SAA8B,GAAG,OAAO,CAI/F;AAQD,MAAM,WAAW,mBAAmB;IAClC,0DAA0D;IAC1D,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,mFAAmF;IACnF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C,+EAA+E;IAC/E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;+EAC2E;IAC3E,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,GAAG,EAAE,GAAG,CAAC;IACT,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB;iFAC6E;IAC7E,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,GAAG,GAAG,EACpB,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAgDvB;AAED,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,GAAG,GAAG,EACpB,IAAI,GAAE,WAAgB,EACtB,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,QAAQ,CAAC,CAgBnB;AAED,KAAK,cAAc,GAAG,CACpB,GAAG,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,EACjC,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,EAClC,MAAM,CAAC,EAAE,MAAM,KACZ,IAAI,CAAC;AAEV;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,cAAc,KAAK,IAAI,CAezE"}
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SsrfBlockedError = void 0;
4
+ exports.isBlockedAddress = isBlockedAddress;
5
+ exports.assertFetchableUrl = assertFetchableUrl;
6
+ exports.safeFetch = safeFetch;
7
+ exports.pinnedLookup = pinnedLookup;
8
+ /**
9
+ * SSRF guard for OUTBOUND fetches of a request- or config-supplied URL.
10
+ *
11
+ * The prod gateway today never fetches an attacker-controlled URL — upstreams
12
+ * are pinned in the manifest, the JWKS URL is operator env, and reply-from /
13
+ * undici do not chase redirects. This module exists so that the MOMENT a sink
14
+ * IS added (a remote-MCP / OpenAPI schema import, a webhook callback, a
15
+ * dynamically-discovered upstream — anything that fetches a URL it did not
16
+ * author) there is a ready, tested guard to wrap it with, instead of a naive
17
+ * `if (host === '127.0.0.1')` string check that the classic bypasses defeat.
18
+ *
19
+ * It defends the three canonical bypasses:
20
+ * 1. DNS name that RESOLVES to an internal IP (`127.0.0.1.nip.io`, rebinding)
21
+ * → we resolve the host and check the RESOLVED addresses, not the string.
22
+ * 2. Redirect chase (`/redirect` 3xx → loopback) → `safeFetch` re-validates
23
+ * EVERY hop with `redirect:'manual'`; it never blindly follows a Location.
24
+ * 3. IPv4-mapped IPv6 loopback (`[::ffff:127.0.0.1]`, `[::ffff:7f00:1]`) →
25
+ * `net.BlockList` normalizes mapped forms against the IPv4 rules.
26
+ *
27
+ * Residual TOCTOU: between the resolve-and-check and the socket connect, a
28
+ * rebinding resolver could flip the record. Close it by pinning the connection
29
+ * to the validated address(es) with `pinnedLookup()` (pass to
30
+ * `new http.Agent({ lookup })` or an undici `connect:{ lookup }` dispatcher).
31
+ *
32
+ * Defaults are operator-overridable but FAIL CLOSED: anything unparseable, any
33
+ * non-http(s) scheme, embedded credentials, an empty host, or a host that
34
+ * resolves to no records is rejected.
35
+ */
36
+ const node_net_1 = require("node:net");
37
+ const promises_1 = require("node:dns/promises");
38
+ /** Thrown for every blocked URL — carries a machine-readable `reason` and, when
39
+ * the block was an address, the offending `address` (the resolved IP, not the
40
+ * hostname, so logs show what the name actually pointed at). */
41
+ class SsrfBlockedError extends Error {
42
+ constructor(reason, url, address, cause) {
43
+ super(`SSRF guard blocked ${url ?? 'request'}: ${reason}${address ? ` (${address})` : ''}`, cause !== undefined ? { cause } : undefined);
44
+ this.name = 'SsrfBlockedError';
45
+ this.reason = reason;
46
+ this.url = url;
47
+ this.address = address;
48
+ }
49
+ }
50
+ exports.SsrfBlockedError = SsrfBlockedError;
51
+ /** The internal / non-routable ranges an outbound fetch must never reach.
52
+ * Mapped IPv4-in-IPv6 (`::ffff:a.b.c.d`) is matched by the IPv4 rules — verified
53
+ * in the spec — so we do NOT blanket-block `::ffff:0:0/96` (mapped *public*
54
+ * IPv4 is legitimate). */
55
+ function buildDefaultBlockList() {
56
+ const b = new node_net_1.BlockList();
57
+ // ── IPv4 ──
58
+ b.addSubnet('0.0.0.0', 8, 'ipv4'); // "this host" / 0.0.0.0
59
+ b.addSubnet('10.0.0.0', 8, 'ipv4'); // RFC1918 private
60
+ b.addSubnet('100.64.0.0', 10, 'ipv4'); // RFC6598 CGNAT
61
+ b.addSubnet('127.0.0.0', 8, 'ipv4'); // loopback
62
+ b.addSubnet('169.254.0.0', 16, 'ipv4'); // link-local incl. cloud metadata 169.254.169.254
63
+ b.addSubnet('172.16.0.0', 12, 'ipv4'); // RFC1918 private (172.16–172.31)
64
+ b.addSubnet('192.0.0.0', 24, 'ipv4'); // IETF protocol assignments
65
+ b.addSubnet('192.168.0.0', 16, 'ipv4'); // RFC1918 private
66
+ b.addSubnet('198.18.0.0', 15, 'ipv4'); // benchmarking
67
+ b.addSubnet('240.0.0.0', 4, 'ipv4'); // reserved + 255.255.255.255 broadcast
68
+ // ── IPv6 ──
69
+ b.addAddress('::', 'ipv6'); // unspecified
70
+ b.addAddress('::1', 'ipv6'); // loopback
71
+ b.addSubnet('fc00::', 7, 'ipv6'); // unique-local (ULA)
72
+ b.addSubnet('fe80::', 10, 'ipv6'); // link-local
73
+ return b;
74
+ }
75
+ const DEFAULT_BLOCK_LIST = buildDefaultBlockList();
76
+ /** True if `ip` (a literal address) falls in a blocked range. Mapped IPv6 is
77
+ * normalized by BlockList; an unparseable string is treated as blocked. */
78
+ function isBlockedAddress(ip, blockList = DEFAULT_BLOCK_LIST) {
79
+ const fam = (0, node_net_1.isIP)(ip);
80
+ if (fam === 0)
81
+ return true; // not an IP → fail closed
82
+ return blockList.check(ip, fam === 6 ? 'ipv6' : 'ipv4');
83
+ }
84
+ const stripBrackets = (host) => host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
85
+ const defaultResolve = async (host) => (await (0, promises_1.lookup)(host, { all: true, verbatim: true })).map((r) => r.address);
86
+ /**
87
+ * Validate that `rawUrl` is safe to fetch. Rejects (throws `SsrfBlockedError`)
88
+ * a malformed URL, a disallowed scheme, embedded credentials, a missing host,
89
+ * a literal internal IP, or a host that resolves to ANY internal address.
90
+ * Returns the parsed URL and the validated address set on success.
91
+ */
92
+ async function assertFetchableUrl(rawUrl, opts = {}) {
93
+ const protocols = opts.allowedProtocols ?? ['http:', 'https:'];
94
+ let url;
95
+ try {
96
+ url = new URL(rawUrl);
97
+ }
98
+ catch {
99
+ throw new SsrfBlockedError('malformed-url', String(rawUrl));
100
+ }
101
+ if (!protocols.includes(url.protocol)) {
102
+ throw new SsrfBlockedError(`scheme-not-allowed:${url.protocol}`, url.href);
103
+ }
104
+ if (url.username !== '' || url.password !== '') {
105
+ throw new SsrfBlockedError('embedded-credentials', url.href);
106
+ }
107
+ const hostname = stripBrackets(url.hostname);
108
+ if (hostname === '')
109
+ throw new SsrfBlockedError('missing-host', url.href);
110
+ if (opts.allowPrivate) {
111
+ return { url, hostname, addresses: (0, node_net_1.isIP)(hostname) ? [hostname] : [] };
112
+ }
113
+ const blockList = opts.blockList ?? DEFAULT_BLOCK_LIST;
114
+ // Literal IP (covers IPv4, IPv6, and IPv4-mapped IPv6 like [::ffff:127.0.0.1]).
115
+ if ((0, node_net_1.isIP)(hostname) !== 0) {
116
+ if (isBlockedAddress(hostname, blockList)) {
117
+ throw new SsrfBlockedError('blocked-address', url.href, hostname);
118
+ }
119
+ return { url, hostname, addresses: [hostname] };
120
+ }
121
+ // DNS name → resolve and check EVERY record (defeats nip.io / rebinding).
122
+ const resolve = opts.resolve ?? defaultResolve;
123
+ let addresses;
124
+ try {
125
+ addresses = await resolve(hostname);
126
+ }
127
+ catch (e) {
128
+ throw new SsrfBlockedError('dns-resolution-failed', url.href, undefined, e);
129
+ }
130
+ if (addresses.length === 0)
131
+ throw new SsrfBlockedError('dns-no-records', url.href);
132
+ for (const addr of addresses) {
133
+ if (isBlockedAddress(addr, blockList)) {
134
+ throw new SsrfBlockedError('blocked-resolved-address', url.href, addr);
135
+ }
136
+ }
137
+ return { url, hostname, addresses };
138
+ }
139
+ /**
140
+ * A `fetch` that validates the target — and re-validates every redirect hop —
141
+ * with the SSRF guard. Redirects are handled manually (`redirect:'manual'`) so
142
+ * a 3xx Location to an internal host is checked BEFORE it is followed, never
143
+ * chased blindly. Returns the final non-redirect `Response`.
144
+ *
145
+ * Note: each hop re-issues with the original `init` (method + body preserved —
146
+ * the security-relevant worst case for 307/308). A streamed request body cannot
147
+ * be replayed across hops.
148
+ */
149
+ async function safeFetch(rawUrl, init = {}, opts = {}) {
150
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
151
+ const maxRedirects = opts.maxRedirects ?? 5;
152
+ let current = rawUrl;
153
+ for (let hop = 0;; hop++) {
154
+ const { url } = await assertFetchableUrl(current, opts); // re-validate THIS hop
155
+ const res = await doFetch(url, { ...init, redirect: 'manual' });
156
+ const location = res.headers.get('location');
157
+ if (res.status >= 300 && res.status < 400 && location) {
158
+ if (hop >= maxRedirects)
159
+ throw new SsrfBlockedError('too-many-redirects', url.href);
160
+ current = new URL(location, url).href; // resolve relative; loop re-validates next hop
161
+ continue;
162
+ }
163
+ return res;
164
+ }
165
+ }
166
+ /**
167
+ * A `dns.lookup`-compatible function that ONLY ever returns the pre-validated
168
+ * `addresses`. Pass it to `new http.Agent({ lookup })` or an undici
169
+ * `connect:{ lookup }` dispatcher so the socket connects to an address the
170
+ * guard already cleared — closing the DNS-rebinding TOCTOU between check and
171
+ * connect. Use the `addresses` from `assertFetchableUrl()`.
172
+ */
173
+ function pinnedLookup(addresses) {
174
+ const recs = addresses.map((address) => ({
175
+ address,
176
+ family: (0, node_net_1.isIP)(address) === 6 ? 6 : 4,
177
+ }));
178
+ return (hostname, options, callback) => {
179
+ const cb = (typeof options === 'function' ? options : callback);
180
+ const all = typeof options === 'object' && options !== null && options.all === true;
181
+ if (recs.length === 0) {
182
+ cb(new Error(`SSRF guard: no pinned address for ${hostname}`));
183
+ return;
184
+ }
185
+ if (all)
186
+ cb(null, recs);
187
+ else
188
+ cb(null, recs[0].address, recs[0].family);
189
+ };
190
+ }
package/gateway.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@lensmcp/cluster/gateway` — the PRODUCTION gateway's public, extendable surface.
3
+ *
4
+ * This is the **deployable edge** (`startProdGateway` — Fastify + undici reverse
5
+ * proxy, manifest-driven, the thing `Dockerfile.prod-gateway` runs). A consumer
6
+ * (e.g. tetros' `server/apps/gateway`) imports from here to run or extend it with
7
+ * its own providers/middleware and ship a specialized image.
8
+ *
9
+ * ⚠️ This is NOT the DEV cluster gateway. The dev gateway is the
10
+ * `@lensmcp/cluster:gateway` nx EXECUTOR (`gateway.lib.ts` → `startGateway`) — a
11
+ * local-only HTTPS front door on :443 that fronts unix-socket pods for
12
+ * development. The two share a routing core (`manifest.ts`) but are separate
13
+ * lifecycles: dev = an nx target you run locally; prod = a containerized service
14
+ * you deploy. Kept on a separate subpath so the build-time webpack-config exports
15
+ * in the main `@lensmcp/cluster` entry never pull in fastify/undici.
16
+ */
17
+ export { startProdGateway, type ProdGatewayOptions, type ProdGatewayHandle, type ServiceStatus, } from './executors/gateway/prod-gateway.lib';
18
+ export { staticManifestProvider, endpointsPodProvider } from './executors/gateway/providers-prod';
19
+ export { createRegistryProviders, redisRegistrySource, type RegistrySource, type RedisLike, DEFAULT_REGISTRY_PREFIX, } from './executors/gateway/registry-source';
20
+ export { type RouteManifest, type RouteMount, type AuthMode, type AuthPolicy, type ManifestProvider, type PodProvider, type Upstream, type Route, manifestFromClusterDecl, } from './executors/gateway/manifest';
21
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EACL,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAGlG,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,uBAAuB,GACxB,MAAM,qCAAqC,CAAC;AAI7C,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,KAAK,EACV,uBAAuB,GACxB,MAAM,8BAA8B,CAAC"}
package/gateway.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * `@lensmcp/cluster/gateway` — the PRODUCTION gateway's public, extendable surface.
4
+ *
5
+ * This is the **deployable edge** (`startProdGateway` — Fastify + undici reverse
6
+ * proxy, manifest-driven, the thing `Dockerfile.prod-gateway` runs). A consumer
7
+ * (e.g. tetros' `server/apps/gateway`) imports from here to run or extend it with
8
+ * its own providers/middleware and ship a specialized image.
9
+ *
10
+ * ⚠️ This is NOT the DEV cluster gateway. The dev gateway is the
11
+ * `@lensmcp/cluster:gateway` nx EXECUTOR (`gateway.lib.ts` → `startGateway`) — a
12
+ * local-only HTTPS front door on :443 that fronts unix-socket pods for
13
+ * development. The two share a routing core (`manifest.ts`) but are separate
14
+ * lifecycles: dev = an nx target you run locally; prod = a containerized service
15
+ * you deploy. Kept on a separate subpath so the build-time webpack-config exports
16
+ * in the main `@lensmcp/cluster` entry never pull in fastify/undici.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.manifestFromClusterDecl = exports.DEFAULT_REGISTRY_PREFIX = exports.redisRegistrySource = exports.createRegistryProviders = exports.endpointsPodProvider = exports.staticManifestProvider = exports.startProdGateway = void 0;
20
+ // The prod gateway engine + its option/handle contract.
21
+ var prod_gateway_lib_1 = require("./executors/gateway/prod-gateway.lib");
22
+ Object.defineProperty(exports, "startProdGateway", { enumerable: true, get: function () { return prod_gateway_lib_1.startProdGateway; } });
23
+ // Static (in-memory) providers — build a gateway from explicit manifests/endpoints.
24
+ var providers_prod_1 = require("./executors/gateway/providers-prod");
25
+ Object.defineProperty(exports, "staticManifestProvider", { enumerable: true, get: function () { return providers_prod_1.staticManifestProvider; } });
26
+ Object.defineProperty(exports, "endpointsPodProvider", { enumerable: true, get: function () { return providers_prod_1.endpointsPodProvider; } });
27
+ // Redis-backed registry providers — the prod path (manifests/rollout read live from Redis).
28
+ var registry_source_1 = require("./executors/gateway/registry-source");
29
+ Object.defineProperty(exports, "createRegistryProviders", { enumerable: true, get: function () { return registry_source_1.createRegistryProviders; } });
30
+ Object.defineProperty(exports, "redisRegistrySource", { enumerable: true, get: function () { return registry_source_1.redisRegistrySource; } });
31
+ Object.defineProperty(exports, "DEFAULT_REGISTRY_PREFIX", { enumerable: true, get: function () { return registry_source_1.DEFAULT_REGISTRY_PREFIX; } });
32
+ // Manifest/route types + the cluster-decl → manifest converter (derive prod
33
+ // manifests from the SAME project.json `cluster` declarations dev uses).
34
+ var manifest_1 = require("./executors/gateway/manifest");
35
+ Object.defineProperty(exports, "manifestFromClusterDecl", { enumerable: true, get: function () { return manifest_1.manifestFromClusterDecl; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
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.8.0",
69
- "@lensmcp/nx-plugin": "1.8.0",
68
+ "@lensmcp/node-instrumentation": "1.10.0",
69
+ "@lensmcp/nx-plugin": "1.10.0",
70
70
  "fork-ts-checker-webpack-plugin": "^9.0.0",
71
71
  "glob": "^11.0.0",
72
72
  "http-proxy": "^1.18.0",