@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,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.baseDomainOf = baseDomainOf;
|
|
4
|
+
exports.readLensScope = readLensScope;
|
|
5
|
+
exports.lensKeyFrom = lensKeyFrom;
|
|
6
|
+
exports.lensSlug = lensSlug;
|
|
7
|
+
const tslib_1 = require("tslib");
|
|
8
|
+
/**
|
|
9
|
+
* Per-workspace lens scope + dashboard-host derivation. Pure (fs reads only),
|
|
10
|
+
* mirrors the `lensmcp` CLI's `workspace-scope` (kept in lock-step).
|
|
11
|
+
*/
|
|
12
|
+
const fs = tslib_1.__importStar(require("node:fs"));
|
|
13
|
+
const path = tslib_1.__importStar(require("node:path"));
|
|
14
|
+
/** The base domain for the lens dashboard host — the longest common dotted
|
|
15
|
+
* suffix of the declared cluster hosts (e.g. ['api.tetros.ai.local',
|
|
16
|
+
* 'tetros.ai.local'] → 'tetros.ai.local'). Falls back to the first host, then
|
|
17
|
+
* to 'lens.local' if no host is declared. */
|
|
18
|
+
function baseDomainOf(hosts) {
|
|
19
|
+
const clean = hosts.filter((h) => !!h && !h.startsWith('*.'));
|
|
20
|
+
if (clean.length === 0)
|
|
21
|
+
return 'local';
|
|
22
|
+
// Use the SHORTEST host as the candidate base (most likely the apex), then
|
|
23
|
+
// require every other host to end with it.
|
|
24
|
+
const shortest = clean.reduce((a, b) => (b.length < a.length ? b : a));
|
|
25
|
+
if (clean.every((h) => h === shortest || h.endsWith('.' + shortest)))
|
|
26
|
+
return shortest;
|
|
27
|
+
// No clean common apex — derive the trailing 2-3 labels of the shortest host.
|
|
28
|
+
const parts = shortest.split('.');
|
|
29
|
+
return parts.slice(-Math.min(parts.length, 3)).join('.');
|
|
30
|
+
}
|
|
31
|
+
/** Per-workspace lens scope (key + dashboard port + mount path). Prefers the
|
|
32
|
+
* CLI-written `.lensmcp/config.json` (so the gateway agrees with `lensmcp gateway`
|
|
33
|
+
* / `lensmcp dashboard`); falls back to deriving from the workspace. */
|
|
34
|
+
function readLensScope(root) {
|
|
35
|
+
try {
|
|
36
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(root, '.lensmcp', 'config.json'), 'utf8'));
|
|
37
|
+
if (cfg && typeof cfg.key === 'string') {
|
|
38
|
+
return {
|
|
39
|
+
key: cfg.key,
|
|
40
|
+
dashboardPort: cfg.ports?.dashboard ?? 4321,
|
|
41
|
+
basePath: cfg.dashboardBasePath ?? '/' + cfg.key,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* no config yet — derive below */
|
|
47
|
+
}
|
|
48
|
+
const key = lensKeyFrom(root);
|
|
49
|
+
return { key, dashboardPort: 4321, basePath: '/' + key };
|
|
50
|
+
}
|
|
51
|
+
function lensKeyFrom(root) {
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
54
|
+
if (pkg?.name)
|
|
55
|
+
return lensSlug(pkg.name);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* fall through to dir name */
|
|
59
|
+
}
|
|
60
|
+
return lensSlug(path.basename(root));
|
|
61
|
+
}
|
|
62
|
+
function lensSlug(input) {
|
|
63
|
+
return (input
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/^@[^/]+\//, '')
|
|
66
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
67
|
+
.replace(/(^-|-$)/g, '') || 'workspace');
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAe,MAAM,SAAS,CAAC;AAEjH,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAmJxB"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startGateway = startGateway;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
/**
|
|
6
|
+
* The composition root. `startGateway` resolves options, discovers routes, wires
|
|
7
|
+
* every layer (observability → service lifecycle → lens children → proxy → auth →
|
|
8
|
+
* hooks → handler/upgrade), boots TLS + listeners, and returns a `GatewayHandle`.
|
|
9
|
+
*
|
|
10
|
+
* The TOP-TO-BOTTOM ORDER here is itself a contract: service keys load before the
|
|
11
|
+
* runtime is built; lens children boot (pushing the dashboard route) BEFORE the
|
|
12
|
+
* cert SANs are derived from `routes`; eager services boot before listeners; the
|
|
13
|
+
* `gateway-up` event fires only after the ports are bound.
|
|
14
|
+
*/
|
|
15
|
+
const http = tslib_1.__importStar(require("node:http"));
|
|
16
|
+
const path = tslib_1.__importStar(require("node:path"));
|
|
17
|
+
const discovery_1 = require("./discovery");
|
|
18
|
+
const service_keys_1 = require("./service-keys");
|
|
19
|
+
const observability_1 = require("./observability");
|
|
20
|
+
const lifecycle_1 = require("./lifecycle");
|
|
21
|
+
const lens_children_1 = require("./lens-children");
|
|
22
|
+
const proxy_1 = require("./proxy");
|
|
23
|
+
const auth_1 = require("./auth");
|
|
24
|
+
const hooks_1 = require("./hooks");
|
|
25
|
+
const handler_1 = require("./handler");
|
|
26
|
+
const upgrade_1 = require("./upgrade");
|
|
27
|
+
async function startGateway(options, context) {
|
|
28
|
+
// NB: this local `https` (TLS-on flag) deliberately SHADOWS the `node:https`
|
|
29
|
+
// module — which is why TLS/agents are reached via `require('node:https')`.
|
|
30
|
+
const https = options.https !== false;
|
|
31
|
+
const ports = options.ports?.length ? options.ports : [443];
|
|
32
|
+
const scanTtlMs = options.scanTtlMs ?? 1500;
|
|
33
|
+
const startupTimeoutMs = options.startupTimeoutMs ?? 120_000;
|
|
34
|
+
const sweepMs = options.sweepMs ?? 10_000;
|
|
35
|
+
const trafficFlushMs = options.trafficFlushMs ?? 5000;
|
|
36
|
+
// Set by stop(); read via rt.stopped() so an in-flight cold start neither
|
|
37
|
+
// spawns a process nor logs after the gateway is torn down.
|
|
38
|
+
let stopped = false;
|
|
39
|
+
// The gateway is a first-class lens source — the shared event file the MCP tails.
|
|
40
|
+
const eventFile = process.env['LENSMCP_EVENT_FILE'] ?? path.join(context.root, '.lensmcp', 'events.jsonl');
|
|
41
|
+
// --- discover ---------------------------------------------------------------
|
|
42
|
+
const { routes, services } = (0, discovery_1.discoverRoutes)(context.root, context.projectsConfigurations?.projects ?? {});
|
|
43
|
+
if (routes.length === 0) {
|
|
44
|
+
throw new Error('[gateway] no project declares a `cluster` field — nothing to route.');
|
|
45
|
+
}
|
|
46
|
+
// --- per-service api keys (the gateway is the trust boundary) ---------------
|
|
47
|
+
const { serviceKeys, keyToProject } = (0, service_keys_1.loadServiceKeys)(context.root, services);
|
|
48
|
+
// --- the shared runtime (state + knobs; stopped() is a live getter) ---------
|
|
49
|
+
const rt = {
|
|
50
|
+
root: context.root,
|
|
51
|
+
eventFile,
|
|
52
|
+
routes,
|
|
53
|
+
services,
|
|
54
|
+
serviceKeys,
|
|
55
|
+
keyToProject,
|
|
56
|
+
scanTtlMs,
|
|
57
|
+
startupTimeoutMs,
|
|
58
|
+
sweepMs,
|
|
59
|
+
trafficFlushMs,
|
|
60
|
+
https,
|
|
61
|
+
stopped: () => stopped,
|
|
62
|
+
};
|
|
63
|
+
// --- layers -----------------------------------------------------------------
|
|
64
|
+
const obs = (0, observability_1.createObservability)(eventFile, { trafficFlushMs });
|
|
65
|
+
const svcLayer = (0, lifecycle_1.createServiceLayer)(rt, obs);
|
|
66
|
+
const lens = (0, lens_children_1.createLensChildren)(rt, obs, options);
|
|
67
|
+
const proxyLayer = (0, proxy_1.createProxy)(rt, obs, svcLayer);
|
|
68
|
+
const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs);
|
|
69
|
+
// Custom user middleware (the JWT seam; internal.* routes skip it) is wired as
|
|
70
|
+
// the BUILT-IN FIRST `afterAuth` hook — so "custom middleware runs AFTER core
|
|
71
|
+
// auth" and the `{ mode:'middleware' }` trace step are preserved.
|
|
72
|
+
let middleware;
|
|
73
|
+
if (options.middleware) {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
75
|
+
const mod = require(path.resolve(context.root, options.middleware));
|
|
76
|
+
middleware = (mod.default ?? mod);
|
|
77
|
+
}
|
|
78
|
+
const builtinMiddleware = (ctx) => {
|
|
79
|
+
if (!middleware)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
middleware(ctx.req);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
obs.traceStep(ctx.trace, 'auth', { mode: 'middleware', ok: false, reason: e.message });
|
|
86
|
+
if (ctx.route)
|
|
87
|
+
obs.finishTrace(ctx.trace, ctx.route.project, 401);
|
|
88
|
+
ctx.sendError(401, 'unauthorized', `Gateway middleware rejected the request: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const userHooks = options.hooks ?? {};
|
|
92
|
+
const hooks = (0, hooks_1.createHooks)({
|
|
93
|
+
...userHooks,
|
|
94
|
+
afterAuth: [builtinMiddleware, ...(userHooks.afterAuth ?? [])],
|
|
95
|
+
});
|
|
96
|
+
const handler = (0, handler_1.createHandler)({ rt, obs, auth: edgeAuth, proxy: proxyLayer, hooks });
|
|
97
|
+
const upgrade = (0, upgrade_1.createUpgrade)({ rt, auth: edgeAuth, proxy: proxyLayer, hooks });
|
|
98
|
+
// Background timers (both `.unref()`'d): traffic-edge flush + idle sweeper.
|
|
99
|
+
const edgeFlusher = obs.startEdgeFlusher();
|
|
100
|
+
const sweeper = svcLayer.startSweeper();
|
|
101
|
+
// --- lens-mode children: dashboard + MCP singletons + `lens:true` apps ------
|
|
102
|
+
// MUST run before TLS — it pushes the dashboard route, which joins the cert SANs.
|
|
103
|
+
lens.bootSingletonsAndApps();
|
|
104
|
+
// Eager services boot with the gateway.
|
|
105
|
+
for (const svc of services) {
|
|
106
|
+
if (svc.decl.eager)
|
|
107
|
+
void svcLayer.ensureUp(svc);
|
|
108
|
+
}
|
|
109
|
+
// --- TLS + listeners --------------------------------------------------------
|
|
110
|
+
let pem;
|
|
111
|
+
let caPath;
|
|
112
|
+
if (https) {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
114
|
+
const basicSsl = require('../../../basic-ssl');
|
|
115
|
+
const cacheDir = (0, service_keys_1.gatewayCacheDir)(context.root);
|
|
116
|
+
const hosts = routes.map((r) => r.host).filter((h) => !!h);
|
|
117
|
+
pem = basicSsl.getCertificateSync(cacheDir, 'gateway', hosts);
|
|
118
|
+
caPath = basicSsl.caCertPath(cacheDir);
|
|
119
|
+
}
|
|
120
|
+
const servers = [];
|
|
121
|
+
const boundPorts = [];
|
|
122
|
+
for (const port of ports) {
|
|
123
|
+
const server = pem
|
|
124
|
+
? // eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
125
|
+
require('node:https').createServer({ key: pem, cert: pem }, handler)
|
|
126
|
+
: http.createServer(handler);
|
|
127
|
+
server.on('upgrade', upgrade);
|
|
128
|
+
server.on('error', (err) => console.error(`[gateway] port ${port}: ${err.code}`));
|
|
129
|
+
await new Promise((resolve) => {
|
|
130
|
+
server.listen(port, () => {
|
|
131
|
+
const actual = server.address()?.port ?? port;
|
|
132
|
+
boundPorts.push(actual);
|
|
133
|
+
console.log(`[gateway] Listening on ${pem ? 'https' : 'http'}://localhost:${actual}`);
|
|
134
|
+
resolve();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
servers.push(server);
|
|
138
|
+
}
|
|
139
|
+
console.log('[gateway] Routes:');
|
|
140
|
+
for (const r of routes) {
|
|
141
|
+
const dest = r.pool ? `pods@${r.pool.dir}` : r.target;
|
|
142
|
+
console.log(`[gateway] ${r.host ?? '(default)'} → ${dest}${r.prependPrefix ? ` (+${r.prependPrefix})` : ''}${r.internal ? ' [internal: no middleware]' : ''} [${r.project}]`);
|
|
143
|
+
}
|
|
144
|
+
if (caPath)
|
|
145
|
+
console.log(`[gateway] CA: ${caPath} — one-time setup via the trust executor.`);
|
|
146
|
+
obs.emit('info', `gateway up: ${routes.length} routes on ${boundPorts.join('/')}`, 'cluster-gateway', {
|
|
147
|
+
kind: 'gateway-up',
|
|
148
|
+
ports: boundPorts,
|
|
149
|
+
routes: routes.map((r) => ({ host: r.host ?? '(default)', project: r.project, mode: r.pool ? 'pods' : 'tcp', internal: !!r.internal, prependPrefix: r.prependPrefix })),
|
|
150
|
+
});
|
|
151
|
+
const stop = async () => {
|
|
152
|
+
if (stopped)
|
|
153
|
+
return;
|
|
154
|
+
stopped = true;
|
|
155
|
+
clearInterval(edgeFlusher);
|
|
156
|
+
clearInterval(sweeper);
|
|
157
|
+
obs.emit('info', 'gateway down', 'cluster-gateway', { kind: 'gateway-down' });
|
|
158
|
+
for (const svc of services)
|
|
159
|
+
svcLayer.killSpawned(svc, 'gateway shutdown');
|
|
160
|
+
// Reap the lens-mode managed children (dashboard + MCP singletons + each
|
|
161
|
+
// `lens:true` app's Vite/capture) — same teardown as the pod services.
|
|
162
|
+
lens.reapAll();
|
|
163
|
+
await Promise.all(servers.map((s) => new Promise((r) => s.close(() => r()))));
|
|
164
|
+
proxyLayer.closeProxy();
|
|
165
|
+
proxyLayer.destroyAgents(); // release pooled upstream sockets
|
|
166
|
+
};
|
|
167
|
+
return { ports: boundPorts, routes, services, stop };
|
|
168
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ServiceCtl } from './types';
|
|
2
|
+
/** The gateway's on-disk cache root — shared by service keys AND the TLS cert cache. */
|
|
3
|
+
export declare function gatewayCacheDir(root: string): string;
|
|
4
|
+
/** Load (or generate + persist) a stable key per pod service, plus the inverse
|
|
5
|
+
* key→project map used to authenticate internal callers. Tolerant of a
|
|
6
|
+
* read-only fs (keys then live only for this process). */
|
|
7
|
+
export declare function loadServiceKeys(root: string, services: ServiceCtl[]): {
|
|
8
|
+
serviceKeys: Record<string, string>;
|
|
9
|
+
keyToProject: Map<string, string>;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=service-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-keys.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/service-keys.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;2DAE2D;AAC3D,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,UAAU,EAAE,GACrB;IAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAc5E"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gatewayCacheDir = gatewayCacheDir;
|
|
4
|
+
exports.loadServiceKeys = loadServiceKeys;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
/**
|
|
7
|
+
* Per-service api keys (be-security: the gateway is the trust boundary).
|
|
8
|
+
* Every pod service gets a stable key in `{cacheDir}/service-keys.json`.
|
|
9
|
+
* - Public routes: the gateway injects the TARGET's key as `x-internal-token` —
|
|
10
|
+
* pods are only reachable through it (unix sockets aren't on the network).
|
|
11
|
+
* - `internal.<host>` routes: the CALLER must present its own key as `x-api-key`;
|
|
12
|
+
* the gateway validates it, stamps `x-api-key-id` (caller identity) and
|
|
13
|
+
* forwards with the target's `x-internal-token`.
|
|
14
|
+
*/
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
16
|
+
const fs = tslib_1.__importStar(require("node:fs"));
|
|
17
|
+
const path = tslib_1.__importStar(require("node:path"));
|
|
18
|
+
/** The gateway's on-disk cache root — shared by service keys AND the TLS cert cache. */
|
|
19
|
+
function gatewayCacheDir(root) {
|
|
20
|
+
return path.join(root, 'node_modules', '.cache', 'davnx-webpack');
|
|
21
|
+
}
|
|
22
|
+
/** Load (or generate + persist) a stable key per pod service, plus the inverse
|
|
23
|
+
* key→project map used to authenticate internal callers. Tolerant of a
|
|
24
|
+
* read-only fs (keys then live only for this process). */
|
|
25
|
+
function loadServiceKeys(root, services) {
|
|
26
|
+
const keysDir = gatewayCacheDir(root);
|
|
27
|
+
const keysPath = path.join(keysDir, 'service-keys.json');
|
|
28
|
+
let serviceKeys = {};
|
|
29
|
+
try {
|
|
30
|
+
serviceKeys = JSON.parse(fs.readFileSync(keysPath, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
catch { /* first run */ }
|
|
33
|
+
let dirty = false;
|
|
34
|
+
for (const svc of services) {
|
|
35
|
+
if (!serviceKeys[svc.project]) {
|
|
36
|
+
serviceKeys[svc.project] = (0, node_crypto_1.randomBytes)(16).toString('hex');
|
|
37
|
+
dirty = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (dirty) {
|
|
41
|
+
try {
|
|
42
|
+
fs.mkdirSync(keysDir, { recursive: true });
|
|
43
|
+
fs.writeFileSync(keysPath, JSON.stringify(serviceKeys, null, 2));
|
|
44
|
+
}
|
|
45
|
+
catch { /* ro fs */ }
|
|
46
|
+
}
|
|
47
|
+
const keyToProject = new Map(Object.entries(serviceKeys).map(([p, k]) => [k, p]));
|
|
48
|
+
return { serviceKeys, keyToProject };
|
|
49
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the layered gateway runtime. This file is TYPES-ONLY and
|
|
3
|
+
* imports nothing from a runtime layer (only `node:*`, `../manifest`,
|
|
4
|
+
* `../schema`, `../gateway-errors` types) — so every layer can import it
|
|
5
|
+
* without creating an import cycle. The composition root (`server.ts`) is the
|
|
6
|
+
* only place that wires concrete layers together.
|
|
7
|
+
*/
|
|
8
|
+
import type { ChildProcess } from 'node:child_process';
|
|
9
|
+
import type * as http from 'node:http';
|
|
10
|
+
import type * as stream from 'node:stream';
|
|
11
|
+
import type { GatewayExecutorSchema } from '../schema';
|
|
12
|
+
import type { AuthPolicy, Attrs } from '../manifest';
|
|
13
|
+
import type { GatewayErrorReason } from '../gateway-errors';
|
|
14
|
+
export type { Attrs };
|
|
15
|
+
export interface GatewayContext {
|
|
16
|
+
root: string;
|
|
17
|
+
projectName?: string;
|
|
18
|
+
projectsConfigurations?: {
|
|
19
|
+
projects: Record<string, {
|
|
20
|
+
root: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Per-project declaration: top-level `cluster` field in project.json (`davnx` legacy). */
|
|
25
|
+
export interface ClusterProjectDecl {
|
|
26
|
+
/** Public dev hostname (e.g. `api.tetros.ai.local`). */
|
|
27
|
+
host?: string;
|
|
28
|
+
/** Mount PATH under the host — backend services share api.<domain> path-routed (e.g. `/channels`). */
|
|
29
|
+
path?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Socket-pool mode (the "pods" model): the cluster devserver's service
|
|
32
|
+
* name. The gateway round-robins DIRECTLY over its children's unix
|
|
33
|
+
* sockets (`$TMPDIR/<service>-devserver/child-*.sock`) — one load
|
|
34
|
+
* balancer in front of N pods, like production. Also auto-exposes
|
|
35
|
+
* `internal.<host>` for east-west calls that SKIP the middleware (JWT).
|
|
36
|
+
*/
|
|
37
|
+
service?: string;
|
|
38
|
+
/** TCP mode (vite apps etc.): where the dev server listens. */
|
|
39
|
+
upstream?: string;
|
|
40
|
+
/** Shorthand for upstream. */
|
|
41
|
+
port?: number;
|
|
42
|
+
/** Prepend to forwarded URLs (e.g. `/api` for an app mounted under a prefix). */
|
|
43
|
+
prependPrefix?: string;
|
|
44
|
+
/** This project also serves unmatched hostnames (the catch-all). */
|
|
45
|
+
default?: boolean;
|
|
46
|
+
/** Override the auto `internal.<host>` name, or `false` to disable it. */
|
|
47
|
+
internalHost?: string | false;
|
|
48
|
+
/** Edge auth policy (default + per-path overrides) — generated from controller decorators
|
|
49
|
+
* by @tetros-server/gateway-sync; the gateway verifies + injects identity + enforces it.
|
|
50
|
+
* Absent ⇒ the service is unguarded at the edge (current behavior — opt-in enforcement). */
|
|
51
|
+
auth?: AuthPolicy;
|
|
52
|
+
/** Start at gateway boot (true) or on first request — scale from zero (false, default). */
|
|
53
|
+
eager?: boolean;
|
|
54
|
+
/** Pod range: spawn with min, autoscale up to max while requests overlap. */
|
|
55
|
+
minPods?: number;
|
|
56
|
+
maxPods?: number;
|
|
57
|
+
/** Kill a gateway-spawned (non-eager) service after this many idle seconds. */
|
|
58
|
+
idleKillSec?: number;
|
|
59
|
+
/** Nx target that runs the service. Default serve-hmr. */
|
|
60
|
+
serveTarget?: string;
|
|
61
|
+
/** Devserver admin endpoint for scaling (default from port: http(s)://localhost:<port>). */
|
|
62
|
+
adminUrl?: string;
|
|
63
|
+
/**
|
|
64
|
+
* FRONTEND apps only: run this app UNDER the lens. When true, the gateway
|
|
65
|
+
* SPAWNS the app's own Vite dev server with `@lensmcp/vite-plugin`
|
|
66
|
+
* instrumentation + a browser-capture sidecar (via the shared
|
|
67
|
+
* `spawnLensFrontend` helper) and routes THAT as the upstream — instead of
|
|
68
|
+
* assuming an external Vite already runs on `port`/`upstream`. The
|
|
69
|
+
* cluster-wide singletons (dashboard + MCP) are owned by the gateway, so a
|
|
70
|
+
* `lens:true` app never spawns its own. Absent/false ⇒ today's behavior
|
|
71
|
+
* (plain TCP upstream; bring-your-own Vite). Ignored for pod (`service`) decls.
|
|
72
|
+
*/
|
|
73
|
+
lens?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface SockPool {
|
|
76
|
+
dir: string;
|
|
77
|
+
socks: string[];
|
|
78
|
+
idx: number;
|
|
79
|
+
scannedAt: number;
|
|
80
|
+
}
|
|
81
|
+
export interface Route {
|
|
82
|
+
host?: string;
|
|
83
|
+
prefix?: string;
|
|
84
|
+
project: string;
|
|
85
|
+
prependPrefix?: string;
|
|
86
|
+
target?: string;
|
|
87
|
+
pool?: SockPool;
|
|
88
|
+
svc?: ServiceCtl;
|
|
89
|
+
internal?: boolean;
|
|
90
|
+
auth?: AuthPolicy;
|
|
91
|
+
/** Frontend `lens:true` route: the gateway spawns this app's Vite + lens
|
|
92
|
+
* instrumentation as the upstream (see `LensFrontend`). */
|
|
93
|
+
lens?: LensFrontend;
|
|
94
|
+
}
|
|
95
|
+
/** A frontend app the gateway runs UNDER the lens (`cluster.lens: true`). The
|
|
96
|
+
* gateway spawns its Vite + browser capture as the upstream, deterministically
|
|
97
|
+
* on `port`, and never spawns the per-app dashboard/MCP (those are singletons
|
|
98
|
+
* the gateway owns). */
|
|
99
|
+
export interface LensFrontend {
|
|
100
|
+
project: string;
|
|
101
|
+
/** Absolute project root (holds vite.config.*). */
|
|
102
|
+
projectRoot: string;
|
|
103
|
+
/** The pinned Vite dev port (also the upstream target's port). */
|
|
104
|
+
port: number;
|
|
105
|
+
/** Set once the Vite child has been spawned (idempotency guard). */
|
|
106
|
+
started?: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface ServiceCtl {
|
|
109
|
+
project: string;
|
|
110
|
+
decl: ClusterProjectDecl;
|
|
111
|
+
pool: SockPool;
|
|
112
|
+
proc?: ChildProcess;
|
|
113
|
+
state: 'down' | 'starting' | 'up';
|
|
114
|
+
lastUsed: number;
|
|
115
|
+
inflight: number;
|
|
116
|
+
lastScaleAt: number;
|
|
117
|
+
spawnAt?: number;
|
|
118
|
+
lastErrors?: string[];
|
|
119
|
+
buildFailed?: boolean;
|
|
120
|
+
}
|
|
121
|
+
/** Test-tunable knobs on top of the user-facing schema. */
|
|
122
|
+
export interface GatewayRuntimeOptions extends GatewayExecutorSchema {
|
|
123
|
+
/** Traffic-edge flush period (ms). Default 5000. */
|
|
124
|
+
trafficFlushMs?: number;
|
|
125
|
+
/** Idle sweeper period (ms). Default 10000. */
|
|
126
|
+
sweepMs?: number;
|
|
127
|
+
/** Sock-dir rescan TTL (ms). Default 1500. */
|
|
128
|
+
scanTtlMs?: number;
|
|
129
|
+
/** Cold-start wait budget (ms). Default 120000. */
|
|
130
|
+
startupTimeoutMs?: number;
|
|
131
|
+
/** Customer-supplied lifecycle hooks layered on top of the built-in pipeline. */
|
|
132
|
+
hooks?: GatewayHooks;
|
|
133
|
+
}
|
|
134
|
+
export interface GatewayHandle {
|
|
135
|
+
/** Ports actually bound (resolves `0` to the assigned ephemeral port). */
|
|
136
|
+
ports: number[];
|
|
137
|
+
routes: Route[];
|
|
138
|
+
services: ServiceCtl[];
|
|
139
|
+
stop: () => Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
export interface GwTrace {
|
|
142
|
+
startedAt: number;
|
|
143
|
+
method: string;
|
|
144
|
+
host: string;
|
|
145
|
+
url: string;
|
|
146
|
+
steps: Array<Record<string, unknown>>;
|
|
147
|
+
flowId?: string;
|
|
148
|
+
requestId?: string;
|
|
149
|
+
originNodeId?: string;
|
|
150
|
+
done?: boolean;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* STATE + KNOBS shared across layers — deliberately holds NO layer-produced
|
|
154
|
+
* functions (so `types.ts` stays acyclic). Each layer factory receives this
|
|
155
|
+
* plus the specific sibling layers it needs. Arrays/maps are passed BY
|
|
156
|
+
* REFERENCE and mutated in place (e.g. the dashboard route is pushed onto
|
|
157
|
+
* `routes`, dead pods are filtered out of a pool).
|
|
158
|
+
*/
|
|
159
|
+
export interface GatewayRuntime {
|
|
160
|
+
readonly root: string;
|
|
161
|
+
readonly eventFile: string;
|
|
162
|
+
readonly routes: Route[];
|
|
163
|
+
readonly services: ServiceCtl[];
|
|
164
|
+
readonly serviceKeys: Record<string, string>;
|
|
165
|
+
readonly keyToProject: ReadonlyMap<string, string>;
|
|
166
|
+
readonly scanTtlMs: number;
|
|
167
|
+
readonly startupTimeoutMs: number;
|
|
168
|
+
readonly sweepMs: number;
|
|
169
|
+
readonly trafficFlushMs: number;
|
|
170
|
+
readonly https: boolean;
|
|
171
|
+
/** Teardown flag — a GETTER, never a captured boolean, so every layer reads
|
|
172
|
+
* the LIVE value (an in-flight cold start / auto-heal must not act after stop). */
|
|
173
|
+
stopped(): boolean;
|
|
174
|
+
}
|
|
175
|
+
/** The phases a customer can register handlers into, in pipeline order. */
|
|
176
|
+
export type GatewayPhase = 'onRequest' | 'beforeAuth' | 'afterAuth' | 'beforeForward' | 'onResponse' | 'onError' | 'onUpgrade';
|
|
177
|
+
export type HookHandler = (ctx: HookContext) => void | Promise<void>;
|
|
178
|
+
/** Per-phase handler lists. Handlers run in array order; the first to respond
|
|
179
|
+
* (write the response / destroy the socket) short-circuits the pipeline. */
|
|
180
|
+
export interface GatewayHooks {
|
|
181
|
+
onRequest?: HookHandler[];
|
|
182
|
+
beforeAuth?: HookHandler[];
|
|
183
|
+
afterAuth?: HookHandler[];
|
|
184
|
+
beforeForward?: HookHandler[];
|
|
185
|
+
onResponse?: HookHandler[];
|
|
186
|
+
onError?: HookHandler[];
|
|
187
|
+
onUpgrade?: HookHandler[];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The context passed to every hook — a typed view over the in-flight request.
|
|
191
|
+
* `caller`/`pod`/`claims`/`trace` mirror the `req.__lensmcp*` fields the
|
|
192
|
+
* built-in layers also read, so a hook and the built-ins see the same values.
|
|
193
|
+
*/
|
|
194
|
+
export interface HookContext {
|
|
195
|
+
readonly req: http.IncomingMessage;
|
|
196
|
+
/** Absent on the `onUpgrade` (WebSocket) path. */
|
|
197
|
+
readonly res?: http.ServerResponse;
|
|
198
|
+
/** Present only on the `onUpgrade` path. */
|
|
199
|
+
readonly socket?: stream.Duplex;
|
|
200
|
+
/** The matched route (undefined before `onRequest`'s match). */
|
|
201
|
+
route?: Route;
|
|
202
|
+
/** Verified JWT claims once `afterAuth` has run (public/jwt routes). */
|
|
203
|
+
claims?: Record<string, unknown>;
|
|
204
|
+
/** Validated caller project (internal-route x-api-key). Mirrors `__lensmcpCaller`. */
|
|
205
|
+
caller?: string;
|
|
206
|
+
/** Chosen pod basename once forwarding to a pool. Mirrors `__lensmcpPod`. */
|
|
207
|
+
pod?: string;
|
|
208
|
+
/** The in-flight trace, if this request is being narrated. Mirrors `__lensmcpTrace`. */
|
|
209
|
+
readonly trace?: GwTrace;
|
|
210
|
+
/** The error available in `onError`. */
|
|
211
|
+
error?: Error;
|
|
212
|
+
/** The response status available in `onResponse`. */
|
|
213
|
+
status?: number;
|
|
214
|
+
/** Short-circuit the pipeline with a gateway edge error (the only sanctioned exit). */
|
|
215
|
+
sendError(status: number, reason: GatewayErrorReason, detail?: string): void;
|
|
216
|
+
/** Has a prior handler already responded? (`res` ended/headers-sent, or `socket` destroyed.) */
|
|
217
|
+
responded(): boolean;
|
|
218
|
+
}
|
|
219
|
+
/** Edge-auth verdict returned by the auth layer; the handler turns a denial
|
|
220
|
+
* (`ok:false`) into `finishTrace` + `sendError`. A flat shape (not a
|
|
221
|
+
* discriminated union) because this package compiles with `strict:false`, under
|
|
222
|
+
* which boolean-discriminant narrowing is unreliable. */
|
|
223
|
+
export interface AuthVerdict {
|
|
224
|
+
ok: boolean;
|
|
225
|
+
/** internal route: the validated caller project (when ok). */
|
|
226
|
+
caller?: string;
|
|
227
|
+
/** public/jwt route: verified claims (when ok). */
|
|
228
|
+
claims?: Record<string, unknown>;
|
|
229
|
+
/** denial details (present when !ok). */
|
|
230
|
+
status?: number;
|
|
231
|
+
reason?: GatewayErrorReason;
|
|
232
|
+
detail?: string;
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebSocket upgrade pipeline. WS upgrades enforce the SAME trust model as
|
|
3
|
+
* HTTP — otherwise an `internal.<host>` socket would be reachable with no
|
|
4
|
+
* x-api-key, and a public socket would reach pods without the x-internal-token
|
|
5
|
+
* they expect. Reuses the shared auth + proxy layers so it can't drift from the
|
|
6
|
+
* HTTP handler.
|
|
7
|
+
*/
|
|
8
|
+
import type * as http from 'node:http';
|
|
9
|
+
import type * as stream from 'node:stream';
|
|
10
|
+
import type { GatewayRuntime } from './types';
|
|
11
|
+
import type { EdgeAuth } from './auth';
|
|
12
|
+
import type { ProxyLayer } from './proxy';
|
|
13
|
+
import type { HooksLayer } from './hooks';
|
|
14
|
+
export interface UpgradeDeps {
|
|
15
|
+
rt: GatewayRuntime;
|
|
16
|
+
auth: EdgeAuth;
|
|
17
|
+
proxy: ProxyLayer;
|
|
18
|
+
hooks: HooksLayer;
|
|
19
|
+
}
|
|
20
|
+
export type UpgradeListener = (req: http.IncomingMessage, socket: stream.Duplex, head: Buffer) => void;
|
|
21
|
+
export declare function createUpgrade(deps: UpgradeDeps): UpgradeListener;
|
|
22
|
+
//# sourceMappingURL=upgrade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/upgrade.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,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,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAEvG,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,CAqChE"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUpgrade = createUpgrade;
|
|
4
|
+
const manifest_1 = require("../manifest");
|
|
5
|
+
const discovery_1 = require("./discovery");
|
|
6
|
+
const edge_1 = require("./edge");
|
|
7
|
+
function createUpgrade(deps) {
|
|
8
|
+
const { rt, auth, proxy, hooks } = deps;
|
|
9
|
+
const match = (req) => (0, manifest_1.matchRoute)(rt.routes, (req.headers.host || '').split(':')[0], req.url ?? '/');
|
|
10
|
+
const run = async (req, socket, head) => {
|
|
11
|
+
for (const h of edge_1.GATEWAY_OWNED_HEADERS)
|
|
12
|
+
delete req.headers[h];
|
|
13
|
+
const route = match(req);
|
|
14
|
+
if (!route) {
|
|
15
|
+
socket.destroy();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const ctx = hooks.upgradeContext(req, socket, undefined);
|
|
19
|
+
ctx.route = route;
|
|
20
|
+
if (route.internal) {
|
|
21
|
+
const v = auth.internal(req, route, undefined);
|
|
22
|
+
if (!v.ok) {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
return;
|
|
25
|
+
} // no valid key → no internal WS
|
|
26
|
+
ctx.caller = v.caller;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const v = auth.public(req, route, undefined);
|
|
30
|
+
if (!v.ok) {
|
|
31
|
+
socket.destroy();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
ctx.claims = v.claims;
|
|
35
|
+
// afterAuth (= builtin middleware first, then user hooks) — public branch only
|
|
36
|
+
if (hooks.has('afterAuth')) {
|
|
37
|
+
await hooks.run('afterAuth', ctx);
|
|
38
|
+
if (ctx.responded())
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (route.pool && rt.serviceKeys[route.project]) {
|
|
43
|
+
req.headers['x-internal-token'] = rt.serviceKeys[route.project];
|
|
44
|
+
}
|
|
45
|
+
if (route.prependPrefix && !req.url?.startsWith(route.prependPrefix)) {
|
|
46
|
+
req.url = route.prependPrefix + (req.url ?? '/');
|
|
47
|
+
}
|
|
48
|
+
if (hooks.has('onUpgrade')) {
|
|
49
|
+
await hooks.run('onUpgrade', ctx);
|
|
50
|
+
if (ctx.responded())
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const target = route.pool
|
|
54
|
+
? (() => { const s = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs); return s ? { socketPath: s } : undefined; })()
|
|
55
|
+
: route.target;
|
|
56
|
+
if (!target) {
|
|
57
|
+
socket.destroy();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
proxy.proxy.ws(req, socket, head, { target, changeOrigin: !route.pool, agent: proxy.agentForTarget(target) }, () => socket.destroy());
|
|
61
|
+
};
|
|
62
|
+
return (req, socket, head) => { void run(req, socket, head); };
|
|
63
|
+
}
|