@lensmcp/cluster 1.18.4 → 1.18.7
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/basic-ssl.js +1 -241
- package/build-scope-patterns.js +1 -40
- package/create-webpack-dev.js +1 -186
- package/create-webpack-prod.js +1 -169
- package/executors/build/build.impl.js +1 -98
- package/executors/gateway/gateway-errors.js +1 -43
- package/executors/gateway/gateway.impl.js +1 -53
- package/executors/gateway/gateway.lib.js +1 -29
- package/executors/gateway/health-check.js +1 -66
- package/executors/gateway/jwks-verify.js +1 -121
- package/executors/gateway/main.prod-gateway.js +2 -573
- package/executors/gateway/main.rollout.js +11 -117
- package/executors/gateway/manifest.js +1 -374
- package/executors/gateway/metrics.js +1 -56
- package/executors/gateway/otel-tracing.js +1 -74
- package/executors/gateway/prod-gateway.lib.js +1 -22
- package/executors/gateway/prod-runtime/access-log.js +1 -24
- package/executors/gateway/prod-runtime/app.js +1 -123
- package/executors/gateway/prod-runtime/auth.js +1 -51
- package/executors/gateway/prod-runtime/cors.js +1 -40
- package/executors/gateway/prod-runtime/edge.js +1 -65
- package/executors/gateway/prod-runtime/handler.js +1 -226
- package/executors/gateway/prod-runtime/hooks.js +1 -42
- package/executors/gateway/prod-runtime/observability.js +1 -125
- package/executors/gateway/prod-runtime/rollout.js +1 -103
- package/executors/gateway/prod-runtime/routing.js +1 -40
- package/executors/gateway/prod-runtime/server.js +1 -79
- package/executors/gateway/prod-runtime/trust.js +1 -32
- package/executors/gateway/prod-runtime/types.js +1 -2
- package/executors/gateway/prod-runtime/upgrade.js +4 -116
- package/executors/gateway/prod-runtime/upstream.js +1 -21
- package/executors/gateway/providers-prod.js +1 -232
- package/executors/gateway/rate-limit.js +2 -75
- package/executors/gateway/registry-source.js +1 -131
- package/executors/gateway/rollout-ops.js +2 -167
- package/executors/gateway/runtime/auth.js +1 -64
- package/executors/gateway/runtime/chooser.js +12 -45
- package/executors/gateway/runtime/control.js +1 -128
- package/executors/gateway/runtime/dev-auth.js +1 -108
- package/executors/gateway/runtime/discovery.js +1 -123
- package/executors/gateway/runtime/edge.js +1 -47
- package/executors/gateway/runtime/handler.js +1 -183
- package/executors/gateway/runtime/hooks.js +1 -55
- package/executors/gateway/runtime/lens-children.js +1 -651
- package/executors/gateway/runtime/lifecycle.js +3 -842
- package/executors/gateway/runtime/observability.js +2 -148
- package/executors/gateway/runtime/pod-env.js +2 -89
- package/executors/gateway/runtime/proxy.js +1 -457
- package/executors/gateway/runtime/route-registry.js +1 -72
- package/executors/gateway/runtime/scope.js +1 -117
- package/executors/gateway/runtime/server.js +3 -487
- package/executors/gateway/runtime/service-keys.js +1 -49
- package/executors/gateway/runtime/types.js +1 -151
- package/executors/gateway/runtime/upgrade.js +1 -71
- package/executors/gateway/runtime/workspace-registry.js +1 -99
- package/executors/gateway/ssrf-guard.js +1 -190
- package/executors/serve/serve.impl.js +1 -280
- package/executors/trust/trust.impl.js +4 -162
- package/gateway.js +1 -35
- package/index.js +1 -16
- package/main.devserver.js +10 -1117
- package/package.json +4 -3
- package/tsgo-check-plugin.js +4 -364
- package/typecheck-bus.js +4 -256
|
@@ -1,22 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.startProdGateway = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Public facade for the PRODUCTION gateway runtime (Fastify + @fastify/reply-from).
|
|
6
|
-
*
|
|
7
|
-
* The implementation now lives in `./prod-runtime/*` as composable LAYERS,
|
|
8
|
-
* mirroring the dev gateway's `./runtime/*` — see `prod-runtime/server.ts` for the
|
|
9
|
-
* composition root (`startProdGateway`), `prod-runtime/types.ts` for the shared
|
|
10
|
-
* types + the customer **hooks** API, and the sibling modules for each layer
|
|
11
|
-
* (routing, observability, rollout, upstream, auth, cors, trust, handler, upgrade,
|
|
12
|
-
* app, access-log, hooks). This barrel preserves the original module's public
|
|
13
|
-
* surface so the entry (`main.prod-gateway.ts`), the `@lensmcp/cluster` re-export
|
|
14
|
-
* (`gateway.ts`), the e2e suite, and the pentest/perf suites import from one stable
|
|
15
|
-
* path, and additionally exposes the hooks types for customer-authored layers.
|
|
16
|
-
*
|
|
17
|
-
* The same routing brain as dev (shared `matchRoute` / `buildRouteTable` / manifest
|
|
18
|
-
* schema); the HTTP transport differs on purpose (dev: node:http + http-proxy over
|
|
19
|
-
* unix-socket pods; prod: Fastify + reply-from's undici pool for the high-RPS edge).
|
|
20
|
-
*/
|
|
21
|
-
var server_1 = require("./prod-runtime/server");
|
|
22
|
-
Object.defineProperty(exports, "startProdGateway", { enumerable: true, get: function () { return server_1.startProdGateway; } });
|
|
1
|
+
"use strict";var s=Object.defineProperty;var o=(r,t)=>s(r,"name",{value:t,configurable:!0});var a=Object.defineProperty,e=o((r,t)=>a(r,"name",{value:t,configurable:!0}),"e");Object.defineProperty(exports,"__esModule",{value:!0}),exports.startProdGateway=void 0;var server_1=require("./prod-runtime/server");Object.defineProperty(exports,"startProdGateway",{enumerable:!0,get:e(function(){return server_1.startProdGateway},"get")});
|
|
@@ -1,24 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.logAccess = void 0;
|
|
4
|
-
const logAccess = (request, reply, st) => {
|
|
5
|
-
const req = request.raw;
|
|
6
|
-
const status = reply.statusCode ?? 0;
|
|
7
|
-
const host = (req.headers.host || '').split(':')[0];
|
|
8
|
-
const path = (req.url || '/').split('?')[0];
|
|
9
|
-
const ms = Math.round(reply.elapsedTime);
|
|
10
|
-
try {
|
|
11
|
-
console.log(JSON.stringify({
|
|
12
|
-
severity: status >= 500 ? 'ERROR' : status >= 400 ? 'WARNING' : 'INFO',
|
|
13
|
-
time: new Date().toISOString(),
|
|
14
|
-
message: `${req.method} ${host}${path} ${status} ${ms}ms`,
|
|
15
|
-
method: req.method, host, path, status, ms,
|
|
16
|
-
...(st ? { service: st.route.service, upstream: st.upLabel } : {}),
|
|
17
|
-
...(st?.version ? { version: st.version } : {}),
|
|
18
|
-
...(st?.caller ? { caller: st.caller } : {}),
|
|
19
|
-
...(req.headers['x-request-id'] ? { requestId: String(req.headers['x-request-id']) } : {}),
|
|
20
|
-
}));
|
|
21
|
-
}
|
|
22
|
-
catch { /* never let logging break the response */ }
|
|
23
|
-
};
|
|
24
|
-
exports.logAccess = logAccess;
|
|
1
|
+
"use strict";var u=Object.defineProperty;var d=(r,s)=>u(r,"name",{value:s,configurable:!0});var n=Object.defineProperty,l=d((r,s)=>n(r,"name",{value:s,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.logAccess=void 0;const logAccess=l((r,s,e)=>{const t=r.raw,o=s.statusCode??0,a=(t.headers.host||"").split(":")[0],c=(t.url||"/").split("?")[0],i=Math.round(s.elapsedTime);try{console.log(JSON.stringify({severity:o>=500?"ERROR":o>=400?"WARNING":"INFO",time:new Date().toISOString(),message:`${t.method} ${a}${c} ${o} ${i}ms`,method:t.method,host:a,path:c,status:o,ms:i,...e?{service:e.route.service,upstream:e.upLabel}:{},...e?.version?{version:e.version}:{},...e?.caller?{caller:e.caller}:{},...t.headers["x-request-id"]?{requestId:String(t.headers["x-request-id"])}:{}}))}catch{}},"logAccess");exports.logAccess=logAccess;
|
|
@@ -1,123 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createApp = createApp;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
/**
|
|
6
|
-
* Builds one Fastify app per port (shared route table + providers). Streams
|
|
7
|
-
* every body straight through (never parses/buffers — this is a proxy), wires
|
|
8
|
-
* reply-from's undici pool, the operational probes (/livez /readyz /statusz
|
|
9
|
-
* /metricsz), and the trust / CORS / edge-finish hooks.
|
|
10
|
-
*/
|
|
11
|
-
const node_crypto_1 = require("node:crypto");
|
|
12
|
-
const fastify_1 = tslib_1.__importDefault(require("fastify"));
|
|
13
|
-
const reply_from_1 = tslib_1.__importDefault(require("@fastify/reply-from"));
|
|
14
|
-
const edge_1 = require("./edge");
|
|
15
|
-
const trust_1 = require("./trust");
|
|
16
|
-
const access_log_1 = require("./access-log");
|
|
17
|
-
function createApp(deps) {
|
|
18
|
-
const { rt, obs, cors, hooks, handler, upgrade, reqState, spanByReq, ctxByReq } = deps;
|
|
19
|
-
const opts = rt.opts;
|
|
20
|
-
const buildApp = async () => {
|
|
21
|
-
const keepAlive = opts.server?.keepAliveTimeout ?? 620_000; // > GCP external ALB 600s backend keep-alive
|
|
22
|
-
const app = (0, fastify_1.default)({
|
|
23
|
-
logger: false,
|
|
24
|
-
keepAliveTimeout: keepAlive,
|
|
25
|
-
requestTimeout: opts.server?.requestTimeout ?? 0, // 0 = no inbound request timeout (long-lived streaming)
|
|
26
|
-
...(opts.server?.connectionTimeout !== undefined ? { connectionTimeout: opts.server.connectionTimeout } : {}),
|
|
27
|
-
...(opts.trustProxyIp ? { trustProxy: true } : {}),
|
|
28
|
-
...(opts.tls ? { https: { key: opts.tls.key, cert: opts.tls.cert } } : {}),
|
|
29
|
-
});
|
|
30
|
-
// headersTimeout must stay above keepAliveTimeout, else Node may close an idle
|
|
31
|
-
// keep-alive socket mid-request under load. Default: just above keepAlive.
|
|
32
|
-
app.server.headersTimeout = opts.server?.headersTimeout ?? keepAlive + 10_000;
|
|
33
|
-
// Stream every body straight through — never parse/buffer (this is a proxy).
|
|
34
|
-
app.removeAllContentTypeParsers();
|
|
35
|
-
app.addContentTypeParser('*', (_req, payload, done) => done(null, payload));
|
|
36
|
-
await app.register(reply_from_1.default, {
|
|
37
|
-
disableRequestLogging: true,
|
|
38
|
-
undici: {
|
|
39
|
-
connections: opts.undici?.connections ?? 256,
|
|
40
|
-
pipelining: opts.undici?.pipelining ?? 1,
|
|
41
|
-
keepAliveTimeout: opts.undici?.keepAliveTimeout ?? 60_000,
|
|
42
|
-
bodyTimeout: opts.undici?.bodyTimeout ?? 0, // 0 = unlimited — never cut long-lived SSE / streaming MCP responses
|
|
43
|
-
headersTimeout: opts.undici?.headersTimeout ?? 60_000, // upstream must send response headers within 60s
|
|
44
|
-
...(opts.undici?.allowH2 ? { allowH2: true } : {}), // multiplex many MCP streams over few h2 conns
|
|
45
|
-
connect: {
|
|
46
|
-
rejectUnauthorized: opts.undici?.rejectUnauthorized ?? false, // local-CA/self-signed by default
|
|
47
|
-
...(opts.undici?.cert ? { cert: opts.undici.cert } : {}), // ── mTLS: present a client cert to upstreams ──
|
|
48
|
-
...(opts.undici?.key ? { key: opts.undici.key } : {}),
|
|
49
|
-
...(opts.undici?.ca ? { ca: opts.undici.ca } : {}), // trust a private CA for the server side
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
// Host-agnostic operational probes (answered before routing).
|
|
54
|
-
app.get('/livez', (_req, reply) => { reply.type('text/plain').send('ok'); });
|
|
55
|
-
app.get('/readyz', (_req, reply) => {
|
|
56
|
-
const ready = rt.table.routes.length > 0;
|
|
57
|
-
reply.code(ready ? 200 : 503).type('text/plain').send(ready ? 'ready' : 'no routes loaded');
|
|
58
|
-
});
|
|
59
|
-
// Per-service health rollup (opt-in; token-guarded). Off by default → 404 (don't leak topology).
|
|
60
|
-
app.get('/statusz', (request, reply) => {
|
|
61
|
-
// Fail closed: served ONLY when both enabled AND a token is set. Constant-time token compare.
|
|
62
|
-
if (!opts.statusEndpoint || !opts.statusToken) {
|
|
63
|
-
reply.code(404).type('text/plain').send('not found');
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const tok = request.query?.token ?? (0, edge_1.hdr)(request.raw.headers['x-status-token']) ?? '';
|
|
67
|
-
const a = Buffer.from(tok), b = Buffer.from(opts.statusToken);
|
|
68
|
-
if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b)) {
|
|
69
|
-
reply.code(401).type('text/plain').send('unauthorized');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
reply.type('application/json').send(JSON.stringify({ generatedAt: Date.now(), services: obs.buildStatusSnapshot(), endpoints: opts.healthChecker?.snapshot() ?? [] }));
|
|
73
|
-
});
|
|
74
|
-
// Process self-metrics — same token gate as /statusz.
|
|
75
|
-
app.get('/metricsz', (request, reply) => {
|
|
76
|
-
if (!opts.metrics || !opts.statusToken) {
|
|
77
|
-
reply.code(404).type('text/plain').send('not found');
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const tok = request.query?.token ?? (0, edge_1.hdr)(request.raw.headers['x-status-token']) ?? '';
|
|
81
|
-
const a = Buffer.from(tok), b = Buffer.from(opts.statusToken);
|
|
82
|
-
if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b)) {
|
|
83
|
-
reply.code(401).type('text/plain').send('unauthorized');
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
reply.type('application/json').send(JSON.stringify(opts.metrics.snapshot()));
|
|
87
|
-
});
|
|
88
|
-
app.addHook('onRequest', (request, _reply, done) => { (0, trust_1.stripTrust)(request.raw); done(); });
|
|
89
|
-
// Edge CORS authority: stamp Access-Control-Allow-Origin on EVERY response that lacks it (gateway-generated),
|
|
90
|
-
// but NEVER bless a proxied upstream success cross-origin (see cors.applyResponseCors).
|
|
91
|
-
app.addHook('onSend', (request, reply, payload, done) => { cors.applyResponseCors(request, reply); done(null, payload); });
|
|
92
|
-
app.addHook('onResponse', (request, reply, done) => {
|
|
93
|
-
const st = reqState.get(request);
|
|
94
|
-
if (st) {
|
|
95
|
-
obs.recordEdge(st.route, Date.now() - st.startedAt, reply.statusCode ?? 0, st.caller, st.version);
|
|
96
|
-
obs.finish(st.trace, st.route.service, reply.statusCode ?? 0, { upstream: st.upLabel, ...(st.version ? { version: st.version } : {}), ...(st.caller ? { caller: st.caller } : {}) });
|
|
97
|
-
reqState.delete(request);
|
|
98
|
-
}
|
|
99
|
-
const sp = spanByReq.get(request);
|
|
100
|
-
if (sp) {
|
|
101
|
-
opts.tracing.end(sp, reply.statusCode ?? 0, { ...(st?.upLabel ? { 'upstream.url': st.upLabel } : {}), ...(st?.version ? { 'lensmcp.version': st.version } : {}) });
|
|
102
|
-
spanByReq.delete(request);
|
|
103
|
-
}
|
|
104
|
-
opts.metrics?.recordRequest(reply.statusCode ?? 0);
|
|
105
|
-
if (rt.accessLog)
|
|
106
|
-
(0, access_log_1.logAccess)(request, reply, st);
|
|
107
|
-
// Customer onResponse (terminal phase — observes the finished response).
|
|
108
|
-
const ctx = ctxByReq.get(request);
|
|
109
|
-
if (ctx) {
|
|
110
|
-
ctx.status = reply.statusCode ?? 0;
|
|
111
|
-
if (hooks.has('onResponse'))
|
|
112
|
-
void hooks.run('onResponse', ctx);
|
|
113
|
-
ctxByReq.delete(request);
|
|
114
|
-
}
|
|
115
|
-
done();
|
|
116
|
-
});
|
|
117
|
-
app.all('/', handler);
|
|
118
|
-
app.all('/*', handler);
|
|
119
|
-
app.server.on('upgrade', upgrade);
|
|
120
|
-
return app;
|
|
121
|
-
};
|
|
122
|
-
return { buildApp };
|
|
123
|
-
}
|
|
1
|
+
"use strict";var q=Object.defineProperty;var p=(a,u)=>q(a,"name",{value:u,configurable:!0});var _=Object.defineProperty,l=p((a,u)=>_(a,"name",{value:u,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createApp=createApp;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),fastify_1=tslib_1.__importDefault(require("fastify")),reply_from_1=tslib_1.__importDefault(require("@fastify/reply-from")),edge_1=require("./edge"),trust_1=require("./trust"),access_log_1=require("./access-log");function createApp(a){const{rt:u,obs:c,cors:k,hooks:f,handler:y,upgrade:T,reqState:g,spanByReq:m,ctxByReq:h}=a,e=u.opts;return{buildApp:l(async()=>{const v=e.server?.keepAliveTimeout??62e4,n=(0,fastify_1.default)({logger:!1,keepAliveTimeout:v,requestTimeout:e.server?.requestTimeout??0,...e.server?.connectionTimeout!==void 0?{connectionTimeout:e.server.connectionTimeout}:{},...e.trustProxyIp?{trustProxy:!0}:{},...e.tls?{https:{key:e.tls.key,cert:e.tls.cert}}:{}});return n.server.headersTimeout=e.server?.headersTimeout??v+1e4,n.removeAllContentTypeParsers(),n.addContentTypeParser("*",(s,r,o)=>o(null,r)),await n.register(reply_from_1.default,{disableRequestLogging:!0,undici:{connections:e.undici?.connections??256,pipelining:e.undici?.pipelining??1,keepAliveTimeout:e.undici?.keepAliveTimeout??6e4,bodyTimeout:e.undici?.bodyTimeout??0,headersTimeout:e.undici?.headersTimeout??6e4,...e.undici?.allowH2?{allowH2:!0}:{},connect:{rejectUnauthorized:e.undici?.rejectUnauthorized??!1,...e.undici?.cert?{cert:e.undici.cert}:{},...e.undici?.key?{key:e.undici.key}:{},...e.undici?.ca?{ca:e.undici.ca}:{}}}}),n.get("/livez",(s,r)=>{r.type("text/plain").send("ok")}),n.get("/readyz",(s,r)=>{const o=u.table.routes.length>0;r.code(o?200:503).type("text/plain").send(o?"ready":"no routes loaded")}),n.get("/statusz",(s,r)=>{if(!e.statusEndpoint||!e.statusToken){r.code(404).type("text/plain").send("not found");return}const o=s.query?.token??(0,edge_1.hdr)(s.raw.headers["x-status-token"])??"",t=Buffer.from(o),i=Buffer.from(e.statusToken);if(t.length!==i.length||!(0,node_crypto_1.timingSafeEqual)(t,i)){r.code(401).type("text/plain").send("unauthorized");return}r.type("application/json").send(JSON.stringify({generatedAt:Date.now(),services:c.buildStatusSnapshot(),endpoints:e.healthChecker?.snapshot()??[]}))}),n.get("/metricsz",(s,r)=>{if(!e.metrics||!e.statusToken){r.code(404).type("text/plain").send("not found");return}const o=s.query?.token??(0,edge_1.hdr)(s.raw.headers["x-status-token"])??"",t=Buffer.from(o),i=Buffer.from(e.statusToken);if(t.length!==i.length||!(0,node_crypto_1.timingSafeEqual)(t,i)){r.code(401).type("text/plain").send("unauthorized");return}r.type("application/json").send(JSON.stringify(e.metrics.snapshot()))}),n.addHook("onRequest",(s,r,o)=>{(0,trust_1.stripTrust)(s.raw),o()}),n.addHook("onSend",(s,r,o,t)=>{k.applyResponseCors(s,r),t(null,o)}),n.addHook("onResponse",(s,r,o)=>{const t=g.get(s);t&&(c.recordEdge(t.route,Date.now()-t.startedAt,r.statusCode??0,t.caller,t.version),c.finish(t.trace,t.route.service,r.statusCode??0,{upstream:t.upLabel,...t.version?{version:t.version}:{},...t.caller?{caller:t.caller}:{}}),g.delete(s));const i=m.get(s);i&&(e.tracing.end(i,r.statusCode??0,{...t?.upLabel?{"upstream.url":t.upLabel}:{},...t?.version?{"lensmcp.version":t.version}:{}}),m.delete(s)),e.metrics?.recordRequest(r.statusCode??0),u.accessLog&&(0,access_log_1.logAccess)(s,r,t);const d=h.get(s);d&&(d.status=r.statusCode??0,f.has("onResponse")&&f.run("onResponse",d),h.delete(s)),o()}),n.all("/",y),n.all("/*",y),n.server.on("upgrade",T),n},"buildApp")}}p(createApp,"createApp"),l(createApp,"createApp");
|
|
@@ -1,51 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createEdgeAuth = createEdgeAuth;
|
|
4
|
-
const manifest_1 = require("../manifest");
|
|
5
|
-
const trust_1 = require("./trust");
|
|
6
|
-
function createEdgeAuth(rt, obs, rollout, authenticate) {
|
|
7
|
-
const internal = (req, _route, trace) => {
|
|
8
|
-
const caller = rt.keyToProject.get(String(req.headers['x-api-key'] ?? ''));
|
|
9
|
-
if (!caller) {
|
|
10
|
-
obs.step(trace, 'auth', { mode: 'x-api-key', ok: false });
|
|
11
|
-
return { ok: false, status: 401, reason: 'unauthorized', detail: 'Invalid or missing x-api-key for internal route.' };
|
|
12
|
-
}
|
|
13
|
-
req.headers['x-api-key-id'] = caller;
|
|
14
|
-
delete req.headers['x-api-key'];
|
|
15
|
-
obs.step(trace, 'auth', { mode: 'x-api-key', ok: true, caller });
|
|
16
|
-
return { ok: true, caller };
|
|
17
|
-
};
|
|
18
|
-
const publicAuth = (req, route, cpath, claims, fallbackIp, trace) => {
|
|
19
|
-
const m = rt.table.manifestByService.get(route.service);
|
|
20
|
-
const resolved = m ? (0, manifest_1.authRuleForPath)(m, cpath, req.method ?? 'GET', false) : { mode: route.auth };
|
|
21
|
-
if (resolved.mode === 'jwt') {
|
|
22
|
-
if (rt.opts.identify) {
|
|
23
|
-
if (!claims) {
|
|
24
|
-
obs.step(trace, 'auth', { mode: 'jwt', ok: false });
|
|
25
|
-
return { ok: false, status: 401, reason: 'unauthorized', detail: 'Unauthorized: invalid or missing token' };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
try {
|
|
30
|
-
authenticate('jwt', req);
|
|
31
|
-
}
|
|
32
|
-
catch (e) {
|
|
33
|
-
obs.step(trace, 'auth', { mode: 'jwt', ok: false });
|
|
34
|
-
return { ok: false, status: 401, reason: 'unauthorized', detail: `Unauthorized: ${e.message}` };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
// edge ABAC: a present rule MUST be satisfied by VERIFIED claims. No claims (no verifier) → DENY (fail closed).
|
|
38
|
-
if (resolved.rule && (!claims || !(0, manifest_1.evalRule)(resolved.rule, rollout.buildAttrs(req, fallbackIp, claims, undefined)))) {
|
|
39
|
-
obs.step(trace, 'authz', { ok: false });
|
|
40
|
-
return { ok: false, status: 403, reason: 'forbidden', detail: 'Forbidden: insufficient permission' };
|
|
41
|
-
}
|
|
42
|
-
obs.step(trace, 'auth', { mode: 'jwt', ok: true });
|
|
43
|
-
if (claims)
|
|
44
|
-
(0, trust_1.stampIdentity)(req, claims);
|
|
45
|
-
return { ok: true };
|
|
46
|
-
}
|
|
47
|
-
obs.step(trace, 'auth', { mode: resolved.mode });
|
|
48
|
-
return { ok: true };
|
|
49
|
-
};
|
|
50
|
-
return { internal, public: publicAuth };
|
|
51
|
-
}
|
|
1
|
+
"use strict";var f=Object.defineProperty;var d=(a,e)=>f(a,"name",{value:e,configurable:!0});var k=Object.defineProperty,n=d((a,e)=>k(a,"name",{value:e,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),trust_1=require("./trust");function createEdgeAuth(a,e,h,p){return{internal:n((r,u,i)=>{const t=a.keyToProject.get(String(r.headers["x-api-key"]??""));return t?(r.headers["x-api-key-id"]=t,delete r.headers["x-api-key"],e.step(i,"auth",{mode:"x-api-key",ok:!0,caller:t}),{ok:!0,caller:t}):(e.step(i,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."})},"internal"),public:n((r,u,i,t,c,o)=>{const l=a.table.manifestByService.get(u.service),s=l?(0,manifest_1.authRuleForPath)(l,i,r.method??"GET",!1):{mode:u.auth};if(s.mode==="jwt"){if(a.opts.identify){if(!t)return e.step(o,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"}}else try{p("jwt",r)}catch(m){return e.step(o,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:`Unauthorized: ${m.message}`}}return s.rule&&(!t||!(0,manifest_1.evalRule)(s.rule,h.buildAttrs(r,c,t,void 0)))?(e.step(o,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):(e.step(o,"auth",{mode:"jwt",ok:!0}),t&&(0,trust_1.stampIdentity)(r,t),{ok:!0})}return e.step(o,"auth",{mode:s.mode}),{ok:!0}},"publicAuth")}}d(createEdgeAuth,"createEdgeAuth"),n(createEdgeAuth,"createEdgeAuth");
|
|
@@ -1,40 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createCors = createCors;
|
|
4
|
-
function createCors(rt, reqState) {
|
|
5
|
-
const setCors = (reply, req) => {
|
|
6
|
-
const origin = req.headers.origin;
|
|
7
|
-
// Credentialed allowlist: echo the EXACT origin + allow credentials (cookies). NEVER `*` with
|
|
8
|
-
// credentials (forbidden by the Fetch spec; a cross-site read hole).
|
|
9
|
-
if (origin && rt.corsCredentialOrigins.has(origin.toLowerCase())) {
|
|
10
|
-
reply.header('access-control-allow-origin', origin);
|
|
11
|
-
reply.header('access-control-allow-credentials', 'true');
|
|
12
|
-
reply.header('vary', 'Origin');
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
// Otherwise — UNAUTHENTICATED gateway responses (404 / health / discovery / preflight):
|
|
16
|
-
// reflect the Origin for convenience but DO NOT allow credentials.
|
|
17
|
-
reply.header('access-control-allow-origin', String(origin ?? '*'));
|
|
18
|
-
reply.header('vary', 'Origin');
|
|
19
|
-
};
|
|
20
|
-
const applyResponseCors = (request, reply) => {
|
|
21
|
-
const origin = request.headers.origin;
|
|
22
|
-
if (origin && !reply.getHeader('access-control-allow-origin')) {
|
|
23
|
-
// Credentialed allowlist: EXACT origin + ACAC:true so a credentialed first-party app can read a
|
|
24
|
-
// cookie-setting routed response AND a gateway-generated 401/403/5xx passes the browser CORS check.
|
|
25
|
-
if (rt.corsCredentialOrigins.has(origin.toLowerCase())) {
|
|
26
|
-
reply.header('access-control-allow-origin', origin);
|
|
27
|
-
reply.header('access-control-allow-credentials', 'true');
|
|
28
|
-
reply.header('vary', 'Origin');
|
|
29
|
-
}
|
|
30
|
-
else if (!reqState.get(request)?.proxied) {
|
|
31
|
-
// A non-allowlisted origin is reflected ONLY on a gateway-GENERATED reply (error / no-route),
|
|
32
|
-
// NEVER on a successful PROXIED upstream response — that would be a blanket cross-origin read
|
|
33
|
-
// of app data (CWE-942). The upstream owns CORS on its own responses.
|
|
34
|
-
reply.header('access-control-allow-origin', String(origin));
|
|
35
|
-
reply.header('vary', 'Origin');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
return { setCors, applyResponseCors };
|
|
40
|
-
}
|
|
1
|
+
"use strict";var c=Object.defineProperty;var t=(s,i)=>c(s,"name",{value:i,configurable:!0});var n=Object.defineProperty,a=t((s,i)=>n(s,"name",{value:i,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createCors=createCors;function createCors(s,i){return{setCors:a((r,o)=>{const e=o.headers.origin;if(e&&s.corsCredentialOrigins.has(e.toLowerCase())){r.header("access-control-allow-origin",e),r.header("access-control-allow-credentials","true"),r.header("vary","Origin");return}r.header("access-control-allow-origin",String(e??"*")),r.header("vary","Origin")},"setCors"),applyResponseCors:a((r,o)=>{const e=r.headers.origin;e&&!o.getHeader("access-control-allow-origin")&&(s.corsCredentialOrigins.has(e.toLowerCase())?(o.header("access-control-allow-origin",e),o.header("access-control-allow-credentials","true"),o.header("vary","Origin")):i.get(r)?.proxied||(o.header("access-control-allow-origin",String(e)),o.header("vary","Origin")))},"applyResponseCors")}}t(createCors,"createCors"),a(createCors,"createCors");
|
|
@@ -1,65 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.canonicalPath = exports.ENCODED_STRUCTURE = exports.sendError = exports.hdr = exports.eid = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Edge primitives for the prod gateway: id/header helpers, the localized JSON
|
|
6
|
-
* error envelope, and the path-canonicalization guard. Pure (no runtime state).
|
|
7
|
-
*/
|
|
8
|
-
const node_crypto_1 = require("node:crypto");
|
|
9
|
-
const gateway_errors_1 = require("../gateway-errors");
|
|
10
|
-
const eid = () => Date.now().toString(36) + (0, node_crypto_1.randomBytes)(6).toString('hex');
|
|
11
|
-
exports.eid = eid;
|
|
12
|
-
const hdr = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
|
|
13
|
-
exports.hdr = hdr;
|
|
14
|
-
/**
|
|
15
|
-
* Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
|
|
16
|
-
* `text/plain`), stamping `x-request-id` so the trace id is on the response. The SAME `{ key, status }`
|
|
17
|
-
* localized contract a service returns, so a browser app renders `t(key)` and surfaces the trace id.
|
|
18
|
-
* `detail` is the dev-facing hint (the old plain-text reason), never shown to end users.
|
|
19
|
-
*/
|
|
20
|
-
const sendError = (reply, req, status, reason, detail) => {
|
|
21
|
-
const traceId = (0, gateway_errors_1.ensureRequestId)(req);
|
|
22
|
-
return reply
|
|
23
|
-
.code(status)
|
|
24
|
-
.header('x-request-id', traceId)
|
|
25
|
-
.type(gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE)
|
|
26
|
-
.send((0, gateway_errors_1.gatewayErrorBody)(status, reason, traceId, detail));
|
|
27
|
-
};
|
|
28
|
-
exports.sendError = sendError;
|
|
29
|
-
// Path-canonicalization guard (CWE-288): a reverse proxy and its backend must agree
|
|
30
|
-
// on the request path, or auth gets evaluated against a DIFFERENT path than the one
|
|
31
|
-
// served (AWS API Gateway trailing-slash bypass, IBM API Connect CVE-2025-13915,
|
|
32
|
-
// Kong/Envoy/NGINX normalization mismatch). We REJECT structurally-encoded paths
|
|
33
|
-
// (encoded dot/slash/backslash/null) outright, and return the DECODED path for auth
|
|
34
|
-
// matching so `/%61dmin` is matched as `/admin` — never skipped because it's encoded.
|
|
35
|
-
exports.ENCODED_STRUCTURE = /%2e|%2f|%5c|%00|\\/i; // encoded . / \ NUL, or a raw backslash
|
|
36
|
-
const NEEDS_NORMALIZE = /[%;#\\]/; // percent-encoding, matrix param, fragment, or backslash
|
|
37
|
-
const TRAVERSAL = /(^|\/)\.\.(?=\/|$)/; // a `..` path segment
|
|
38
|
-
/** Canonical path for auth matching, or undefined if the path is unsafe (→ 400 / drop). */
|
|
39
|
-
const canonicalPath = (rawUrl) => {
|
|
40
|
-
const q = rawUrl.indexOf('?');
|
|
41
|
-
const p = q === -1 ? rawUrl : rawUrl.slice(0, q);
|
|
42
|
-
if (p.indexOf('\0') !== -1)
|
|
43
|
-
return undefined; // raw NUL is never a valid path (mirrors the decoded-NUL reject)
|
|
44
|
-
// FAST PATH (the overwhelming majority of production traffic): a clean path — no percent-encoding,
|
|
45
|
-
// matrix params, fragment, or backslash — needs no decoding or normalization (it already equals what
|
|
46
|
-
// the backend resolves). Only the `..` traversal check remains. Saves a decodeURIComponent + two regex
|
|
47
|
-
// + a split/map/join on every request.
|
|
48
|
-
if (!NEEDS_NORMALIZE.test(p))
|
|
49
|
-
return TRAVERSAL.test(p) ? undefined : (p || '/');
|
|
50
|
-
// SLOW PATH: structurally-encoded → reject outright; else decode + strip matrix/fragment so
|
|
51
|
-
// /admin;x=1 and /admin#.css can't skip the /admin policy (CWE-288), and /%61dmin matches /admin.
|
|
52
|
-
if (exports.ENCODED_STRUCTURE.test(p))
|
|
53
|
-
return undefined;
|
|
54
|
-
let decoded;
|
|
55
|
-
try {
|
|
56
|
-
decoded = decodeURIComponent(p);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return undefined;
|
|
60
|
-
} // malformed %xx
|
|
61
|
-
if (decoded.includes('\0') || TRAVERSAL.test(decoded))
|
|
62
|
-
return undefined; // NUL or `..` traversal
|
|
63
|
-
return decoded.split('#')[0].split('/').map((s) => s.split(';')[0]).join('/') || '/';
|
|
64
|
-
};
|
|
65
|
-
exports.canonicalPath = canonicalPath;
|
|
1
|
+
"use strict";var c=Object.defineProperty;var a=(r,t)=>c(r,"name",{value:t,configurable:!0});var i=Object.defineProperty,o=a((r,t)=>i(r,"name",{value:t,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.canonicalPath=exports.ENCODED_STRUCTURE=exports.sendError=exports.hdr=exports.eid=void 0;const node_crypto_1=require("node:crypto"),gateway_errors_1=require("../gateway-errors"),eid=o(()=>Date.now().toString(36)+(0,node_crypto_1.randomBytes)(6).toString("hex"),"eid");exports.eid=eid;const hdr=o(r=>typeof r=="string"&&r!==""?r:Array.isArray(r)?r[0]:void 0,"hdr");exports.hdr=hdr;const sendError=o((r,t,e,n,s)=>{const d=(0,gateway_errors_1.ensureRequestId)(t);return r.code(e).header("x-request-id",d).type(gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE).send((0,gateway_errors_1.gatewayErrorBody)(e,n,d,s))},"sendError");exports.sendError=sendError,exports.ENCODED_STRUCTURE=/%2e|%2f|%5c|%00|\\/i;const NEEDS_NORMALIZE=/[%;#\\]/,TRAVERSAL=/(^|\/)\.\.(?=\/|$)/,canonicalPath=o(r=>{const t=r.indexOf("?"),e=t===-1?r:r.slice(0,t);if(e.indexOf("\0")!==-1)return;if(!NEEDS_NORMALIZE.test(e))return TRAVERSAL.test(e)?void 0:e||"/";if(exports.ENCODED_STRUCTURE.test(e))return;let n;try{n=decodeURIComponent(e)}catch{return}if(!(n.includes("\0")||TRAVERSAL.test(n)))return n.split("#")[0].split("/").map(s=>s.split(";")[0]).join("/")||"/"},"canonicalPath");exports.canonicalPath=canonicalPath;
|
|
@@ -1,226 +1 @@
|
|
|
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 edge_1 = require("./edge");
|
|
7
|
-
function createHandler(deps) {
|
|
8
|
-
const { rt, obs, cors, auth, upstream, rollout, hooks, reqState, spanByReq, ctxByReq } = deps;
|
|
9
|
-
// Resolved ONCE: is there any reason to run the per-request version-selection block at all? A plain
|
|
10
|
-
// route-and-forward gateway (no rollout / cookie / sticky bucketing) skips resolveStickyKey + buildAttrs
|
|
11
|
-
// entirely. `rolloutOn` further gates the (more expensive) ABAC attribute bag to ABAC-targeted services.
|
|
12
|
-
const rolloutOn = !!rt.opts.pods?.resolveVersion; // ABAC version targeting
|
|
13
|
-
const cookieOn = !!rt.opts.cookie; // device-cookie minting
|
|
14
|
-
const stickyOn = !!rt.opts.pods?.bucket && (cookieOn || !!rt.opts.uidHeader); // sticky weighted bucketing
|
|
15
|
-
const versionSelectionOn = rolloutOn || cookieOn || stickyOn;
|
|
16
|
-
const handleNoRoute = (request, reply, host, url) => {
|
|
17
|
-
const req = request.raw;
|
|
18
|
-
cors.setCors(reply, req);
|
|
19
|
-
if (req.method === 'OPTIONS') {
|
|
20
|
-
reply.code(204)
|
|
21
|
-
.header('access-control-allow-methods', 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS')
|
|
22
|
-
.header('access-control-allow-headers', String(req.headers['access-control-request-headers'] ?? '*'))
|
|
23
|
-
.send();
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const mounted = rt.table.routes.filter((r) => r.host && (0, manifest_1.hostMatches)(r.host, host) && r.prefix);
|
|
27
|
-
const p = url.split('?')[0];
|
|
28
|
-
if (mounted.length > 0 && (p === '/health' || p === '/healthz')) {
|
|
29
|
-
reply.code(200).type('application/json').send(JSON.stringify({ status: 'ok', host, services: mounted.map((r) => ({ service: r.service, prefix: r.prefix })) }));
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (p === '/.well-known/lensmcp-routes') {
|
|
33
|
-
reply.code(200).type('application/json').send(JSON.stringify({ host, routes: rt.table.routes.filter((r) => !r.host || (0, manifest_1.hostMatches)(r.host, host)).map((r) => ({ host: r.host ?? '(default)', service: r.service, prefix: r.prefix, internal: !!r.internal, auth: r.auth })) }));
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
(0, edge_1.sendError)(reply, req, 404, 'not_found', mounted.length > 0 ? `No service mounted at ${url} on ${host}.` : 'No gateway route for this hostname.');
|
|
37
|
-
};
|
|
38
|
-
return async (request, reply) => {
|
|
39
|
-
const req = request.raw;
|
|
40
|
-
const host = (req.headers.host || '').split(':')[0];
|
|
41
|
-
const url = req.url ?? '/';
|
|
42
|
-
const trace = obs.beginTrace(req);
|
|
43
|
-
// Mint a trace id for EVERY request: forwarded to the upstream (adopted as its traceId) and returned.
|
|
44
|
-
(0, gateway_errors_1.ensureRequestId)(req);
|
|
45
|
-
obs.step(trace, 'received', { host, method: req.method, url });
|
|
46
|
-
// Hook context: built ONLY when hooks are configured (otherwise undefined → zero overhead).
|
|
47
|
-
const ctx = hooks.any ? hooks.httpContext(req, request, reply, trace) : undefined;
|
|
48
|
-
if (ctx)
|
|
49
|
-
ctxByReq.set(request, ctx);
|
|
50
|
-
if (ctx && hooks.has('onRequest')) {
|
|
51
|
-
await hooks.run('onRequest', ctx);
|
|
52
|
-
if (ctx.responded())
|
|
53
|
-
return reply;
|
|
54
|
-
}
|
|
55
|
-
// Reject HTTP/0.9 — a header-less simple request has no Host, defeating host-keyed routing/policy.
|
|
56
|
-
if (req.httpVersionMajor < 1) {
|
|
57
|
-
obs.finish(trace, 'gateway', 400);
|
|
58
|
-
return (0, edge_1.sendError)(reply, req, 400, 'bad_request', 'Bad Request');
|
|
59
|
-
}
|
|
60
|
-
// Reject path-canonicalization attacks BEFORE routing/auth (proxy↔backend mismatch).
|
|
61
|
-
const cpath = (0, edge_1.canonicalPath)(url);
|
|
62
|
-
if (cpath === undefined) {
|
|
63
|
-
obs.step(trace, 'bad-path', {});
|
|
64
|
-
obs.finish(trace, 'gateway', 400);
|
|
65
|
-
return (0, edge_1.sendError)(reply, req, 400, 'bad_request', 'Bad Request');
|
|
66
|
-
}
|
|
67
|
-
// Rate limit (opt-in) BEFORE routing/auth, per real client IP — defense-in-depth.
|
|
68
|
-
if (rt.opts.rateLimiter) {
|
|
69
|
-
const d = await rt.opts.rateLimiter.check(rollout.clientIpOf(req, req.socket?.remoteAddress ?? request.ip));
|
|
70
|
-
if (!d.allowed) {
|
|
71
|
-
reply.header('retry-after', String(Math.ceil(d.retryAfterMs / 1000)));
|
|
72
|
-
obs.step(trace, 'rate-limited', {});
|
|
73
|
-
obs.finish(trace, 'gateway', 429);
|
|
74
|
-
return (0, edge_1.sendError)(reply, req, 429, 'rate_limited', 'Too Many Requests');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const route = (0, manifest_1.matchRoute)(rt.table.routes, host, url);
|
|
78
|
-
if (!route) {
|
|
79
|
-
handleNoRoute(request, reply, host, url);
|
|
80
|
-
return reply;
|
|
81
|
-
}
|
|
82
|
-
if (ctx)
|
|
83
|
-
ctx.route = route;
|
|
84
|
-
obs.step(trace, 'route-match', { project: route.service, ...(route.prefix ? { prefix: route.prefix } : {}), ...(route.internal ? { internal: true } : {}) });
|
|
85
|
-
// CORS preflight on a ROUTED path: answer it HERE, BEFORE edge auth (preflight carries no token).
|
|
86
|
-
if (req.method === 'OPTIONS') {
|
|
87
|
-
cors.setCors(reply, req);
|
|
88
|
-
reply.code(204)
|
|
89
|
-
.header('access-control-allow-methods', 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS')
|
|
90
|
-
.header('access-control-allow-headers', String(req.headers['access-control-request-headers'] ?? '*'))
|
|
91
|
-
.send();
|
|
92
|
-
return reply;
|
|
93
|
-
}
|
|
94
|
-
// OTel SERVER span (opt-in): continues the incoming traceparent. Ended in onResponse on ANY exit path.
|
|
95
|
-
const span = rt.opts.tracing?.startSpan(req, `gateway ${route.service}`, {
|
|
96
|
-
'http.request.method': (req.method ?? 'GET'),
|
|
97
|
-
'url.path': url.split('?')[0],
|
|
98
|
-
'server.address': host,
|
|
99
|
-
'lensmcp.service': route.service,
|
|
100
|
-
...(route.internal ? { 'lensmcp.internal': true } : {}),
|
|
101
|
-
});
|
|
102
|
-
if (span)
|
|
103
|
-
spanByReq.set(request, span);
|
|
104
|
-
if (ctx && hooks.has('beforeAuth')) {
|
|
105
|
-
await hooks.run('beforeAuth', ctx);
|
|
106
|
-
if (ctx.responded())
|
|
107
|
-
return reply;
|
|
108
|
-
}
|
|
109
|
-
// Verify the end-user JWT ONCE (when a verifier is configured) — reused for edge auth + rollout targeting.
|
|
110
|
-
const claims = route.internal ? undefined : rt.opts.identify?.(req);
|
|
111
|
-
let caller;
|
|
112
|
-
if (route.internal) {
|
|
113
|
-
const v = auth.internal(req, route, trace);
|
|
114
|
-
if (!v.ok) {
|
|
115
|
-
obs.finish(trace, route.service, v.status);
|
|
116
|
-
return (0, edge_1.sendError)(reply, req, v.status, v.reason, v.detail);
|
|
117
|
-
}
|
|
118
|
-
caller = v.caller;
|
|
119
|
-
if (ctx)
|
|
120
|
-
ctx.caller = caller;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
const v = auth.public(req, route, cpath, claims, request.ip, trace);
|
|
124
|
-
if (!v.ok) {
|
|
125
|
-
obs.finish(trace, route.service, v.status);
|
|
126
|
-
return (0, edge_1.sendError)(reply, req, v.status, v.reason, v.detail);
|
|
127
|
-
}
|
|
128
|
-
if (ctx)
|
|
129
|
-
ctx.claims = claims;
|
|
130
|
-
// afterAuth (customer hooks) — public branch ONLY (mirrors dev; internal east-west has no middleware).
|
|
131
|
-
if (ctx && hooks.has('afterAuth')) {
|
|
132
|
-
await hooks.run('afterAuth', ctx);
|
|
133
|
-
if (ctx.responded())
|
|
134
|
-
return reply;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (rt.opts.serviceKeys?.[route.service])
|
|
138
|
-
req.headers['x-internal-token'] = rt.opts.serviceKeys[route.service];
|
|
139
|
-
if (route.prependPrefix) {
|
|
140
|
-
const u = req.url ?? '/';
|
|
141
|
-
const pp = route.prependPrefix;
|
|
142
|
-
if (u !== pp && !u.startsWith(pp + '/'))
|
|
143
|
-
req.url = pp + u;
|
|
144
|
-
} // segment-boundary, not bare startsWith
|
|
145
|
-
// ── version selection: explicit pin → ABAC rule → sticky-key weighted → SWRR ──
|
|
146
|
-
// Skipped entirely for a plain route-and-forward gateway (no rollout/cookie/sticky) — the common
|
|
147
|
-
// hot path avoids resolveStickyKey + buildAttrs. buildAttrs only runs when ABAC targeting is wired.
|
|
148
|
-
const explicit = (0, edge_1.hdr)(req.headers[rt.versionHeader]);
|
|
149
|
-
let pinned = explicit;
|
|
150
|
-
let mintKey; // set → append Set-Cookie on the response
|
|
151
|
-
if (versionSelectionOn && !route.internal && !explicit) {
|
|
152
|
-
const sk = rollout.resolveStickyKey(req);
|
|
153
|
-
if (sk.mint)
|
|
154
|
-
mintKey = sk.key;
|
|
155
|
-
const targeted = rolloutOn ? rt.opts.pods.resolveVersion(route.service, rollout.buildAttrs(req, request.ip, claims, sk.key)) : undefined;
|
|
156
|
-
if (targeted) {
|
|
157
|
-
pinned = targeted.version;
|
|
158
|
-
obs.step(trace, 'targeting', { rule: targeted.rule, version: targeted.version });
|
|
159
|
-
}
|
|
160
|
-
else if (sk.key) {
|
|
161
|
-
const b = rt.opts.pods?.bucket?.(route.service, sk.key);
|
|
162
|
-
if (b) {
|
|
163
|
-
pinned = b.version;
|
|
164
|
-
obs.step(trace, 'targeting', { mode: 'sticky', version: b.version });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const up = await upstream.resolveUpstream(route, trace, pinned);
|
|
169
|
-
if (!up) {
|
|
170
|
-
obs.finish(trace, route.service, 503);
|
|
171
|
-
return (0, edge_1.sendError)(reply, req, 503, 'unavailable', `Service ${route.service} has no available upstream.`);
|
|
172
|
-
}
|
|
173
|
-
if (!('url' in up)) {
|
|
174
|
-
obs.finish(trace, route.service, 502);
|
|
175
|
-
return (0, edge_1.sendError)(reply, req, 502, 'unavailable', `Service ${route.service}: socket upstreams are not supported by the prod gateway.`);
|
|
176
|
-
}
|
|
177
|
-
const originUrl = new URL(up.url).origin;
|
|
178
|
-
const upLabel = up.url;
|
|
179
|
-
const st = { route, caller, startedAt: Date.now(), trace, version: up.version, upLabel };
|
|
180
|
-
reqState.set(request, st);
|
|
181
|
-
if (ctx)
|
|
182
|
-
ctx.version = up.version;
|
|
183
|
-
obs.step(trace, up.version ? 'rollout' : 'upstream', { upstream: upLabel, ...(up.version ? { version: up.version, ...(explicit ? { pinned: true } : {}) } : {}) });
|
|
184
|
-
// Propagate the gateway span's context downstream (W3C traceparent on the forwarded request).
|
|
185
|
-
if (span)
|
|
186
|
-
rt.opts.tracing.inject(span, req.headers);
|
|
187
|
-
if (ctx && hooks.has('beforeForward')) {
|
|
188
|
-
await hooks.run('beforeForward', ctx);
|
|
189
|
-
if (ctx.responded())
|
|
190
|
-
return reply;
|
|
191
|
-
}
|
|
192
|
-
// No source → reply-from forwards the (prefix-rewritten) req.url to `origin`.
|
|
193
|
-
return reply.from(undefined, {
|
|
194
|
-
getUpstream: () => originUrl,
|
|
195
|
-
rewriteHeaders: (headers) => {
|
|
196
|
-
st.proxied = true; // a real upstream response came back → this body is app data, not a gateway reply
|
|
197
|
-
headers['x-lensmcp-upstream'] = upLabel;
|
|
198
|
-
if (!headers['x-request-id'])
|
|
199
|
-
headers['x-request-id'] = (0, gateway_errors_1.ensureRequestId)(req); // trace id on success too
|
|
200
|
-
if (up.version)
|
|
201
|
-
headers['x-lensmcp-version'] = up.version; // which rollout cohort served
|
|
202
|
-
// The gateway owns the edge response: drop the upstream framework leak + brand the hop.
|
|
203
|
-
delete headers['x-powered-by'];
|
|
204
|
-
delete headers['connection'];
|
|
205
|
-
delete headers['keep-alive'];
|
|
206
|
-
delete headers['proxy-connection']; // hop-by-hop
|
|
207
|
-
headers['server'] = 'LensMCP';
|
|
208
|
-
if (mintKey) { // first contact → stamp the device cookie (appended to any upstream Set-Cookie)
|
|
209
|
-
const ex = headers['set-cookie'];
|
|
210
|
-
const list = Array.isArray(ex) ? ex : ex ? [ex] : [];
|
|
211
|
-
headers['set-cookie'] = [...list, rollout.mintCookieHeader(mintKey)];
|
|
212
|
-
}
|
|
213
|
-
return headers;
|
|
214
|
-
},
|
|
215
|
-
onError: (rep, { error }) => {
|
|
216
|
-
rt.opts.pods?.drop?.(route.service, up);
|
|
217
|
-
void error; // don't reflect the raw upstream error (host:port / errno) to the client
|
|
218
|
-
if (ctx && hooks.has('onError')) {
|
|
219
|
-
ctx.error = error;
|
|
220
|
-
void hooks.run('onError', ctx);
|
|
221
|
-
}
|
|
222
|
-
(0, edge_1.sendError)(rep, req, 502, 'unavailable', `Upstream ${route.service} unavailable.`);
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
};
|
|
226
|
-
}
|
|
1
|
+
"use strict";var D=Object.defineProperty;var P=(m,a)=>D(m,"name",{value:a,configurable:!0});var j=Object.defineProperty,v=P((m,a)=>j(m,"name",{value:a,configurable:!0}),"v");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHandler=createHandler;const manifest_1=require("../manifest"),gateway_errors_1=require("../gateway-errors"),edge_1=require("./edge");function createHandler(m){const{rt:a,obs:c,cors:S,auth:O,upstream:_,rollout:w,hooks:l,reqState:N,spanByReq:M,ctxByReq:C}=m,A=!!a.opts.pods?.resolveVersion,H=!!a.opts.cookie,I=!!a.opts.pods?.bucket&&(H||!!a.opts.uidHeader),L=A||H||I,U=v((p,o,r,f)=>{const h=p.raw;if(S.setCors(o,h),h.method==="OPTIONS"){o.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(h.headers["access-control-request-headers"]??"*")).send();return}const i=a.table.routes.filter(n=>n.host&&(0,manifest_1.hostMatches)(n.host,r)&&n.prefix),s=f.split("?")[0];if(i.length>0&&(s==="/health"||s==="/healthz")){o.code(200).type("application/json").send(JSON.stringify({status:"ok",host:r,services:i.map(n=>({service:n.service,prefix:n.prefix}))}));return}if(s==="/.well-known/lensmcp-routes"){o.code(200).type("application/json").send(JSON.stringify({host:r,routes:a.table.routes.filter(n=>!n.host||(0,manifest_1.hostMatches)(n.host,r)).map(n=>({host:n.host??"(default)",service:n.service,prefix:n.prefix,internal:!!n.internal,auth:n.auth}))}));return}(0,edge_1.sendError)(o,h,404,"not_found",i.length>0?`No service mounted at ${f} on ${r}.`:"No gateway route for this hostname.")},"handleNoRoute");return async(p,o)=>{const r=p.raw,f=(r.headers.host||"").split(":")[0],h=r.url??"/",i=c.beginTrace(r);(0,gateway_errors_1.ensureRequestId)(r),c.step(i,"received",{host:f,method:r.method,url:h});const s=l.any?l.httpContext(r,p,o,i):void 0;if(s&&C.set(p,s),s&&l.has("onRequest")&&(await l.run("onRequest",s),s.responded()))return o;if(r.httpVersionMajor<1)return c.finish(i,"gateway",400),(0,edge_1.sendError)(o,r,400,"bad_request","Bad Request");const n=(0,edge_1.canonicalPath)(h);if(n===void 0)return c.step(i,"bad-path",{}),c.finish(i,"gateway",400),(0,edge_1.sendError)(o,r,400,"bad_request","Bad Request");if(a.opts.rateLimiter){const e=await a.opts.rateLimiter.check(w.clientIpOf(r,r.socket?.remoteAddress??p.ip));if(!e.allowed)return o.header("retry-after",String(Math.ceil(e.retryAfterMs/1e3))),c.step(i,"rate-limited",{}),c.finish(i,"gateway",429),(0,edge_1.sendError)(o,r,429,"rate_limited","Too Many Requests")}const t=(0,manifest_1.matchRoute)(a.table.routes,f,h);if(!t)return U(p,o,f,h),o;if(s&&(s.route=t),c.step(i,"route-match",{project:t.service,...t.prefix?{prefix:t.prefix}:{},...t.internal?{internal:!0}:{}}),r.method==="OPTIONS")return S.setCors(o,r),o.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(r.headers["access-control-request-headers"]??"*")).send(),o;const g=a.opts.tracing?.startSpan(r,`gateway ${t.service}`,{"http.request.method":r.method??"GET","url.path":h.split("?")[0],"server.address":f,"lensmcp.service":t.service,...t.internal?{"lensmcp.internal":!0}:{}});if(g&&M.set(p,g),s&&l.has("beforeAuth")&&(await l.run("beforeAuth",s),s.responded()))return o;const b=t.internal?void 0:a.opts.identify?.(r);let E;if(t.internal){const e=O.internal(r,t,i);if(!e.ok)return c.finish(i,t.service,e.status),(0,edge_1.sendError)(o,r,e.status,e.reason,e.detail);E=e.caller,s&&(s.caller=E)}else{const e=O.public(r,t,n,b,p.ip,i);if(!e.ok)return c.finish(i,t.service,e.status),(0,edge_1.sendError)(o,r,e.status,e.reason,e.detail);if(s&&(s.claims=b),s&&l.has("afterAuth")&&(await l.run("afterAuth",s),s.responded()))return o}if(a.opts.serviceKeys?.[t.service]&&(r.headers["x-internal-token"]=a.opts.serviceKeys[t.service]),t.prependPrefix){const e=r.url??"/",d=t.prependPrefix;e!==d&&!e.startsWith(d+"/")&&(r.url=d+e)}const k=(0,edge_1.hdr)(r.headers[a.versionHeader]);let x=k,q;if(L&&!t.internal&&!k){const e=w.resolveStickyKey(r);e.mint&&(q=e.key);const d=A?a.opts.pods.resolveVersion(t.service,w.buildAttrs(r,p.ip,b,e.key)):void 0;if(d)x=d.version,c.step(i,"targeting",{rule:d.rule,version:d.version});else if(e.key){const y=a.opts.pods?.bucket?.(t.service,e.key);y&&(x=y.version,c.step(i,"targeting",{mode:"sticky",version:y.version}))}}const u=await _.resolveUpstream(t,i,x);if(!u)return c.finish(i,t.service,503),(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${t.service} has no available upstream.`);if(!("url"in u))return c.finish(i,t.service,502),(0,edge_1.sendError)(o,r,502,"unavailable",`Service ${t.service}: socket upstreams are not supported by the prod gateway.`);const $=new URL(u.url).origin,T=u.url,R={route:t,caller:E,startedAt:Date.now(),trace:i,version:u.version,upLabel:T};return N.set(p,R),s&&(s.version=u.version),c.step(i,u.version?"rollout":"upstream",{upstream:T,...u.version?{version:u.version,...k?{pinned:!0}:{}}:{}}),g&&a.opts.tracing.inject(g,r.headers),s&&l.has("beforeForward")&&(await l.run("beforeForward",s),s.responded())?o:o.from(void 0,{getUpstream:v(()=>$,"getUpstream"),rewriteHeaders:v(e=>{if(R.proxied=!0,e["x-lensmcp-upstream"]=T,e["x-request-id"]||(e["x-request-id"]=(0,gateway_errors_1.ensureRequestId)(r)),u.version&&(e["x-lensmcp-version"]=u.version),delete e["x-powered-by"],delete e.connection,delete e["keep-alive"],delete e["proxy-connection"],e.server="LensMCP",q){const d=e["set-cookie"],y=Array.isArray(d)?d:d?[d]:[];e["set-cookie"]=[...y,w.mintCookieHeader(q)]}return e},"rewriteHeaders"),onError:v((e,{error:d})=>{a.opts.pods?.drop?.(t.service,u),s&&l.has("onError")&&(s.error=d,l.run("onError",s)),(0,edge_1.sendError)(e,r,502,"unavailable",`Upstream ${t.service} unavailable.`)},"onError")})}}P(createHandler,"createHandler"),v(createHandler,"createHandler");
|
|
@@ -1,42 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createHooks = createHooks;
|
|
4
|
-
const edge_1 = require("./edge");
|
|
5
|
-
const TERMINAL_PHASES = new Set(['onResponse', 'onError']);
|
|
6
|
-
function createHooks(hooks) {
|
|
7
|
-
const run = async (phase, ctx) => {
|
|
8
|
-
const list = hooks[phase];
|
|
9
|
-
if (!list || list.length === 0)
|
|
10
|
-
return;
|
|
11
|
-
const terminal = TERMINAL_PHASES.has(phase);
|
|
12
|
-
for (const h of list) {
|
|
13
|
-
if (!terminal && ctx.responded())
|
|
14
|
-
return;
|
|
15
|
-
await h(ctx);
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
const has = (phase) => (hooks[phase]?.length ?? 0) > 0;
|
|
19
|
-
const any = ['onRequest', 'beforeAuth', 'afterAuth', 'beforeForward', 'onResponse', 'onError', 'onUpgrade'].some((p) => has(p));
|
|
20
|
-
const httpContext = (req, request, reply, trace) => {
|
|
21
|
-
let didRespond = false;
|
|
22
|
-
return {
|
|
23
|
-
req, request, reply, socket: undefined, trace,
|
|
24
|
-
route: undefined, caller: undefined, claims: undefined, version: undefined, error: undefined, status: undefined,
|
|
25
|
-
sendError: (status, reason, detail) => { (0, edge_1.sendError)(reply, req, status, reason, detail); didRespond = true; },
|
|
26
|
-
responded: () => didRespond || reply.raw.writableEnded,
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
const upgradeContext = (req, socket, trace) => {
|
|
30
|
-
let didRespond = false;
|
|
31
|
-
return {
|
|
32
|
-
req, request: undefined, reply: undefined, socket, trace,
|
|
33
|
-
route: undefined, caller: undefined, claims: undefined, version: undefined, error: undefined, status: undefined,
|
|
34
|
-
sendError: () => { try {
|
|
35
|
-
socket.destroy();
|
|
36
|
-
}
|
|
37
|
-
catch { /* already gone */ } didRespond = true; },
|
|
38
|
-
responded: () => didRespond || socket.destroyed,
|
|
39
|
-
};
|
|
40
|
-
};
|
|
41
|
-
return { any, run, has, httpContext, upgradeContext };
|
|
42
|
-
}
|
|
1
|
+
"use strict";var f=Object.defineProperty;var i=(t,d)=>f(t,"name",{value:d,configurable:!0});var l=Object.defineProperty,n=i((t,d)=>l(t,"name",{value:d,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHooks=createHooks;const edge_1=require("./edge"),TERMINAL_PHASES=new Set(["onResponse","onError"]);function createHooks(t){const d=n(async(e,o)=>{const r=t[e];if(!r||r.length===0)return;const s=TERMINAL_PHASES.has(e);for(const a of r){if(!s&&o.responded())return;await a(o)}},"run"),u=n(e=>(t[e]?.length??0)>0,"has");return{any:["onRequest","beforeAuth","afterAuth","beforeForward","onResponse","onError","onUpgrade"].some(e=>u(e)),run:d,has:u,httpContext:n((e,o,r,s)=>{let a=!1;return{req:e,request:o,reply:r,socket:void 0,trace:s,route:void 0,caller:void 0,claims:void 0,version:void 0,error:void 0,status:void 0,sendError:n((c,v,p)=>{(0,edge_1.sendError)(r,e,c,v,p),a=!0},"sendError"),responded:n(()=>a||r.raw.writableEnded,"responded")}},"httpContext"),upgradeContext:n((e,o,r)=>{let s=!1;return{req:e,request:void 0,reply:void 0,socket:o,trace:r,route:void 0,caller:void 0,claims:void 0,version:void 0,error:void 0,status:void 0,sendError:n(()=>{try{o.destroy()}catch{}s=!0},"sendError"),responded:n(()=>s||o.destroyed,"responded")}},"upgradeContext")}}i(createHooks,"createHooks"),n(createHooks,"createHooks");
|