@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.
- package/executors/gateway/gateway.lib.d.ts +15 -167
- package/executors/gateway/gateway.lib.d.ts.map +1 -1
- package/executors/gateway/gateway.lib.js +24 -1041
- package/executors/gateway/main.prod-gateway.js +74 -2
- package/executors/gateway/prod-gateway.lib.d.ts +19 -151
- package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
- package/executors/gateway/prod-gateway.lib.js +16 -863
- package/executors/gateway/prod-runtime/access-log.d.ts +9 -0
- package/executors/gateway/prod-runtime/access-log.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/access-log.js +24 -0
- package/executors/gateway/prod-runtime/app.d.ts +23 -0
- package/executors/gateway/prod-runtime/app.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/app.js +123 -0
- package/executors/gateway/prod-runtime/auth.d.ts +30 -0
- package/executors/gateway/prod-runtime/auth.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/auth.js +51 -0
- package/executors/gateway/prod-runtime/cors.d.ts +19 -0
- package/executors/gateway/prod-runtime/cors.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/cors.js +40 -0
- package/executors/gateway/prod-runtime/edge.d.ts +16 -0
- package/executors/gateway/prod-runtime/edge.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/edge.js +65 -0
- package/executors/gateway/prod-runtime/handler.d.ts +32 -0
- package/executors/gateway/prod-runtime/handler.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/handler.js +226 -0
- package/executors/gateway/prod-runtime/hooks.d.ts +20 -0
- package/executors/gateway/prod-runtime/hooks.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/hooks.js +42 -0
- package/executors/gateway/prod-runtime/observability.d.ts +23 -0
- package/executors/gateway/prod-runtime/observability.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/observability.js +125 -0
- package/executors/gateway/prod-runtime/rollout.d.ts +18 -0
- package/executors/gateway/prod-runtime/rollout.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/rollout.js +103 -0
- package/executors/gateway/prod-runtime/routing.d.ts +4 -0
- package/executors/gateway/prod-runtime/routing.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/routing.js +40 -0
- package/executors/gateway/prod-runtime/server.d.ts +3 -0
- package/executors/gateway/prod-runtime/server.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/server.js +79 -0
- package/executors/gateway/prod-runtime/trust.d.ts +12 -0
- package/executors/gateway/prod-runtime/trust.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/trust.js +32 -0
- package/executors/gateway/prod-runtime/types.d.ts +241 -0
- package/executors/gateway/prod-runtime/types.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/types.js +2 -0
- package/executors/gateway/prod-runtime/upgrade.d.ts +21 -0
- package/executors/gateway/prod-runtime/upgrade.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/upgrade.js +116 -0
- package/executors/gateway/prod-runtime/upstream.d.ts +13 -0
- package/executors/gateway/prod-runtime/upstream.d.ts.map +1 -0
- package/executors/gateway/prod-runtime/upstream.js +21 -0
- package/executors/gateway/runtime/auth.d.ts +8 -0
- package/executors/gateway/runtime/auth.d.ts.map +1 -0
- package/executors/gateway/runtime/auth.js +60 -0
- package/executors/gateway/runtime/dev-auth.d.ts +9 -0
- package/executors/gateway/runtime/dev-auth.d.ts.map +1 -0
- package/executors/gateway/runtime/dev-auth.js +79 -0
- package/executors/gateway/runtime/discovery.d.ts +17 -0
- package/executors/gateway/runtime/discovery.d.ts.map +1 -0
- package/executors/gateway/runtime/discovery.js +111 -0
- package/executors/gateway/runtime/edge.d.ts +31 -0
- package/executors/gateway/runtime/edge.d.ts.map +1 -0
- package/executors/gateway/runtime/edge.js +47 -0
- package/executors/gateway/runtime/handler.d.ts +24 -0
- package/executors/gateway/runtime/handler.d.ts.map +1 -0
- package/executors/gateway/runtime/handler.js +165 -0
- package/executors/gateway/runtime/hooks.d.ts +23 -0
- package/executors/gateway/runtime/hooks.d.ts.map +1 -0
- package/executors/gateway/runtime/hooks.js +55 -0
- package/executors/gateway/runtime/lens-children.d.ts +13 -0
- package/executors/gateway/runtime/lens-children.d.ts.map +1 -0
- package/executors/gateway/runtime/lens-children.js +136 -0
- package/executors/gateway/runtime/lifecycle.d.ts +13 -0
- package/executors/gateway/runtime/lifecycle.d.ts.map +1 -0
- package/executors/gateway/runtime/lifecycle.js +179 -0
- package/executors/gateway/runtime/observability.d.ts +17 -0
- package/executors/gateway/runtime/observability.d.ts.map +1 -0
- package/executors/gateway/runtime/observability.js +107 -0
- package/executors/gateway/runtime/proxy.d.ts +20 -0
- package/executors/gateway/runtime/proxy.d.ts.map +1 -0
- package/executors/gateway/runtime/proxy.js +113 -0
- package/executors/gateway/runtime/scope.d.ts +16 -0
- package/executors/gateway/runtime/scope.d.ts.map +1 -0
- package/executors/gateway/runtime/scope.js +68 -0
- package/executors/gateway/runtime/server.d.ts +3 -0
- package/executors/gateway/runtime/server.d.ts.map +1 -0
- package/executors/gateway/runtime/server.js +168 -0
- package/executors/gateway/runtime/service-keys.d.ts +11 -0
- package/executors/gateway/runtime/service-keys.d.ts.map +1 -0
- package/executors/gateway/runtime/service-keys.js +49 -0
- package/executors/gateway/runtime/types.d.ts +234 -0
- package/executors/gateway/runtime/types.d.ts.map +1 -0
- package/executors/gateway/runtime/types.js +2 -0
- package/executors/gateway/runtime/upgrade.d.ts +22 -0
- package/executors/gateway/runtime/upgrade.d.ts.map +1 -0
- package/executors/gateway/runtime/upgrade.js +63 -0
- 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,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
|
+
}
|