@lensmcp/cluster 1.11.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-errors.d.ts +36 -0
- package/executors/gateway/gateway-errors.d.ts.map +1 -0
- package/executors/gateway/gateway-errors.js +43 -0
- 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 -1034
- package/executors/gateway/main.prod-gateway.js +74 -2
- package/executors/gateway/prod-gateway.lib.d.ts +19 -143
- package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
- package/executors/gateway/prod-gateway.lib.js +16 -831
- 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,9 @@
|
|
|
1
|
+
import type * as http from 'node:http';
|
|
2
|
+
import type { Attrs } from './types';
|
|
3
|
+
/** Verify the end-user JWT and return its claims, or undefined. */
|
|
4
|
+
export declare function verifyDevJwt(req: http.IncomingMessage): Record<string, unknown> | undefined;
|
|
5
|
+
/** Stamp verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
|
|
6
|
+
export declare function stampIdentity(req: http.IncomingMessage, claims: Record<string, unknown>): void;
|
|
7
|
+
/** Build the ABAC attribute bag from the verified claims + request envelope. */
|
|
8
|
+
export declare const devAttrs: (req: http.IncomingMessage, claims: Record<string, unknown>) => Attrs;
|
|
9
|
+
//# sourceMappingURL=dev-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-auth.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/dev-auth.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,mEAAmE;AACnE,wBAAgB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAiC3F;AAED,+GAA+G;AAC/G,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAQ9F;AAED,gFAAgF;AAChF,eAAO,MAAM,QAAQ,GAAI,KAAK,IAAI,CAAC,eAAe,EAAE,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,KAKpF,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.devAttrs = void 0;
|
|
4
|
+
exports.verifyDevJwt = verifyDevJwt;
|
|
5
|
+
exports.stampIdentity = stampIdentity;
|
|
6
|
+
/**
|
|
7
|
+
* Dev edge auth (HS256), mirroring the prod gateway: verify the end-user token, then stamp the
|
|
8
|
+
* trusted identity headers the services read (via GatewayTrustGuard). RS256/JWKS is prod's job;
|
|
9
|
+
* dev shares the `AUTH_JWT_SECRET` the auth host signs with. Pure functions.
|
|
10
|
+
*/
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
|
+
/** Verify the end-user JWT and return its claims, or undefined. */
|
|
13
|
+
function verifyDevJwt(req) {
|
|
14
|
+
const secret = process.env['AUTH_JWT_SECRET'] ?? 'tetros-dev-secret';
|
|
15
|
+
const auth = String((Array.isArray(req.headers['authorization']) ? req.headers['authorization'][0] : req.headers['authorization']) ?? '');
|
|
16
|
+
if (!auth.startsWith('Bearer '))
|
|
17
|
+
return undefined;
|
|
18
|
+
const [h, p, s] = auth.slice(7).split('.');
|
|
19
|
+
if (!h || !p || !s)
|
|
20
|
+
return undefined;
|
|
21
|
+
// An IdP that signs ASYMMETRIC (RS256 + JWKS — e.g. tetros's auth host) does NOT use the HS256 dev
|
|
22
|
+
// shared-secret this dev edge assumed. The PROD gateway verifies RS256 via jwks-verify; in dev we
|
|
23
|
+
// DECODE + exp-check the claims (no signature check) — the gateway is the only local ingress, the token
|
|
24
|
+
// came from the trusted local IdP, and a dev IdP's ephemeral per-pod keys make in-cluster JWKS
|
|
25
|
+
// verification unreliable. (The HS256 path below stays for services using the shared-secret convention.)
|
|
26
|
+
let alg;
|
|
27
|
+
try {
|
|
28
|
+
alg = JSON.parse(Buffer.from(h, 'base64url').toString())['alg'];
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (typeof alg === 'string' && alg !== 'HS256') {
|
|
34
|
+
try {
|
|
35
|
+
const rsClaims = JSON.parse(Buffer.from(p, 'base64url').toString());
|
|
36
|
+
if (typeof rsClaims['exp'] === 'number' && Date.now() / 1000 > rsClaims['exp'])
|
|
37
|
+
return undefined;
|
|
38
|
+
return rsClaims;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const expected = (0, node_crypto_1.createHmac)('sha256', secret).update(`${h}.${p}`).digest('base64url');
|
|
45
|
+
const a = Buffer.from(s);
|
|
46
|
+
const b = Buffer.from(expected);
|
|
47
|
+
if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b))
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const claims = JSON.parse(Buffer.from(p, 'base64url').toString());
|
|
51
|
+
if (typeof claims['exp'] === 'number' && Date.now() / 1000 > claims['exp'])
|
|
52
|
+
return undefined;
|
|
53
|
+
return claims;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Stamp verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
|
|
60
|
+
function stampIdentity(req, claims) {
|
|
61
|
+
const set = (k, v) => { if (v !== undefined && v !== null && v !== '')
|
|
62
|
+
req.headers[k] = String(v); };
|
|
63
|
+
set('x-user-id', claims['sub']);
|
|
64
|
+
set('x-tenant-id', claims['tid']);
|
|
65
|
+
const roles = claims['roles'];
|
|
66
|
+
if (Array.isArray(roles) && roles.length)
|
|
67
|
+
req.headers['x-user-roles'] = roles.map(String).join(',');
|
|
68
|
+
const perms = claims['perms'];
|
|
69
|
+
if (Array.isArray(perms) && perms.length)
|
|
70
|
+
req.headers['x-user-permissions'] = perms.map(String).join(',');
|
|
71
|
+
}
|
|
72
|
+
/** Build the ABAC attribute bag from the verified claims + request envelope. */
|
|
73
|
+
const devAttrs = (req, claims) => ({
|
|
74
|
+
...claims,
|
|
75
|
+
ip: req.socket.remoteAddress ?? '',
|
|
76
|
+
method: (req.method ?? 'GET').toUpperCase(),
|
|
77
|
+
path: (req.url ?? '/').split('?')[0],
|
|
78
|
+
});
|
|
79
|
+
exports.devAttrs = devAttrs;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Route, ServiceCtl, SockPool } from './types';
|
|
2
|
+
/** Round-robin over the pool's sockets, rescanning the dir when the cache ages out. */
|
|
3
|
+
export declare function pickSock(pool: SockPool, scanTtlMs?: number): string | undefined;
|
|
4
|
+
/** Extract the port from an `upstream` URL like `http://localhost:5173`. */
|
|
5
|
+
export declare function lensPortFromUpstream(upstream?: string): number | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Read every project's `cluster` (or legacy `davnx`) declaration and build
|
|
8
|
+
* the route table: host routes first (plus auto `internal.<host>` for pod
|
|
9
|
+
* services), the `default` catch-all last.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverRoutes(rootDir: string, projects: Record<string, {
|
|
12
|
+
root: string;
|
|
13
|
+
}>, sockBaseDir?: string): {
|
|
14
|
+
routes: Route[];
|
|
15
|
+
services: ServiceCtl[];
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/discovery.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAoC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE7F,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,SAAO,GAAG,MAAM,GAAG,SAAS,CAa7E;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI1E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAC1C,WAAW,SAAc,GACxB;IAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;CAAE,CAyD7C"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pickSock = pickSock;
|
|
4
|
+
exports.lensPortFromUpstream = lensPortFromUpstream;
|
|
5
|
+
exports.discoverRoutes = discoverRoutes;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
/**
|
|
8
|
+
* Route discovery + socket-pool round-robin. Pure (fs reads only): reads every
|
|
9
|
+
* project's `cluster` (or legacy `davnx`) declaration and builds the route table.
|
|
10
|
+
*/
|
|
11
|
+
const fs = tslib_1.__importStar(require("node:fs"));
|
|
12
|
+
const os = tslib_1.__importStar(require("node:os"));
|
|
13
|
+
const path = tslib_1.__importStar(require("node:path"));
|
|
14
|
+
const manifest_1 = require("../manifest");
|
|
15
|
+
/** Round-robin over the pool's sockets, rescanning the dir when the cache ages out. */
|
|
16
|
+
function pickSock(pool, scanTtlMs = 1500) {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
if (now - pool.scannedAt > scanTtlMs) {
|
|
19
|
+
try {
|
|
20
|
+
pool.socks = fs.readdirSync(pool.dir)
|
|
21
|
+
.filter((f) => /^child-\d+\.sock$/.test(f))
|
|
22
|
+
.map((f) => path.join(pool.dir, f));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
pool.socks = [];
|
|
26
|
+
}
|
|
27
|
+
pool.scannedAt = now;
|
|
28
|
+
}
|
|
29
|
+
if (pool.socks.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
pool.idx = (pool.idx + 1) % pool.socks.length;
|
|
32
|
+
return pool.socks[pool.idx];
|
|
33
|
+
}
|
|
34
|
+
/** Extract the port from an `upstream` URL like `http://localhost:5173`. */
|
|
35
|
+
function lensPortFromUpstream(upstream) {
|
|
36
|
+
if (!upstream)
|
|
37
|
+
return undefined;
|
|
38
|
+
const m = /:(\d+)/.exec(upstream);
|
|
39
|
+
return m ? Number(m[1]) : undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read every project's `cluster` (or legacy `davnx`) declaration and build
|
|
43
|
+
* the route table: host routes first (plus auto `internal.<host>` for pod
|
|
44
|
+
* services), the `default` catch-all last.
|
|
45
|
+
*/
|
|
46
|
+
function discoverRoutes(rootDir, projects, sockBaseDir = os.tmpdir()) {
|
|
47
|
+
const routes = [];
|
|
48
|
+
const services = [];
|
|
49
|
+
let defaultRoute;
|
|
50
|
+
for (const [name, p] of Object.entries(projects)) {
|
|
51
|
+
let decl;
|
|
52
|
+
try {
|
|
53
|
+
const raw = JSON.parse(fs.readFileSync(path.join(rootDir, p.root, 'project.json'), 'utf8'));
|
|
54
|
+
decl = raw.cluster ?? raw.davnx;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!decl)
|
|
60
|
+
continue;
|
|
61
|
+
let pool;
|
|
62
|
+
let svc;
|
|
63
|
+
let target;
|
|
64
|
+
let lens;
|
|
65
|
+
if (decl.service) {
|
|
66
|
+
// `lens` applies to FRONTEND apps only; a pod service is already
|
|
67
|
+
// instrumented by the serve executor's node-instrumentation graft.
|
|
68
|
+
if (decl.lens)
|
|
69
|
+
console.warn(`[gateway] ${name}: cluster.lens is ignored for pod (service) decls — backends are instrumented by serve-hmr.`);
|
|
70
|
+
pool = { dir: path.join(sockBaseDir, `${decl.service}-devserver`), socks: [], idx: -1, scannedAt: 0 };
|
|
71
|
+
svc = { project: name, decl, pool, state: 'down', lastUsed: Date.now(), inflight: 0, lastScaleAt: 0 };
|
|
72
|
+
services.push(svc);
|
|
73
|
+
}
|
|
74
|
+
else if (decl.lens) {
|
|
75
|
+
// Lens-mode frontend: the gateway SPAWNS this app's Vite + instrumentation
|
|
76
|
+
// (deterministically on `port`) and upstreams to it. One LensFrontend per
|
|
77
|
+
// project is SHARED across its host/default routes (so the spawn fires once).
|
|
78
|
+
const port = decl.port ?? lensPortFromUpstream(decl.upstream) ?? 5173;
|
|
79
|
+
target = `http://localhost:${port}`;
|
|
80
|
+
lens = { project: name, projectRoot: path.join(rootDir, p.root), port };
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
target = decl.upstream ?? (decl.port ? `http://localhost:${decl.port}` : undefined);
|
|
84
|
+
if (!target) {
|
|
85
|
+
console.warn(`[gateway] ${name}: no service/upstream/port — skipped.`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const base = { project: name, prependPrefix: decl.prependPrefix, target, pool, svc, prefix: decl.path, auth: decl.auth, lens };
|
|
90
|
+
if (decl.host) {
|
|
91
|
+
routes.push({ ...base, host: decl.host });
|
|
92
|
+
// internal.<host>: east-west traffic, same pods, NO middleware.
|
|
93
|
+
if (pool && decl.internalHost !== false) {
|
|
94
|
+
routes.push({
|
|
95
|
+
...base,
|
|
96
|
+
host: typeof decl.internalHost === 'string' ? decl.internalHost : `internal.${decl.host}`,
|
|
97
|
+
internal: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (decl.default)
|
|
102
|
+
defaultRoute = { ...base, prefix: undefined };
|
|
103
|
+
}
|
|
104
|
+
if (defaultRoute)
|
|
105
|
+
routes.push(defaultRoute);
|
|
106
|
+
// specificity sort (shared with prod): host+path beats host-only beats
|
|
107
|
+
// catch-all — so a path-mounted service (api.x/channels) wins over the
|
|
108
|
+
// host's owner (api.x → api).
|
|
109
|
+
(0, manifest_1.sortBySpecificity)(routes);
|
|
110
|
+
return { routes, services };
|
|
111
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge primitives shared by the HTTP handler and the WS upgrade: the localized
|
|
3
|
+
* error envelope, the CORS headers, and the trust-header allowlist. Pure (no
|
|
4
|
+
* runtime state) so both pipelines and any hook can use them.
|
|
5
|
+
*/
|
|
6
|
+
import type * as http from 'node:http';
|
|
7
|
+
import { type GatewayErrorReason } from '../gateway-errors';
|
|
8
|
+
/**
|
|
9
|
+
* Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
|
|
10
|
+
* `text/plain`) + an `x-request-id` header — the SAME localized contract a service returns, so a browser
|
|
11
|
+
* app renders `t(key)` and surfaces the trace id. Mirrors the prod gateway's `sendError`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const sendError: (res: http.ServerResponse, req: http.IncomingMessage, status: number, reason: GatewayErrorReason, detail?: string) => void;
|
|
14
|
+
/**
|
|
15
|
+
* The gateway OWNS these headers — strip any client-supplied copy at ingress so identity + trust can
|
|
16
|
+
* only originate from the gateway (mirrors the prod gateway's stripTrust). Applied at the very top of
|
|
17
|
+
* BOTH the HTTP handler and the WS upgrade.
|
|
18
|
+
*/
|
|
19
|
+
export declare const GATEWAY_OWNED_HEADERS: string[];
|
|
20
|
+
/** Reflect the request Origin back with credentials — the gateway is the edge CORS authority. */
|
|
21
|
+
export declare const cors: (req: http.IncomingMessage, res: http.ServerResponse) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Answer a CORS preflight (OPTIONS) at the edge: reflect CORS + 204 with the allowed `methods` and the
|
|
24
|
+
* requested headers echoed back. `methods` differs by caller — the no-route host-root uses the narrow
|
|
25
|
+
* `GET,HEAD,OPTIONS`; a routed path uses the wide verb list.
|
|
26
|
+
*/
|
|
27
|
+
export declare const corsPreflight: (req: http.IncomingMessage, res: http.ServerResponse, methods: string) => void;
|
|
28
|
+
/** Allowed-methods lists for the two preflight sites. */
|
|
29
|
+
export declare const PREFLIGHT_METHODS_NOROUTE = "GET,HEAD,OPTIONS";
|
|
30
|
+
export declare const PREFLIGHT_METHODS_ROUTED = "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS";
|
|
31
|
+
//# sourceMappingURL=edge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/edge.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,eAAO,MAAM,SAAS,GACpB,KAAK,IAAI,CAAC,cAAc,EACxB,KAAK,IAAI,CAAC,eAAe,EACzB,QAAQ,MAAM,EACd,QAAQ,kBAAkB,EAC1B,SAAS,MAAM,KACd,IAIF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,UAAyG,CAAC;AAE5I,iGAAiG;AACjG,eAAO,MAAM,IAAI,GAAI,KAAK,IAAI,CAAC,eAAe,EAAE,KAAK,IAAI,CAAC,cAAc,KAAG,IAK1E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,IAAI,CAAC,eAAe,EAAE,KAAK,IAAI,CAAC,cAAc,EAAE,SAAS,MAAM,KAAG,IAOpG,CAAC;AAEF,yDAAyD;AACzD,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAC5D,eAAO,MAAM,wBAAwB,2CAA2C,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PREFLIGHT_METHODS_ROUTED = exports.PREFLIGHT_METHODS_NOROUTE = exports.corsPreflight = exports.cors = exports.GATEWAY_OWNED_HEADERS = exports.sendError = void 0;
|
|
4
|
+
const gateway_errors_1 = require("../gateway-errors");
|
|
5
|
+
/**
|
|
6
|
+
* Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
|
|
7
|
+
* `text/plain`) + an `x-request-id` header — the SAME localized contract a service returns, so a browser
|
|
8
|
+
* app renders `t(key)` and surfaces the trace id. Mirrors the prod gateway's `sendError`.
|
|
9
|
+
*/
|
|
10
|
+
const sendError = (res, req, status, reason, detail) => {
|
|
11
|
+
const traceId = (0, gateway_errors_1.ensureRequestId)(req);
|
|
12
|
+
if (!res.headersSent)
|
|
13
|
+
res.writeHead(status, { 'content-type': gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE, 'x-request-id': traceId });
|
|
14
|
+
res.end((0, gateway_errors_1.gatewayErrorBody)(status, reason, traceId, detail));
|
|
15
|
+
};
|
|
16
|
+
exports.sendError = sendError;
|
|
17
|
+
/**
|
|
18
|
+
* The gateway OWNS these headers — strip any client-supplied copy at ingress so identity + trust can
|
|
19
|
+
* only originate from the gateway (mirrors the prod gateway's stripTrust). Applied at the very top of
|
|
20
|
+
* BOTH the HTTP handler and the WS upgrade.
|
|
21
|
+
*/
|
|
22
|
+
exports.GATEWAY_OWNED_HEADERS = ['x-internal-token', 'x-api-key-id', 'x-user-id', 'x-tenant-id', 'x-user-roles', 'x-user-permissions'];
|
|
23
|
+
/** Reflect the request Origin back with credentials — the gateway is the edge CORS authority. */
|
|
24
|
+
const cors = (req, res) => {
|
|
25
|
+
const origin = String(req.headers.origin ?? '*');
|
|
26
|
+
res.setHeader('access-control-allow-origin', origin);
|
|
27
|
+
res.setHeader('access-control-allow-credentials', 'true');
|
|
28
|
+
res.setHeader('vary', 'Origin');
|
|
29
|
+
};
|
|
30
|
+
exports.cors = cors;
|
|
31
|
+
/**
|
|
32
|
+
* Answer a CORS preflight (OPTIONS) at the edge: reflect CORS + 204 with the allowed `methods` and the
|
|
33
|
+
* requested headers echoed back. `methods` differs by caller — the no-route host-root uses the narrow
|
|
34
|
+
* `GET,HEAD,OPTIONS`; a routed path uses the wide verb list.
|
|
35
|
+
*/
|
|
36
|
+
const corsPreflight = (req, res, methods) => {
|
|
37
|
+
(0, exports.cors)(req, res);
|
|
38
|
+
res.writeHead(204, {
|
|
39
|
+
'access-control-allow-methods': methods,
|
|
40
|
+
'access-control-allow-headers': String(req.headers['access-control-request-headers'] ?? '*'),
|
|
41
|
+
});
|
|
42
|
+
res.end();
|
|
43
|
+
};
|
|
44
|
+
exports.corsPreflight = corsPreflight;
|
|
45
|
+
/** Allowed-methods lists for the two preflight sites. */
|
|
46
|
+
exports.PREFLIGHT_METHODS_NOROUTE = 'GET,HEAD,OPTIONS';
|
|
47
|
+
exports.PREFLIGHT_METHODS_ROUTED = 'GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP request pipeline — the composition of every layer into one ordered
|
|
3
|
+
* flow. The ORDER here is a security contract (each step's position fixes a real
|
|
4
|
+
* bug); see the inline notes. Customer hooks slot in at the named phases.
|
|
5
|
+
*
|
|
6
|
+
* strip-trust → onRequest → match → (no-route fallback) → routed-preflight →
|
|
7
|
+
* cors → beforeAuth → auth/authz → afterAuth(+builtin middleware) →
|
|
8
|
+
* inject-token → beforeForward → forward → onResponse / onError
|
|
9
|
+
*/
|
|
10
|
+
import type * as http from 'node:http';
|
|
11
|
+
import type { GatewayRuntime } from './types';
|
|
12
|
+
import type { Observability } from './observability';
|
|
13
|
+
import type { EdgeAuth } from './auth';
|
|
14
|
+
import type { ProxyLayer } from './proxy';
|
|
15
|
+
import type { HooksLayer } from './hooks';
|
|
16
|
+
export interface HandlerDeps {
|
|
17
|
+
rt: GatewayRuntime;
|
|
18
|
+
obs: Observability;
|
|
19
|
+
auth: EdgeAuth;
|
|
20
|
+
proxy: ProxyLayer;
|
|
21
|
+
hooks: HooksLayer;
|
|
22
|
+
}
|
|
23
|
+
export declare function createHandler(deps: HandlerDeps): http.RequestListener;
|
|
24
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAWvC,OAAO,KAAK,EAAE,cAAc,EAAW,MAAM,SAAS,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,cAAc,CAAC;IACnB,GAAG,EAAE,aAAa,CAAC;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC,eAAe,CA2IrE"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHandler = createHandler;
|
|
4
|
+
const manifest_1 = require("../manifest");
|
|
5
|
+
const gateway_errors_1 = require("../gateway-errors");
|
|
6
|
+
const discovery_1 = require("./discovery");
|
|
7
|
+
const edge_1 = require("./edge");
|
|
8
|
+
function createHandler(deps) {
|
|
9
|
+
const { rt, obs, auth, proxy, hooks } = deps;
|
|
10
|
+
const match = (req) => (0, manifest_1.matchRoute)(rt.routes, (req.headers.host || '').split(':')[0], req.url ?? '/');
|
|
11
|
+
// Host-root infra: when a host only has PATH-mounted services (pure
|
|
12
|
+
// microservices under api.<domain>/{prefix}), the gateway itself owns the
|
|
13
|
+
// root — CORS, health aggregation + service discovery, like a real edge.
|
|
14
|
+
const handleNoRoute = (req, res) => {
|
|
15
|
+
// gateway-owned endpoints are cross-origin by design (the app calls
|
|
16
|
+
// api.<domain>/health from app.<domain>) — answer CORS here; routed
|
|
17
|
+
// paths inherit CORS from the services themselves.
|
|
18
|
+
(0, edge_1.cors)(req, res);
|
|
19
|
+
if (req.method === 'OPTIONS') {
|
|
20
|
+
res.writeHead(204, {
|
|
21
|
+
'access-control-allow-methods': edge_1.PREFLIGHT_METHODS_NOROUTE,
|
|
22
|
+
'access-control-allow-headers': String(req.headers['access-control-request-headers'] ?? '*'),
|
|
23
|
+
});
|
|
24
|
+
res.end();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const host = (req.headers.host || '').split(':')[0];
|
|
28
|
+
const mounted = rt.routes.filter((r) => r.host && (0, manifest_1.hostMatches)(r.host, host) && r.prefix);
|
|
29
|
+
const url = (req.url ?? '/').split('?')[0];
|
|
30
|
+
if (mounted.length > 0 && (url === '/health' || url === '/healthz')) {
|
|
31
|
+
const services = mounted.map((r) => ({
|
|
32
|
+
project: r.project,
|
|
33
|
+
prefix: r.prefix,
|
|
34
|
+
pods: r.pool ? ((0, discovery_1.pickSock)(r.pool, 0), r.pool.socks.length) : undefined,
|
|
35
|
+
state: r.svc?.state ?? (r.target ? 'up' : 'unknown'),
|
|
36
|
+
}));
|
|
37
|
+
const ok = services.every((s) => s.state !== 'down' || true);
|
|
38
|
+
res.writeHead(ok ? 200 : 503, { 'content-type': 'application/json' });
|
|
39
|
+
res.end(JSON.stringify({ status: 'ok', host, services }, null, 2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (mounted.length > 0 && url === '/.well-known/services') {
|
|
43
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
44
|
+
res.end(JSON.stringify({ host, services: mounted.map((r) => ({ prefix: r.prefix, project: r.project, internal: !!r.internal })) }, null, 2));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
(0, edge_1.sendError)(res, req, 404, 'not_found', mounted.length > 0
|
|
48
|
+
? `No service mounted at ${url}. Mounted on ${host}: ${[...new Set(mounted.map((r) => r.prefix))].join(', ')} (+ /health, /.well-known/services)`
|
|
49
|
+
: 'No gateway route for this hostname.');
|
|
50
|
+
};
|
|
51
|
+
const runPipeline = async (req, res) => {
|
|
52
|
+
// 1. STRIP client-supplied trust headers before anything else: the gateway is
|
|
53
|
+
// the ONLY party allowed to assert `x-internal-token` (came through me) and
|
|
54
|
+
// `x-api-key-id` (validated caller identity). Without this, a keyless/TCP route
|
|
55
|
+
// would leak a spoofed token and any public route would forward a spoofed caller
|
|
56
|
+
// id to downstream audit. The gateway re-stamps them below where earned.
|
|
57
|
+
for (const h of edge_1.GATEWAY_OWNED_HEADERS)
|
|
58
|
+
delete req.headers[h];
|
|
59
|
+
// 2. Mint a trace id for EVERY request (not just lens-traced ones): forwarded to the
|
|
60
|
+
// pod (adopted as its traceId) and returned on the response — any request is traceable.
|
|
61
|
+
(0, gateway_errors_1.ensureRequestId)(req);
|
|
62
|
+
const trace = obs.beginTrace(req);
|
|
63
|
+
if (trace) {
|
|
64
|
+
obs.traceStep(trace, 'received', { host: trace.host, method: trace.method, url: trace.url });
|
|
65
|
+
req.__lensmcpTrace = trace;
|
|
66
|
+
}
|
|
67
|
+
const ctx = hooks.httpContext(req, res, trace);
|
|
68
|
+
try {
|
|
69
|
+
// 3. onRequest (sees already-stripped headers; may short-circuit)
|
|
70
|
+
if (hooks.has('onRequest')) {
|
|
71
|
+
await hooks.run('onRequest', ctx);
|
|
72
|
+
if (ctx.responded())
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// 4. route match
|
|
76
|
+
const route = match(req);
|
|
77
|
+
if (!route) {
|
|
78
|
+
handleNoRoute(req, res);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
ctx.route = route;
|
|
82
|
+
obs.traceStep(trace, 'route-match', {
|
|
83
|
+
project: route.project,
|
|
84
|
+
...(route.prefix ? { prefix: route.prefix } : {}),
|
|
85
|
+
...(route.host ? { routeHost: route.host } : {}),
|
|
86
|
+
...(route.internal ? { internal: true } : {}),
|
|
87
|
+
});
|
|
88
|
+
// 7. CORS preflight on a ROUTED path: answer it HERE, BEFORE edge auth — an OPTIONS preflight
|
|
89
|
+
// carries no token by spec, so the JWT (or x-api-key) enforcement below would 401 it and the
|
|
90
|
+
// browser's CORS check fails. The matching real request still goes through auth normally.
|
|
91
|
+
if (req.method === 'OPTIONS') {
|
|
92
|
+
(0, edge_1.cors)(req, res);
|
|
93
|
+
res.writeHead(204, {
|
|
94
|
+
'access-control-allow-methods': edge_1.PREFLIGHT_METHODS_ROUTED,
|
|
95
|
+
'access-control-allow-headers': String(req.headers['access-control-request-headers'] ?? '*'),
|
|
96
|
+
});
|
|
97
|
+
res.end();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// 8. CORS on EVERY routed response — including the 401/403/5xx the gateway generates ITSELF
|
|
101
|
+
// below. Without this a gateway-generated error short-circuits before the upstream (which
|
|
102
|
+
// carries CORS on success), so the browser sees the error WITHOUT an Access-Control-Allow-Origin
|
|
103
|
+
// and reports a CORS failure that MASKS the real status. On success the upstream's CORS header
|
|
104
|
+
// overwrites this one (http-proxy setHeader), so there's no duplicate.
|
|
105
|
+
(0, edge_1.cors)(req, res);
|
|
106
|
+
// 9. beforeAuth
|
|
107
|
+
if (hooks.has('beforeAuth')) {
|
|
108
|
+
await hooks.run('beforeAuth', ctx);
|
|
109
|
+
if (ctx.responded())
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// 10/11. auth + authz. internal.* validates the caller's api-key; public verifies the end-user
|
|
113
|
+
// JWT + enforces per-route rules. Custom middleware (afterAuth) runs AFTER core auth, public ONLY.
|
|
114
|
+
if (route.internal) {
|
|
115
|
+
const v = auth.internal(req, route, trace);
|
|
116
|
+
if (!v.ok) {
|
|
117
|
+
obs.finishTrace(trace, route.project, v.status);
|
|
118
|
+
(0, edge_1.sendError)(res, req, v.status, v.reason, v.detail);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
ctx.caller = v.caller;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const v = auth.public(req, route, trace);
|
|
125
|
+
if (!v.ok) {
|
|
126
|
+
obs.finishTrace(trace, route.project, v.status);
|
|
127
|
+
(0, edge_1.sendError)(res, req, v.status, v.reason, v.detail);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
ctx.claims = v.claims;
|
|
131
|
+
if (hooks.has('afterAuth')) {
|
|
132
|
+
await hooks.run('afterAuth', ctx);
|
|
133
|
+
if (ctx.responded())
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 12. inject the target service's key so pods trust the gateway (pool routes, both branches)
|
|
138
|
+
if (route.pool && rt.serviceKeys[route.project]) {
|
|
139
|
+
req.headers['x-internal-token'] = rt.serviceKeys[route.project];
|
|
140
|
+
}
|
|
141
|
+
// 13. beforeForward (last chance to mutate headers/url)
|
|
142
|
+
if (hooks.has('beforeForward')) {
|
|
143
|
+
await hooks.run('beforeForward', ctx);
|
|
144
|
+
if (ctx.responded())
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// 15. onResponse — fire user handlers once the response closes (the built-in
|
|
148
|
+
// recordEdge/finishTrace live in proxy.forward's own close handler).
|
|
149
|
+
if (hooks.has('onResponse')) {
|
|
150
|
+
res.on('close', () => { ctx.status = res.statusCode ?? 0; void hooks.run('onResponse', ctx); });
|
|
151
|
+
}
|
|
152
|
+
// 14. forward
|
|
153
|
+
void proxy.forward(route, req, res);
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
// A hook (or the pipeline) threw — narrate via onError, then 500 if nobody responded.
|
|
157
|
+
ctx.error = e;
|
|
158
|
+
if (hooks.has('onError'))
|
|
159
|
+
await hooks.run('onError', ctx);
|
|
160
|
+
if (!ctx.responded())
|
|
161
|
+
(0, edge_1.sendError)(res, req, 500, 'internal', e.message);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
return (req, res) => { void runPipeline(req, res); };
|
|
165
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The customer extensibility layer: phase-named hooks. Customers register
|
|
3
|
+
* handlers per phase (`onRequest`, `beforeAuth`, `afterAuth`, `beforeForward`,
|
|
4
|
+
* `onResponse`, `onError`, `onUpgrade`); the built-in pipeline runs them in
|
|
5
|
+
* order at the matching point and short-circuits the moment one responds. The
|
|
6
|
+
* built-in `options.middleware` is wired (by the orchestrator) as the FIRST
|
|
7
|
+
* `afterAuth` handler, so "custom middleware runs AFTER core auth" is preserved.
|
|
8
|
+
*/
|
|
9
|
+
import type * as http from 'node:http';
|
|
10
|
+
import type * as stream from 'node:stream';
|
|
11
|
+
import type { GatewayHooks, GatewayPhase, GwTrace, HookContext } from './types';
|
|
12
|
+
export interface HooksLayer {
|
|
13
|
+
/** Run every handler registered for `phase`, in order, stopping if one responds. */
|
|
14
|
+
run(phase: GatewayPhase, ctx: HookContext): Promise<void>;
|
|
15
|
+
/** True if any handler is registered for `phase` (lets the pipeline skip work). */
|
|
16
|
+
has(phase: GatewayPhase): boolean;
|
|
17
|
+
/** Build the per-request context for the HTTP pipeline. */
|
|
18
|
+
httpContext(req: http.IncomingMessage, res: http.ServerResponse, trace: GwTrace | undefined): HookContext;
|
|
19
|
+
/** Build the per-request context for the WS upgrade pipeline. */
|
|
20
|
+
upgradeContext(req: http.IncomingMessage, socket: stream.Duplex, trace: GwTrace | undefined): HookContext;
|
|
21
|
+
}
|
|
22
|
+
export declare function createHooks(hooks: GatewayHooks): HooksLayer;
|
|
23
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAOhF,MAAM,WAAW,UAAU;IACzB,oFAAoF;IACpF,GAAG,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,mFAAmF;IACnF,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;IAClC,2DAA2D;IAC3D,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IAC1G,iEAAiE;IACjE,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;CAC3G;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,UAAU,CA6C3D"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHooks = createHooks;
|
|
4
|
+
const edge_1 = require("./edge");
|
|
5
|
+
// Terminal phases observe a request whose response is ALREADY complete, so
|
|
6
|
+
// `responded()` is true by definition — they must run every handler regardless.
|
|
7
|
+
// The request-flow phases short-circuit the moment a handler responds.
|
|
8
|
+
const TERMINAL_PHASES = new Set(['onResponse', 'onError']);
|
|
9
|
+
function createHooks(hooks) {
|
|
10
|
+
const run = async (phase, ctx) => {
|
|
11
|
+
const list = hooks[phase];
|
|
12
|
+
if (!list || list.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
const terminal = TERMINAL_PHASES.has(phase);
|
|
15
|
+
for (const h of list) {
|
|
16
|
+
if (!terminal && ctx.responded())
|
|
17
|
+
return; // a request-flow hook already responded → stop
|
|
18
|
+
await h(ctx);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const has = (phase) => (hooks[phase]?.length ?? 0) > 0;
|
|
22
|
+
const httpContext = (req, res, trace) => ({
|
|
23
|
+
req,
|
|
24
|
+
res,
|
|
25
|
+
socket: undefined,
|
|
26
|
+
trace,
|
|
27
|
+
route: undefined,
|
|
28
|
+
claims: undefined,
|
|
29
|
+
caller: undefined,
|
|
30
|
+
pod: undefined,
|
|
31
|
+
error: undefined,
|
|
32
|
+
status: undefined,
|
|
33
|
+
sendError: (status, reason, detail) => (0, edge_1.sendError)(res, req, status, reason, detail),
|
|
34
|
+
responded: () => res.writableEnded || res.headersSent,
|
|
35
|
+
});
|
|
36
|
+
const upgradeContext = (req, socket, trace) => ({
|
|
37
|
+
req,
|
|
38
|
+
res: undefined,
|
|
39
|
+
socket,
|
|
40
|
+
trace,
|
|
41
|
+
route: undefined,
|
|
42
|
+
claims: undefined,
|
|
43
|
+
caller: undefined,
|
|
44
|
+
pod: undefined,
|
|
45
|
+
error: undefined,
|
|
46
|
+
status: undefined,
|
|
47
|
+
// No HTTP response on an upgrade — short-circuiting means destroying the socket.
|
|
48
|
+
sendError: () => { try {
|
|
49
|
+
socket.destroy();
|
|
50
|
+
}
|
|
51
|
+
catch { /* already gone */ } },
|
|
52
|
+
responded: () => socket.destroyed,
|
|
53
|
+
});
|
|
54
|
+
return { run, has, httpContext, upgradeContext };
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SpawnChild } from '@lensmcp/nx-plugin/lens-frontend';
|
|
2
|
+
import type { GatewayRuntime, GatewayRuntimeOptions } from './types';
|
|
3
|
+
import type { Observability } from './observability';
|
|
4
|
+
export interface LensChildren {
|
|
5
|
+
spawnManagedChild: SpawnChild;
|
|
6
|
+
/** Boot the dashboard + MCP singletons and every `lens:true` app. Pushes the
|
|
7
|
+
* dashboard route onto `rt.routes` — MUST run before TLS SAN derivation. */
|
|
8
|
+
bootSingletonsAndApps(): void;
|
|
9
|
+
/** Reap all managed children (called from `stop()`). */
|
|
10
|
+
reapAll(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function createLensChildren(rt: GatewayRuntime, obs: Observability, options: GatewayRuntimeOptions): LensChildren;
|
|
13
|
+
//# sourceMappingURL=lens-children.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AACnF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMrD,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CA0HvH"}
|