@lensmcp/cluster 1.18.3 → 1.18.6
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.d.ts +0 -1
- package/basic-ssl.js +1 -241
- package/build-scope-patterns.d.ts +0 -1
- package/build-scope-patterns.js +1 -40
- package/create-webpack-dev.d.ts +0 -1
- package/create-webpack-dev.js +1 -186
- package/create-webpack-prod.d.ts +0 -1
- package/create-webpack-prod.js +1 -169
- package/executors/build/build.impl.d.ts +0 -1
- package/executors/build/build.impl.js +1 -98
- package/executors/gateway/gateway-errors.d.ts +0 -1
- package/executors/gateway/gateway-errors.js +1 -43
- package/executors/gateway/gateway.impl.d.ts +0 -1
- package/executors/gateway/gateway.impl.js +1 -53
- package/executors/gateway/gateway.lib.d.ts +0 -1
- package/executors/gateway/gateway.lib.js +1 -29
- package/executors/gateway/health-check.d.ts +0 -1
- package/executors/gateway/health-check.js +1 -66
- package/executors/gateway/jwks-verify.d.ts +0 -1
- package/executors/gateway/jwks-verify.js +1 -121
- package/executors/gateway/main.prod-gateway.d.ts +0 -1
- package/executors/gateway/main.prod-gateway.js +2 -573
- package/executors/gateway/main.rollout.d.ts +0 -1
- package/executors/gateway/main.rollout.js +11 -117
- package/executors/gateway/manifest.d.ts +0 -1
- package/executors/gateway/manifest.js +1 -374
- package/executors/gateway/metrics.d.ts +0 -1
- package/executors/gateway/metrics.js +1 -56
- package/executors/gateway/otel-tracing.d.ts +0 -1
- package/executors/gateway/otel-tracing.js +1 -74
- package/executors/gateway/prod-gateway.lib.d.ts +0 -1
- package/executors/gateway/prod-gateway.lib.js +1 -22
- package/executors/gateway/prod-runtime/access-log.d.ts +0 -1
- package/executors/gateway/prod-runtime/access-log.js +1 -24
- package/executors/gateway/prod-runtime/app.d.ts +0 -1
- package/executors/gateway/prod-runtime/app.js +1 -123
- package/executors/gateway/prod-runtime/auth.d.ts +0 -1
- package/executors/gateway/prod-runtime/auth.js +1 -51
- package/executors/gateway/prod-runtime/cors.d.ts +0 -1
- package/executors/gateway/prod-runtime/cors.js +1 -40
- package/executors/gateway/prod-runtime/edge.d.ts +0 -1
- package/executors/gateway/prod-runtime/edge.js +1 -65
- package/executors/gateway/prod-runtime/handler.d.ts +0 -1
- package/executors/gateway/prod-runtime/handler.js +1 -226
- package/executors/gateway/prod-runtime/hooks.d.ts +0 -1
- package/executors/gateway/prod-runtime/hooks.js +1 -42
- package/executors/gateway/prod-runtime/observability.d.ts +0 -1
- package/executors/gateway/prod-runtime/observability.js +1 -125
- package/executors/gateway/prod-runtime/rollout.d.ts +0 -1
- package/executors/gateway/prod-runtime/rollout.js +1 -103
- package/executors/gateway/prod-runtime/routing.d.ts +0 -1
- package/executors/gateway/prod-runtime/routing.js +1 -40
- package/executors/gateway/prod-runtime/server.d.ts +0 -1
- package/executors/gateway/prod-runtime/server.js +1 -79
- package/executors/gateway/prod-runtime/trust.d.ts +0 -1
- package/executors/gateway/prod-runtime/trust.js +1 -32
- package/executors/gateway/prod-runtime/types.d.ts +0 -1
- package/executors/gateway/prod-runtime/types.js +1 -2
- package/executors/gateway/prod-runtime/upgrade.d.ts +0 -1
- package/executors/gateway/prod-runtime/upgrade.js +4 -116
- package/executors/gateway/prod-runtime/upstream.d.ts +0 -1
- package/executors/gateway/prod-runtime/upstream.js +1 -21
- package/executors/gateway/providers-prod.d.ts +0 -1
- package/executors/gateway/providers-prod.js +1 -232
- package/executors/gateway/rate-limit.d.ts +0 -1
- package/executors/gateway/rate-limit.js +2 -75
- package/executors/gateway/registry-source.d.ts +0 -1
- package/executors/gateway/registry-source.js +1 -131
- package/executors/gateway/rollout-ops.d.ts +0 -1
- package/executors/gateway/rollout-ops.js +2 -167
- package/executors/gateway/runtime/auth.d.ts +0 -1
- package/executors/gateway/runtime/auth.js +1 -64
- package/executors/gateway/runtime/chooser.d.ts +0 -1
- package/executors/gateway/runtime/chooser.js +12 -45
- package/executors/gateway/runtime/control.d.ts +0 -1
- package/executors/gateway/runtime/control.js +1 -128
- package/executors/gateway/runtime/dev-auth.d.ts +0 -1
- package/executors/gateway/runtime/dev-auth.js +1 -108
- package/executors/gateway/runtime/discovery.d.ts +0 -1
- package/executors/gateway/runtime/discovery.js +1 -123
- package/executors/gateway/runtime/edge.d.ts +0 -1
- package/executors/gateway/runtime/edge.js +1 -47
- package/executors/gateway/runtime/handler.d.ts +0 -1
- package/executors/gateway/runtime/handler.js +1 -183
- package/executors/gateway/runtime/hooks.d.ts +0 -1
- package/executors/gateway/runtime/hooks.js +1 -55
- package/executors/gateway/runtime/lens-children.d.ts +0 -1
- package/executors/gateway/runtime/lens-children.js +1 -651
- package/executors/gateway/runtime/lifecycle.d.ts +0 -1
- package/executors/gateway/runtime/lifecycle.js +3 -842
- package/executors/gateway/runtime/observability.d.ts +0 -1
- package/executors/gateway/runtime/observability.js +2 -148
- package/executors/gateway/runtime/pod-env.d.ts +0 -1
- package/executors/gateway/runtime/pod-env.js +2 -89
- package/executors/gateway/runtime/proxy.d.ts +0 -1
- package/executors/gateway/runtime/proxy.js +1 -457
- package/executors/gateway/runtime/route-registry.d.ts +0 -1
- package/executors/gateway/runtime/route-registry.js +1 -72
- package/executors/gateway/runtime/scope.d.ts +0 -1
- package/executors/gateway/runtime/scope.js +1 -117
- package/executors/gateway/runtime/server.d.ts +0 -1
- package/executors/gateway/runtime/server.js +3 -487
- package/executors/gateway/runtime/service-keys.d.ts +0 -1
- package/executors/gateway/runtime/service-keys.js +1 -49
- package/executors/gateway/runtime/types.d.ts +0 -1
- package/executors/gateway/runtime/types.js +1 -151
- package/executors/gateway/runtime/upgrade.d.ts +0 -1
- package/executors/gateway/runtime/upgrade.js +1 -71
- package/executors/gateway/runtime/workspace-registry.d.ts +0 -1
- package/executors/gateway/runtime/workspace-registry.js +1 -99
- package/executors/gateway/ssrf-guard.d.ts +0 -1
- package/executors/gateway/ssrf-guard.js +1 -190
- package/executors/serve/serve.impl.d.ts +0 -1
- package/executors/serve/serve.impl.js +1 -280
- package/executors/trust/trust.impl.d.ts +0 -1
- package/executors/trust/trust.impl.js +4 -162
- package/gateway.d.ts +0 -1
- package/gateway.js +1 -35
- package/index.d.ts +0 -1
- package/index.js +1 -16
- package/main.devserver.d.ts +0 -1
- package/main.devserver.js +10 -1117
- package/package.json +4 -3
- package/tsgo-check-plugin.d.ts +0 -1
- package/tsgo-check-plugin.js +4 -364
- package/typecheck-bus.d.ts +0 -1
- package/typecheck-bus.js +4 -256
- package/basic-ssl.d.ts.map +0 -1
- package/build-scope-patterns.d.ts.map +0 -1
- package/create-webpack-dev.d.ts.map +0 -1
- package/create-webpack-prod.d.ts.map +0 -1
- package/executors/build/build.impl.d.ts.map +0 -1
- package/executors/gateway/gateway-errors.d.ts.map +0 -1
- package/executors/gateway/gateway.impl.d.ts.map +0 -1
- package/executors/gateway/gateway.lib.d.ts.map +0 -1
- package/executors/gateway/health-check.d.ts.map +0 -1
- package/executors/gateway/jwks-verify.d.ts.map +0 -1
- package/executors/gateway/main.prod-gateway.d.ts.map +0 -1
- package/executors/gateway/main.rollout.d.ts.map +0 -1
- package/executors/gateway/manifest.d.ts.map +0 -1
- package/executors/gateway/metrics.d.ts.map +0 -1
- package/executors/gateway/otel-tracing.d.ts.map +0 -1
- package/executors/gateway/prod-gateway.lib.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/access-log.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/app.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/auth.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/cors.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/edge.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/handler.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/hooks.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/observability.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/rollout.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/routing.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/server.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/trust.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/types.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/upgrade.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/upstream.d.ts.map +0 -1
- package/executors/gateway/providers-prod.d.ts.map +0 -1
- package/executors/gateway/rate-limit.d.ts.map +0 -1
- package/executors/gateway/registry-source.d.ts.map +0 -1
- package/executors/gateway/rollout-ops.d.ts.map +0 -1
- package/executors/gateway/runtime/auth.d.ts.map +0 -1
- package/executors/gateway/runtime/chooser.d.ts.map +0 -1
- package/executors/gateway/runtime/control.d.ts.map +0 -1
- package/executors/gateway/runtime/dev-auth.d.ts.map +0 -1
- package/executors/gateway/runtime/discovery.d.ts.map +0 -1
- package/executors/gateway/runtime/edge.d.ts.map +0 -1
- package/executors/gateway/runtime/handler.d.ts.map +0 -1
- package/executors/gateway/runtime/hooks.d.ts.map +0 -1
- package/executors/gateway/runtime/lens-children.d.ts.map +0 -1
- package/executors/gateway/runtime/lifecycle.d.ts.map +0 -1
- package/executors/gateway/runtime/observability.d.ts.map +0 -1
- package/executors/gateway/runtime/pod-env.d.ts.map +0 -1
- package/executors/gateway/runtime/proxy.d.ts.map +0 -1
- package/executors/gateway/runtime/route-registry.d.ts.map +0 -1
- package/executors/gateway/runtime/scope.d.ts.map +0 -1
- package/executors/gateway/runtime/server.d.ts.map +0 -1
- package/executors/gateway/runtime/service-keys.d.ts.map +0 -1
- package/executors/gateway/runtime/types.d.ts.map +0 -1
- package/executors/gateway/runtime/upgrade.d.ts.map +0 -1
- package/executors/gateway/runtime/workspace-registry.d.ts.map +0 -1
- package/executors/gateway/ssrf-guard.d.ts.map +0 -1
- package/executors/serve/serve.impl.d.ts.map +0 -1
- package/executors/trust/trust.impl.d.ts.map +0 -1
- package/gateway.d.ts.map +0 -1
- package/index.d.ts.map +0 -1
- package/main.devserver.d.ts.map +0 -1
- package/tsgo-check-plugin.d.ts.map +0 -1
- package/typecheck-bus.d.ts.map +0 -1
|
@@ -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");
|
|
@@ -1,125 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createObservability = createObservability;
|
|
4
|
-
const edge_1 = require("./edge");
|
|
5
|
-
function createObservability(opts, getRoutes) {
|
|
6
|
-
const emit = (severity, title, fingerprint, raw, ctx) => {
|
|
7
|
-
if (!opts.emit)
|
|
8
|
-
return; // lens off by default — no work, no allocation beyond the call
|
|
9
|
-
try {
|
|
10
|
-
opts.emit({
|
|
11
|
-
id: (0, edge_1.eid)(), sessionId: 'pending', timestamp: Date.now(),
|
|
12
|
-
source: 'gateway', category: 'cluster', severity,
|
|
13
|
-
context: { sessionId: 'pending', ...ctx }, fingerprint, title, raw,
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
catch { /* never let the bus break traffic */ }
|
|
17
|
-
};
|
|
18
|
-
// ---- traffic edges (host→service[@version]), flushed to the lens periodically ----
|
|
19
|
-
const edges = new Map();
|
|
20
|
-
const recordEdge = (route, ms, status, caller, version) => {
|
|
21
|
-
const host = route.host ?? '(default)';
|
|
22
|
-
const key = (caller ? caller + '⇒' : '') + host + '→' + route.service + (version ? '@' + version : '');
|
|
23
|
-
const e = edges.get(key) ?? { host, service: route.service, caller, version, count: 0, totalMs: 0, errors: 0 };
|
|
24
|
-
e.count += 1;
|
|
25
|
-
e.totalMs += ms;
|
|
26
|
-
if (status >= 500 || status === 0)
|
|
27
|
-
e.errors += 1;
|
|
28
|
-
edges.set(key, e);
|
|
29
|
-
};
|
|
30
|
-
// ---- per-service health rollup (feeds /statusz + the Redis status fan-out) ----
|
|
31
|
-
// A ring of the last N flush windows, summed at read time → a rolling view that
|
|
32
|
-
// doesn't flap to "idle" the instant traffic pauses. Derived purely from the edges.
|
|
33
|
-
const statusWindowCount = Math.max(1, opts.statusWindows ?? 12);
|
|
34
|
-
const statusWindows = [];
|
|
35
|
-
const lastSeenAt = new Map();
|
|
36
|
-
const routeServices = () => [...new Set(getRoutes().map((r) => r.service))];
|
|
37
|
-
const pushStatusWindow = () => {
|
|
38
|
-
const w = new Map();
|
|
39
|
-
for (const e of edges.values()) {
|
|
40
|
-
const s = w.get(e.service) ?? { requests: 0, errors: 0, totalMs: 0, versions: new Set() };
|
|
41
|
-
s.requests += e.count;
|
|
42
|
-
s.errors += e.errors;
|
|
43
|
-
s.totalMs += e.totalMs;
|
|
44
|
-
if (e.version)
|
|
45
|
-
s.versions.add(e.version);
|
|
46
|
-
w.set(e.service, s);
|
|
47
|
-
}
|
|
48
|
-
const now = Date.now();
|
|
49
|
-
for (const [svc, s] of w)
|
|
50
|
-
if (s.requests > 0)
|
|
51
|
-
lastSeenAt.set(svc, now);
|
|
52
|
-
statusWindows.push(w);
|
|
53
|
-
while (statusWindows.length > statusWindowCount)
|
|
54
|
-
statusWindows.shift();
|
|
55
|
-
};
|
|
56
|
-
const buildStatusSnapshot = () => {
|
|
57
|
-
const agg = new Map();
|
|
58
|
-
for (const w of statusWindows)
|
|
59
|
-
for (const [svc, s] of w) {
|
|
60
|
-
const a = agg.get(svc) ?? { requests: 0, errors: 0, totalMs: 0, versions: new Set() };
|
|
61
|
-
a.requests += s.requests;
|
|
62
|
-
a.errors += s.errors;
|
|
63
|
-
a.totalMs += s.totalMs;
|
|
64
|
-
for (const v of s.versions)
|
|
65
|
-
a.versions.add(v);
|
|
66
|
-
agg.set(svc, a);
|
|
67
|
-
}
|
|
68
|
-
const routes = getRoutes();
|
|
69
|
-
return routeServices().map((svc) => {
|
|
70
|
-
const a = agg.get(svc) ?? { requests: 0, errors: 0, totalMs: 0, versions: new Set() };
|
|
71
|
-
const hosts = [...new Set(routes.filter((r) => r.service === svc).map((r) => r.host ?? '(default)'))];
|
|
72
|
-
const er = a.requests ? a.errors / a.requests : 0;
|
|
73
|
-
let status = a.requests === 0 ? 'idle' : er >= 0.5 ? 'down' : er >= 0.05 ? 'degraded' : 'healthy';
|
|
74
|
-
// active liveness: if every known upstream for this service is failing probes, it's down (even with no traffic)
|
|
75
|
-
if (opts.healthChecker) {
|
|
76
|
-
const svcUrls = routes.filter((r) => r.service === svc && r.upstream).map((r) => r.upstream);
|
|
77
|
-
if (svcUrls.length && svcUrls.every((u) => !opts.healthChecker.isHealthy(u)))
|
|
78
|
-
status = 'down';
|
|
79
|
-
}
|
|
80
|
-
return { service: svc, hosts, status, requests: a.requests, errorRate: +er.toFixed(4), avgMs: a.requests ? Math.round(a.totalMs / a.requests) : 0, versions: [...a.versions], lastSeen: lastSeenAt.get(svc) ?? null };
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
const startFlusher = () => {
|
|
84
|
-
const t = setInterval(() => {
|
|
85
|
-
pushStatusWindow(); // always: roll the health window (independent of the lens)
|
|
86
|
-
if (opts.onStatus) {
|
|
87
|
-
try {
|
|
88
|
-
opts.onStatus(buildStatusSnapshot());
|
|
89
|
-
}
|
|
90
|
-
catch { /* never let a status sink break traffic */ }
|
|
91
|
-
}
|
|
92
|
-
if (opts.emit) {
|
|
93
|
-
for (const e of edges.values()) {
|
|
94
|
-
emit(e.errors ? 'warning' : 'info', `traffic ${e.host} → ${e.service}${e.version ? '@' + e.version : ''}: ${e.count} req`, `gateway-traffic:${e.host}->${e.service}${e.version ? '@' + e.version : ''}`, { kind: 'gateway-traffic', host: e.host, project: e.service, caller: e.caller, version: e.version, count: e.count, avgMs: Math.round(e.totalMs / e.count), errors: e.errors });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
edges.clear();
|
|
98
|
-
}, opts.trafficFlushMs ?? 5000);
|
|
99
|
-
t.unref?.();
|
|
100
|
-
return t;
|
|
101
|
-
};
|
|
102
|
-
// ---- per-request flow trace (only for flow-header-carrying requests) ----
|
|
103
|
-
const beginTrace = (req) => {
|
|
104
|
-
if (!opts.emit)
|
|
105
|
-
return undefined; // no lens → no trace overhead
|
|
106
|
-
const flowId = (0, edge_1.hdr)(req.headers['x-lensmcp-flow-id']);
|
|
107
|
-
let requestId = (0, edge_1.hdr)(req.headers['x-request-id']);
|
|
108
|
-
if (!flowId && !requestId && !(0, edge_1.hdr)(req.headers['traceparent']))
|
|
109
|
-
return undefined;
|
|
110
|
-
if (!requestId) {
|
|
111
|
-
requestId = (0, edge_1.eid)();
|
|
112
|
-
req.headers['x-request-id'] = requestId;
|
|
113
|
-
}
|
|
114
|
-
return { startedAt: Date.now(), method: (req.method ?? 'GET').toUpperCase(), host: (req.headers.host || '').split(':')[0], url: req.url ?? '/', steps: [], ...(flowId ? { flowId } : {}), ...(requestId ? { requestId } : {}) };
|
|
115
|
-
};
|
|
116
|
-
const step = (t, name, info) => { if (t)
|
|
117
|
-
t.steps.push({ step: name, atMs: Date.now() - t.startedAt, ...info }); };
|
|
118
|
-
const finish = (t, service, status, extra) => {
|
|
119
|
-
if (!t || t.done)
|
|
120
|
-
return;
|
|
121
|
-
t.done = true;
|
|
122
|
-
emit(status >= 500 ? 'error' : 'info', `gateway ${t.method} ${t.host}${t.url} → ${status} (${Date.now() - t.startedAt}ms)`, `gateway-request:${service}`, { kind: 'gateway-request', project: service, host: t.host, method: t.method, url: t.url, status, durationMs: Date.now() - t.startedAt, startedAt: t.startedAt, steps: t.steps, ...extra }, { ...(t.flowId ? { flowId: t.flowId } : {}), ...(t.requestId ? { requestId: t.requestId } : {}) });
|
|
123
|
-
};
|
|
124
|
-
return { emit, beginTrace, step, finish, recordEdge, buildStatusSnapshot, startFlusher };
|
|
125
|
-
}
|
|
1
|
+
"use strict";var b=Object.defineProperty;var p=(o,d)=>b(o,"name",{value:d,configurable:!0});var y=Object.defineProperty,c=p((o,d)=>y(o,"name",{value:d,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createObservability=createObservability;const redact_1=require("@lensmcp/core/redact"),edge_1=require("./edge");function createObservability(o,d){const v=c((e,t,s,r,a)=>{if(o.emit)try{o.emit({id:(0,edge_1.eid)(),sessionId:"pending",timestamp:Date.now(),source:"gateway",category:"cluster",severity:e,context:(0,redact_1.redactDeep)({sessionId:"pending",...a}),fingerprint:s,title:(0,redact_1.safeTitle)(t),raw:r&&(0,redact_1.redactDeep)(r)})}catch{}},"emit"),h=new Map,q=c((e,t,s,r,a)=>{const n=e.host??"(default)",u=(r?r+"\u21D2":"")+n+"\u2192"+e.service+(a?"@"+a:""),i=h.get(u)??{host:n,service:e.service,caller:r,version:a,count:0,totalMs:0,errors:0};i.count+=1,i.totalMs+=t,(s>=500||s===0)&&(i.errors+=1),h.set(u,i)},"recordEdge"),m=Math.max(1,o.statusWindows??12),f=[],w=new Map,M=c(()=>[...new Set(d().map(e=>e.service))],"routeServices"),S=c(()=>{const e=new Map;for(const s of h.values()){const r=e.get(s.service)??{requests:0,errors:0,totalMs:0,versions:new Set};r.requests+=s.count,r.errors+=s.errors,r.totalMs+=s.totalMs,s.version&&r.versions.add(s.version),e.set(s.service,r)}const t=Date.now();for(const[s,r]of e)r.requests>0&&w.set(s,t);for(f.push(e);f.length>m;)f.shift()},"pushStatusWindow"),g=c(()=>{const e=new Map;for(const s of f)for(const[r,a]of s){const n=e.get(r)??{requests:0,errors:0,totalMs:0,versions:new Set};n.requests+=a.requests,n.errors+=a.errors,n.totalMs+=a.totalMs;for(const u of a.versions)n.versions.add(u);e.set(r,n)}const t=d();return M().map(s=>{const r=e.get(s)??{requests:0,errors:0,totalMs:0,versions:new Set},a=[...new Set(t.filter(i=>i.service===s).map(i=>i.host??"(default)"))],n=r.requests?r.errors/r.requests:0;let u=r.requests===0?"idle":n>=.5?"down":n>=.05?"degraded":"healthy";if(o.healthChecker){const i=t.filter(l=>l.service===s&&l.upstream).map(l=>l.upstream);i.length&&i.every(l=>!o.healthChecker.isHealthy(l))&&(u="down")}return{service:s,hosts:a,status:u,requests:r.requests,errorRate:+n.toFixed(4),avgMs:r.requests?Math.round(r.totalMs/r.requests):0,versions:[...r.versions],lastSeen:w.get(s)??null}})},"buildStatusSnapshot");return{emit:v,beginTrace:c(e=>{if(!o.emit)return;const t=(0,edge_1.hdr)(e.headers["x-lensmcp-flow-id"]);let s=(0,edge_1.hdr)(e.headers["x-request-id"]);if(!(!t&&!s&&!(0,edge_1.hdr)(e.headers.traceparent)))return s||(s=(0,edge_1.eid)(),e.headers["x-request-id"]=s),{startedAt:Date.now(),method:(e.method??"GET").toUpperCase(),host:(e.headers.host||"").split(":")[0],url:e.url??"/",steps:[],...t?{flowId:t}:{},...s?{requestId:s}:{}}},"beginTrace"),step:c((e,t,s)=>{e&&e.steps.push({step:t,atMs:Date.now()-e.startedAt,...s})},"step"),finish:c((e,t,s,r)=>{!e||e.done||(e.done=!0,v(s>=500?"error":"info",`gateway ${e.method} ${e.host}${e.url} \u2192 ${s} (${Date.now()-e.startedAt}ms)`,`gateway-request:${t}`,{kind:"gateway-request",project:t,host:e.host,method:e.method,url:e.url,status:s,durationMs:Date.now()-e.startedAt,startedAt:e.startedAt,steps:e.steps,...r},{...e.flowId?{flowId:e.flowId}:{},...e.requestId?{requestId:e.requestId}:{}}))},"finish"),recordEdge:q,buildStatusSnapshot:g,startFlusher:c(()=>{const e=setInterval(()=>{if(S(),o.onStatus)try{o.onStatus(g())}catch{}if(o.emit)for(const t of h.values())v(t.errors?"warning":"info",`traffic ${t.host} \u2192 ${t.service}${t.version?"@"+t.version:""}: ${t.count} req`,`gateway-traffic:${t.host}->${t.service}${t.version?"@"+t.version:""}`,{kind:"gateway-traffic",host:t.host,project:t.service,caller:t.caller,version:t.version,count:t.count,avgMs:Math.round(t.totalMs/t.count),errors:t.errors});h.clear()},o.trafficFlushMs??5e3);return e.unref?.(),e},"startFlusher")}}p(createObservability,"createObservability"),c(createObservability,"createObservability");
|
|
@@ -1,103 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createRollout = createRollout;
|
|
4
|
-
/**
|
|
5
|
-
* Targeted-rollout support: authoritative client-IP resolution (trusted
|
|
6
|
-
* front-proxy header, anti-spoof), the sticky device key (signed cookie → uid
|
|
7
|
-
* header → minted), and the flat ABAC attribute bag shared by the HTTP + WS
|
|
8
|
-
* paths so a rule sees an identical bag on both.
|
|
9
|
-
*/
|
|
10
|
-
const node_crypto_1 = require("node:crypto");
|
|
11
|
-
const manifest_1 = require("../manifest");
|
|
12
|
-
const edge_1 = require("./edge");
|
|
13
|
-
function createRollout(opts) {
|
|
14
|
-
const clientIpHeader = opts.clientIpHeader?.toLowerCase();
|
|
15
|
-
// `clientIpHeader` is only trusted from a known front proxy. Default: loopback only
|
|
16
|
-
// (a co-located cloudflared/sidecar) — an off-proxy attacker hitting the origin directly
|
|
17
|
-
// cannot spoof the `ip` ABAC attribute.
|
|
18
|
-
const trustedProxies = opts.clientIpTrustedProxies ?? [];
|
|
19
|
-
const peerTrusted = (peer) => {
|
|
20
|
-
if (!peer)
|
|
21
|
-
return false;
|
|
22
|
-
const ip = peer.startsWith('::ffff:') ? peer.slice(7) : peer;
|
|
23
|
-
if (trustedProxies.length === 0)
|
|
24
|
-
return ip === '127.0.0.1' || peer === '::1';
|
|
25
|
-
return trustedProxies.some((c) => (0, manifest_1.matchCidr)(ip, c));
|
|
26
|
-
};
|
|
27
|
-
const clientIpOf = (req, fallback) => {
|
|
28
|
-
if (clientIpHeader && peerTrusted(req.socket?.remoteAddress ?? undefined)) {
|
|
29
|
-
const h = (0, edge_1.hdr)(req.headers[clientIpHeader]);
|
|
30
|
-
if (h)
|
|
31
|
-
return h;
|
|
32
|
-
}
|
|
33
|
-
return fallback;
|
|
34
|
-
};
|
|
35
|
-
const attrHeaders = (opts.attributeHeaders ?? []).map((h) => h.toLowerCase());
|
|
36
|
-
const uidHeader = opts.uidHeader?.toLowerCase();
|
|
37
|
-
const cookieOn = !!opts.cookie;
|
|
38
|
-
const cookieName = opts.cookie?.name ?? 'lensmcp_did';
|
|
39
|
-
const cookieSecret = opts.cookie?.secret;
|
|
40
|
-
const cookieDomain = opts.cookie?.domain;
|
|
41
|
-
const cookieMaxAge = opts.cookie?.maxAge ?? 31_536_000; // 1y
|
|
42
|
-
if (cookieOn && !cookieSecret)
|
|
43
|
-
console.warn('[gateway] device cookie has no secret — values are UNSIGNED (set cookie.secret to prevent bucket-grinding).');
|
|
44
|
-
const signId = (id) => (0, node_crypto_1.createHmac)('sha256', cookieSecret).update(id).digest('base64url');
|
|
45
|
-
const signedCookie = (id) => (cookieSecret ? `${id}.${signId(id)}` : id);
|
|
46
|
-
const verifyCookie = (val) => {
|
|
47
|
-
if (!cookieSecret)
|
|
48
|
-
return val || undefined; // unsigned mode
|
|
49
|
-
const i = val.lastIndexOf('.');
|
|
50
|
-
if (i < 0)
|
|
51
|
-
return undefined;
|
|
52
|
-
const id = val.slice(0, i);
|
|
53
|
-
const a = Buffer.from(val.slice(i + 1)), b = Buffer.from(signId(id));
|
|
54
|
-
return a.length === b.length && (0, node_crypto_1.timingSafeEqual)(a, b) ? id : undefined;
|
|
55
|
-
};
|
|
56
|
-
const parseCookie = (header, name) => {
|
|
57
|
-
if (!header)
|
|
58
|
-
return undefined;
|
|
59
|
-
for (const part of header.split(';')) {
|
|
60
|
-
const eq = part.indexOf('=');
|
|
61
|
-
if (eq > 0 && part.slice(0, eq).trim() === name)
|
|
62
|
-
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
63
|
-
}
|
|
64
|
-
return undefined;
|
|
65
|
-
};
|
|
66
|
-
const mintCookieHeader = (id) => {
|
|
67
|
-
let c = `${cookieName}=${encodeURIComponent(signedCookie(id))}; Path=/; Max-Age=${cookieMaxAge}; HttpOnly; SameSite=Lax; Secure`;
|
|
68
|
-
if (cookieDomain)
|
|
69
|
-
c += `; Domain=${cookieDomain}`;
|
|
70
|
-
return c;
|
|
71
|
-
};
|
|
72
|
-
const resolveStickyKey = (req) => {
|
|
73
|
-
if (cookieOn) {
|
|
74
|
-
const c = parseCookie(req.headers.cookie, cookieName);
|
|
75
|
-
const id = c ? verifyCookie(c) : undefined;
|
|
76
|
-
if (id)
|
|
77
|
-
return { key: id, mint: false };
|
|
78
|
-
}
|
|
79
|
-
if (uidHeader) {
|
|
80
|
-
const u = (0, edge_1.hdr)(req.headers[uidHeader]);
|
|
81
|
-
if (u)
|
|
82
|
-
return { key: u, mint: false };
|
|
83
|
-
}
|
|
84
|
-
if (cookieOn)
|
|
85
|
-
return { key: 'd_' + (0, node_crypto_1.randomBytes)(12).toString('base64url'), mint: true };
|
|
86
|
-
return { mint: false };
|
|
87
|
-
};
|
|
88
|
-
const buildAttrs = (req, fallbackIp, principal, key) => {
|
|
89
|
-
const a = { ...(principal ?? {}) };
|
|
90
|
-
a['ip'] = clientIpOf(req, fallbackIp);
|
|
91
|
-
a['method'] = (req.method ?? 'GET').toUpperCase();
|
|
92
|
-
a['path'] = (req.url ?? '/').split('?')[0];
|
|
93
|
-
if (key)
|
|
94
|
-
a['device'] = key;
|
|
95
|
-
for (const h of attrHeaders) {
|
|
96
|
-
const v = (0, edge_1.hdr)(req.headers[h]);
|
|
97
|
-
if (v !== undefined)
|
|
98
|
-
a['header.' + h] = v;
|
|
99
|
-
}
|
|
100
|
-
return a;
|
|
101
|
-
};
|
|
102
|
-
return { clientIpOf, buildAttrs, resolveStickyKey, mintCookieHeader };
|
|
103
|
-
}
|
|
1
|
+
"use strict";var H=Object.defineProperty;var u=(i,c)=>H(i,"name",{value:c,configurable:!0});var S=Object.defineProperty,r=u((i,c)=>S(i,"name",{value:c,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRollout=createRollout;const node_crypto_1=require("node:crypto"),manifest_1=require("../manifest"),edge_1=require("./edge");function createRollout(i){const c=i.clientIpHeader?.toLowerCase(),l=i.clientIpTrustedProxies??[],y=r(e=>{if(!e)return!1;const t=e.startsWith("::ffff:")?e.slice(7):e;return l.length===0?t==="127.0.0.1"||e==="::1":l.some(o=>(0,manifest_1.matchCidr)(t,o))},"peerTrusted"),f=r((e,t)=>{if(c&&y(e.socket?.remoteAddress??void 0)){const o=(0,edge_1.hdr)(e.headers[c]);if(o)return o}return t},"clientIpOf"),C=(i.attributeHeaders??[]).map(e=>e.toLowerCase()),m=i.uidHeader?.toLowerCase(),d=!!i.cookie,p=i.cookie?.name??"lensmcp_did",a=i.cookie?.secret,k=i.cookie?.domain,I=i.cookie?.maxAge??31536e3;d&&!a&&console.warn("[gateway] device cookie has no secret \u2014 values are UNSIGNED (set cookie.secret to prevent bucket-grinding).");const h=r(e=>(0,node_crypto_1.createHmac)("sha256",a).update(e).digest("base64url"),"signId"),b=r(e=>a?`${e}.${h(e)}`:e,"signedCookie"),x=r(e=>{if(!a)return e||void 0;const t=e.lastIndexOf(".");if(t<0)return;const o=e.slice(0,t),n=Buffer.from(e.slice(t+1)),s=Buffer.from(h(o));return n.length===s.length&&(0,node_crypto_1.timingSafeEqual)(n,s)?o:void 0},"verifyCookie"),_=r((e,t)=>{if(e)for(const o of e.split(";")){const n=o.indexOf("=");if(n>0&&o.slice(0,n).trim()===t)return decodeURIComponent(o.slice(n+1).trim())}},"parseCookie");return{clientIpOf:f,buildAttrs:r((e,t,o,n)=>{const s={...o??{}};s.ip=f(e,t),s.method=(e.method??"GET").toUpperCase(),s.path=(e.url??"/").split("?")[0],n&&(s.device=n);for(const g of C){const v=(0,edge_1.hdr)(e.headers[g]);v!==void 0&&(s["header."+g]=v)}return s},"buildAttrs"),resolveStickyKey:r(e=>{if(d){const t=_(e.headers.cookie,p),o=t?x(t):void 0;if(o)return{key:o,mint:!1}}if(m){const t=(0,edge_1.hdr)(e.headers[m]);if(t)return{key:t,mint:!1}}return d?{key:"d_"+(0,node_crypto_1.randomBytes)(12).toString("base64url"),mint:!0}:{mint:!1}},"resolveStickyKey"),mintCookieHeader:r(e=>{let t=`${p}=${encodeURIComponent(b(e))}; Path=/; Max-Age=${I}; HttpOnly; SameSite=Lax; Secure`;return k&&(t+=`; Domain=${k}`),t},"mintCookieHeader")}}u(createRollout,"createRollout"),r(createRollout,"createRollout");
|
|
@@ -1,40 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createRouteTable = createRouteTable;
|
|
4
|
-
/**
|
|
5
|
-
* The route table — rebuilt atomically from the manifest source (the SAME
|
|
6
|
-
* `buildRouteTable` brain as dev). The `routes` array + `manifestByService` map
|
|
7
|
-
* keep STABLE identities (content-replaced in place) so every other layer can
|
|
8
|
-
* hold the reference and always read the live table.
|
|
9
|
-
*/
|
|
10
|
-
const manifest_1 = require("../manifest");
|
|
11
|
-
function createRouteTable(opts, obs) {
|
|
12
|
-
const routes = [];
|
|
13
|
-
const manifestByService = new Map();
|
|
14
|
-
let current = opts.manifests.list();
|
|
15
|
-
const apply = (next) => {
|
|
16
|
-
current = next;
|
|
17
|
-
const built = (0, manifest_1.buildRouteTable)(next);
|
|
18
|
-
routes.length = 0;
|
|
19
|
-
routes.push(...built);
|
|
20
|
-
manifestByService.clear();
|
|
21
|
-
for (const m of next)
|
|
22
|
-
manifestByService.set(m.service, m);
|
|
23
|
-
};
|
|
24
|
-
const rebuild = (next) => {
|
|
25
|
-
apply(next);
|
|
26
|
-
if (opts.healthChecker) { // monitor every reachable upstream: direct manifest URLs + pooled endpoints
|
|
27
|
-
const urls = new Set();
|
|
28
|
-
for (const r of routes)
|
|
29
|
-
if (r.upstream)
|
|
30
|
-
urls.add(r.upstream);
|
|
31
|
-
for (const u of opts.pods?.endpoints?.() ?? [])
|
|
32
|
-
urls.add(u);
|
|
33
|
-
opts.healthChecker.track([...urls]);
|
|
34
|
-
}
|
|
35
|
-
obs.emit('info', `gateway routes: ${routes.length} on ${current.length} services`, 'gateway-up', { kind: 'gateway-up', routes: routes.map((r) => ({ host: r.host ?? '(default)', service: r.service, internal: !!r.internal })) });
|
|
36
|
-
};
|
|
37
|
-
apply(current); // initial build (no emit yet; server.ts fires the initial gateway-up via rebuild)
|
|
38
|
-
const unwatch = opts.manifests.watch?.((next) => rebuild(next));
|
|
39
|
-
return { routes, manifestByService, rebuild, manifests: () => current, stop: () => unwatch?.() };
|
|
40
|
-
}
|
|
1
|
+
"use strict";var d=Object.defineProperty;var u=(e,o)=>d(e,"name",{value:o,configurable:!0});var h=Object.defineProperty,r=u((e,o)=>h(e,"name",{value:o,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRouteTable=createRouteTable;const manifest_1=require("../manifest");function createRouteTable(e,o){const n=[],c=new Map;let i=e.manifests.list();const l=r(a=>{i=a;const t=(0,manifest_1.buildRouteTable)(a);n.length=0,n.push(...t),c.clear();for(const s of a)c.set(s.service,s)},"apply"),f=r(a=>{if(l(a),e.healthChecker){const t=new Set;for(const s of n)s.upstream&&t.add(s.upstream);for(const s of e.pods?.endpoints?.()??[])t.add(s);e.healthChecker.track([...t])}o.emit("info",`gateway routes: ${n.length} on ${i.length} services`,"gateway-up",{kind:"gateway-up",routes:n.map(t=>({host:t.host??"(default)",service:t.service,internal:!!t.internal}))})},"rebuild");l(i);const p=e.manifests.watch?.(a=>f(a));return{routes:n,manifestByService:c,rebuild:f,manifests:r(()=>i,"manifests"),stop:r(()=>p?.(),"stop")}}u(createRouteTable,"createRouteTable"),r(createRouteTable,"createRouteTable");
|