@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,79 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.startProdGateway = startProdGateway;
|
|
4
|
-
const observability_1 = require("./observability");
|
|
5
|
-
const routing_1 = require("./routing");
|
|
6
|
-
const rollout_1 = require("./rollout");
|
|
7
|
-
const upstream_1 = require("./upstream");
|
|
8
|
-
const auth_1 = require("./auth");
|
|
9
|
-
const hooks_1 = require("./hooks");
|
|
10
|
-
const cors_1 = require("./cors");
|
|
11
|
-
const handler_1 = require("./handler");
|
|
12
|
-
const upgrade_1 = require("./upgrade");
|
|
13
|
-
const app_1 = require("./app");
|
|
14
|
-
async function startProdGateway(opts) {
|
|
15
|
-
const ports = opts.ports?.length ? opts.ports : [opts.tls ? 8443 : 8080];
|
|
16
|
-
// Default authenticator FAILS CLOSED: a jwt route with no authenticator wired is rejected.
|
|
17
|
-
const authenticate = opts.authenticate ?? ((mode) => {
|
|
18
|
-
if (mode === 'jwt')
|
|
19
|
-
throw new Error('jwt route but no authenticator configured');
|
|
20
|
-
});
|
|
21
|
-
// ---- option-derived config (resolved once) ----
|
|
22
|
-
const versionHeader = (opts.versionHeader ?? 'x-lensmcp-version').toLowerCase();
|
|
23
|
-
const accessLog = !!opts.accessLog;
|
|
24
|
-
const corsCredentialOrigins = new Set((opts.corsCredentialOrigins ?? []).map((o) => o.toLowerCase()));
|
|
25
|
-
const keyToProject = new Map(Object.entries(opts.serviceKeys ?? {}).map(([p, k]) => [k, p]));
|
|
26
|
-
// ---- route table + observability (lazy getRoutes breaks the table↔obs cycle) ----
|
|
27
|
-
// A holder, so obs can read the routes lazily while `table` stays a const (assigned once,
|
|
28
|
-
// before any request — obs's getter only runs at request time).
|
|
29
|
-
const tableRef = {};
|
|
30
|
-
const obs = (0, observability_1.createObservability)(opts, () => tableRef.current.routes);
|
|
31
|
-
const table = (0, routing_1.createRouteTable)(opts, obs);
|
|
32
|
-
tableRef.current = table;
|
|
33
|
-
const rt = { opts, table, keyToProject, versionHeader, accessLog, corsCredentialOrigins };
|
|
34
|
-
// ---- layers ----
|
|
35
|
-
const rollout = (0, rollout_1.createRollout)(opts);
|
|
36
|
-
const upstream = (0, upstream_1.createUpstream)(opts, obs);
|
|
37
|
-
const edgeAuth = (0, auth_1.createEdgeAuth)(rt, obs, rollout, authenticate);
|
|
38
|
-
const hooks = (0, hooks_1.createHooks)(opts.hooks ?? {});
|
|
39
|
-
const reqState = new WeakMap();
|
|
40
|
-
const spanByReq = new WeakMap();
|
|
41
|
-
const ctxByReq = new WeakMap();
|
|
42
|
-
const cors = (0, cors_1.createCors)(rt, reqState);
|
|
43
|
-
const handler = (0, handler_1.createHandler)({ rt, obs, cors, auth: edgeAuth, upstream, rollout, hooks, reqState, spanByReq, ctxByReq });
|
|
44
|
-
const upgrade = (0, upgrade_1.createUpgrade)({ rt, auth: edgeAuth, upstream, hooks });
|
|
45
|
-
const { buildApp } = (0, app_1.createApp)({ rt, obs, cors, hooks, handler, upgrade, reqState, spanByReq, ctxByReq });
|
|
46
|
-
const flusher = obs.startFlusher();
|
|
47
|
-
table.rebuild(table.manifests()); // emit initial gateway-up (+ healthChecker.track)
|
|
48
|
-
// ---- listeners ----
|
|
49
|
-
const apps = [];
|
|
50
|
-
const boundPorts = [];
|
|
51
|
-
for (const port of ports) {
|
|
52
|
-
const app = await buildApp();
|
|
53
|
-
await app.listen({ port, host: '0.0.0.0' });
|
|
54
|
-
const addr = app.server.address();
|
|
55
|
-
const bound = typeof addr === 'object' && addr ? addr.port : port;
|
|
56
|
-
boundPorts.push(bound);
|
|
57
|
-
console.log(`[gateway] listening on ${opts.tls ? 'https' : 'http'}://0.0.0.0:${bound}`);
|
|
58
|
-
apps.push(app);
|
|
59
|
-
}
|
|
60
|
-
opts.healthChecker?.start(); // begin probing now that the route table (and tracked URLs) is warm
|
|
61
|
-
let stopped = false;
|
|
62
|
-
return {
|
|
63
|
-
ports: boundPorts,
|
|
64
|
-
routes: () => table.routes,
|
|
65
|
-
status: obs.buildStatusSnapshot,
|
|
66
|
-
stop: async () => {
|
|
67
|
-
if (stopped)
|
|
68
|
-
return;
|
|
69
|
-
stopped = true;
|
|
70
|
-
clearInterval(flusher);
|
|
71
|
-
table.stop();
|
|
72
|
-
opts.healthChecker?.stop();
|
|
73
|
-
opts.rateLimiter?.stop();
|
|
74
|
-
opts.metrics?.stop();
|
|
75
|
-
obs.emit('info', 'gateway down', 'gateway-down', { kind: 'gateway-down' });
|
|
76
|
-
await Promise.all(apps.map((a) => a.close())); // reply-from closes its undici pool on close
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
1
|
+
"use strict";var B=Object.defineProperty;var l=(e,o)=>B(e,"name",{value:o,configurable:!0});var x=Object.defineProperty,n=l((e,o)=>x(e,"name",{value:o,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.startProdGateway=startProdGateway;const observability_1=require("./observability"),routing_1=require("./routing"),rollout_1=require("./rollout"),upstream_1=require("./upstream"),auth_1=require("./auth"),hooks_1=require("./hooks"),cors_1=require("./cors"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),app_1=require("./app");async function startProdGateway(e){const o=e.ports?.length?e.ports:[e.tls?8443:8080],f=e.authenticate??(t=>{if(t==="jwt")throw new Error("jwt route but no authenticator configured")}),j=(e.versionHeader??"x-lensmcp-version").toLowerCase(),C=!!e.accessLog,P=new Set((e.corsCredentialOrigins??[]).map(t=>t.toLowerCase())),O=new Map(Object.entries(e.serviceKeys??{}).map(([t,i])=>[i,t])),h={},r=(0,observability_1.createObservability)(e,()=>h.current.routes),a=(0,routing_1.createRouteTable)(e,r);h.current=a;const s={opts:e,table:a,keyToProject:O,versionHeader:j,accessLog:C,corsCredentialOrigins:P},d=(0,rollout_1.createRollout)(e),w=(0,upstream_1.createUpstream)(e,r),y=(0,auth_1.createEdgeAuth)(s,r,d,f),u=(0,hooks_1.createHooks)(e.hooks??{}),c=new WeakMap,g=new WeakMap,b=new WeakMap,q=(0,cors_1.createCors)(s,c),R=(0,handler_1.createHandler)({rt:s,obs:r,cors:q,auth:y,upstream:w,rollout:d,hooks:u,reqState:c,spanByReq:g,ctxByReq:b}),L=(0,upgrade_1.createUpgrade)({rt:s,auth:y,upstream:w,hooks:u}),{buildApp:M}=(0,app_1.createApp)({rt:s,obs:r,cors:q,hooks:u,handler:R,upgrade:L,reqState:c,spanByReq:g,ctxByReq:b}),S=r.startFlusher();a.rebuild(a.manifests());const m=[],k=[];for(const t of o){const i=await M();await i.listen({port:t,host:"0.0.0.0"});const p=i.server.address(),_=typeof p=="object"&&p?p.port:t;k.push(_),console.log(`[gateway] listening on ${e.tls?"https":"http"}://0.0.0.0:${_}`),m.push(i)}e.healthChecker?.start();let v=!1;return{ports:k,routes:n(()=>a.routes,"routes"),status:r.buildStatusSnapshot,stop:n(async()=>{v||(v=!0,clearInterval(S),a.stop(),e.healthChecker?.stop(),e.rateLimiter?.stop(),e.metrics?.stop(),r.emit("info","gateway down","gateway-down",{kind:"gateway-down"}),await Promise.all(m.map(t=>t.close())))},"stop")}}l(startProdGateway,"startProdGateway"),n(startProdGateway,"startProdGateway");
|
|
@@ -9,4 +9,3 @@ export declare const HOP_BY_HOP: string[];
|
|
|
9
9
|
export declare const stripTrust: (req: http.IncomingMessage) => void;
|
|
10
10
|
/** Stamp the verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
|
|
11
11
|
export declare const stampIdentity: (req: http.IncomingMessage, claims: Record<string, unknown>) => void;
|
|
12
|
-
//# sourceMappingURL=trust.d.ts.map
|
|
@@ -1,32 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.stampIdentity = exports.stripTrust = exports.HOP_BY_HOP = exports.TRUST_HEADERS = void 0;
|
|
4
|
-
// Drop any client-supplied copy at ingress so identity can only originate from a verified token.
|
|
5
|
-
exports.TRUST_HEADERS = ['x-internal-token', 'x-api-key-id', 'x-user-id', 'x-tenant-id', 'x-user-roles', 'x-user-permissions'];
|
|
6
|
-
// Also drop client-supplied hop-by-hop control headers: a `Connection: x-internal-token`
|
|
7
|
-
// would otherwise make undici strip the gateway's OWN injected trust headers before the
|
|
8
|
-
// upstream (CVE-2026-33805). The proxy manages its own upstream connection semantics.
|
|
9
|
-
exports.HOP_BY_HOP = ['connection', 'proxy-connection', 'keep-alive'];
|
|
10
|
-
const stripTrust = (req) => {
|
|
11
|
-
for (const h of exports.TRUST_HEADERS)
|
|
12
|
-
delete req.headers[h];
|
|
13
|
-
for (const h of exports.HOP_BY_HOP)
|
|
14
|
-
delete req.headers[h];
|
|
15
|
-
};
|
|
16
|
-
exports.stripTrust = stripTrust;
|
|
17
|
-
/** Stamp the verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
|
|
18
|
-
const stampIdentity = (req, claims) => {
|
|
19
|
-
// Only stamp SCALAR claims — an object/array sub/tid must never coerce to
|
|
20
|
-
// "[object Object]" / a CSV tenant header (claim type-confusion).
|
|
21
|
-
const set = (k, v) => { if ((typeof v === 'string' && v !== '') || typeof v === 'number')
|
|
22
|
-
req.headers[k] = String(v); };
|
|
23
|
-
set('x-user-id', claims['sub']);
|
|
24
|
-
set('x-tenant-id', claims['tid']);
|
|
25
|
-
const roles = claims['roles'];
|
|
26
|
-
if (Array.isArray(roles) && roles.length)
|
|
27
|
-
req.headers['x-user-roles'] = roles.map(String).join(',');
|
|
28
|
-
const perms = claims['perms'];
|
|
29
|
-
if (Array.isArray(perms) && perms.length)
|
|
30
|
-
req.headers['x-user-permissions'] = perms.map(String).join(',');
|
|
31
|
-
};
|
|
32
|
-
exports.stampIdentity = stampIdentity;
|
|
1
|
+
"use strict";var d=Object.defineProperty;var p=(t,e)=>d(t,"name",{value:e,configurable:!0});var x=Object.defineProperty,r=p((t,e)=>x(t,"name",{value:e,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.stampIdentity=exports.stripTrust=exports.HOP_BY_HOP=exports.TRUST_HEADERS=void 0,exports.TRUST_HEADERS=["x-internal-token","x-api-key-id","x-user-id","x-tenant-id","x-user-roles","x-user-permissions"],exports.HOP_BY_HOP=["connection","proxy-connection","keep-alive"];const stripTrust=r(t=>{for(const e of exports.TRUST_HEADERS)delete t.headers[e];for(const e of exports.HOP_BY_HOP)delete t.headers[e]},"stripTrust");exports.stripTrust=stripTrust;const stampIdentity=r((t,e)=>{const i=r((a,s)=>{(typeof s=="string"&&s!==""||typeof s=="number")&&(t.headers[a]=String(s))},"set");i("x-user-id",e.sub),i("x-tenant-id",e.tid);const o=e.roles;Array.isArray(o)&&o.length&&(t.headers["x-user-roles"]=o.map(String).join(","));const n=e.perms;Array.isArray(n)&&n.length&&(t.headers["x-user-permissions"]=n.map(String).join(","))},"stampIdentity");exports.stampIdentity=stampIdentity;
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});
|
|
@@ -1,116 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Raw WebSocket upgrade proxy (same trust model; net-level pipe). The upgrade
|
|
7
|
-
* request is rebuilt from `req.rawHeaders` verbatim — minus the gateway-owned
|
|
8
|
-
* trust headers + the client's hop-by-hop control — plus the validated caller +
|
|
9
|
-
* target token + verified identity. Reuses the shared auth + upstream layers.
|
|
10
|
-
*/
|
|
11
|
-
const net = tslib_1.__importStar(require("node:net"));
|
|
12
|
-
const manifest_1 = require("../manifest");
|
|
13
|
-
const edge_1 = require("./edge");
|
|
14
|
-
const trust_1 = require("./trust");
|
|
15
|
-
function createUpgrade(deps) {
|
|
16
|
-
const { rt, auth, upstream, hooks } = deps;
|
|
17
|
-
const run = async (req, socket, head) => {
|
|
18
|
-
(0, trust_1.stripTrust)(req);
|
|
19
|
-
const host = (req.headers.host || '').split(':')[0];
|
|
20
|
-
const cpath = (0, edge_1.canonicalPath)(req.url ?? '/');
|
|
21
|
-
if (cpath === undefined) {
|
|
22
|
-
socket.destroy();
|
|
23
|
-
return;
|
|
24
|
-
} // reject path-canonicalization attacks
|
|
25
|
-
const route = (0, manifest_1.matchRoute)(rt.table.routes, host, req.url ?? '/');
|
|
26
|
-
if (!route) {
|
|
27
|
-
socket.destroy();
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const ctx = hooks.any ? hooks.upgradeContext(req, socket, undefined) : undefined;
|
|
31
|
-
if (ctx)
|
|
32
|
-
ctx.route = route;
|
|
33
|
-
const claims = route.internal ? undefined : rt.opts.identify?.(req);
|
|
34
|
-
let caller;
|
|
35
|
-
if (route.internal) {
|
|
36
|
-
const v = auth.internal(req, route, undefined);
|
|
37
|
-
if (!v.ok) {
|
|
38
|
-
socket.destroy();
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
caller = v.caller;
|
|
42
|
-
if (ctx)
|
|
43
|
-
ctx.caller = caller;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
// Same fail-closed ABAC + identical attribute bag as the HTTP path (no divergence).
|
|
47
|
-
const v = auth.public(req, route, cpath, claims, req.socket.remoteAddress ?? '', undefined);
|
|
48
|
-
if (!v.ok) {
|
|
49
|
-
socket.destroy();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (ctx)
|
|
53
|
-
ctx.claims = claims;
|
|
54
|
-
if (ctx && hooks.has('afterAuth')) {
|
|
55
|
-
await hooks.run('afterAuth', ctx);
|
|
56
|
-
if (ctx.responded())
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (route.prependPrefix) {
|
|
61
|
-
const u = req.url ?? '/';
|
|
62
|
-
const pp = route.prependPrefix;
|
|
63
|
-
if (u !== pp && !u.startsWith(pp + '/'))
|
|
64
|
-
req.url = pp + u;
|
|
65
|
-
} // segment-boundary
|
|
66
|
-
if (ctx && hooks.has('onUpgrade')) {
|
|
67
|
-
await hooks.run('onUpgrade', ctx);
|
|
68
|
-
if (ctx.responded())
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const pinned = (0, edge_1.hdr)(req.headers[rt.versionHeader]);
|
|
72
|
-
const up = await upstream.resolveUpstream(route, undefined, pinned);
|
|
73
|
-
if (!up || !('url' in up)) {
|
|
74
|
-
socket.destroy();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const u = new URL(up.url);
|
|
78
|
-
const upstreamSock = net.connect(Number(u.port) || (u.protocol === 'https:' ? 443 : 80), u.hostname, () => {
|
|
79
|
-
// rebuild the upgrade request from rawHeaders (verbatim), minus the trust headers we own (incl.
|
|
80
|
-
// client identity) + the client's hop-by-hop control (so a `Connection: x-internal-token` can't
|
|
81
|
-
// strip the gateway's injected headers downstream), plus the validated caller + token + identity.
|
|
82
|
-
const strip = new Set(['x-internal-token', 'x-api-key-id', 'x-api-key', 'x-user-id', 'x-tenant-id', 'x-user-roles', 'x-user-permissions', 'connection', 'proxy-connection', 'keep-alive']);
|
|
83
|
-
const lines = [`${req.method} ${req.url} HTTP/1.1`, 'Connection: Upgrade']; // gateway-owned, clean upgrade
|
|
84
|
-
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
85
|
-
const k = req.rawHeaders[i];
|
|
86
|
-
if (!strip.has(k.toLowerCase()))
|
|
87
|
-
lines.push(`${k}: ${req.rawHeaders[i + 1]}`);
|
|
88
|
-
}
|
|
89
|
-
if (caller)
|
|
90
|
-
lines.push(`x-api-key-id: ${caller}`);
|
|
91
|
-
const token = rt.opts.serviceKeys?.[route.service];
|
|
92
|
-
if (token)
|
|
93
|
-
lines.push(`x-internal-token: ${token}`);
|
|
94
|
-
if (claims) { // stamp the verified identity (mirrors the HTTP path's stampIdentity)
|
|
95
|
-
if (claims['sub'])
|
|
96
|
-
lines.push(`x-user-id: ${String(claims['sub'])}`);
|
|
97
|
-
if (claims['tid'])
|
|
98
|
-
lines.push(`x-tenant-id: ${String(claims['tid'])}`);
|
|
99
|
-
const roles = claims['roles'];
|
|
100
|
-
if (Array.isArray(roles) && roles.length)
|
|
101
|
-
lines.push(`x-user-roles: ${roles.map(String).join(',')}`);
|
|
102
|
-
const perms = claims['perms'];
|
|
103
|
-
if (Array.isArray(perms) && perms.length)
|
|
104
|
-
lines.push(`x-user-permissions: ${perms.map(String).join(',')}`);
|
|
105
|
-
}
|
|
106
|
-
upstreamSock.write(lines.join('\r\n') + '\r\n\r\n');
|
|
107
|
-
if (head?.length)
|
|
108
|
-
upstreamSock.write(head);
|
|
109
|
-
socket.pipe(upstreamSock);
|
|
110
|
-
upstreamSock.pipe(socket);
|
|
111
|
-
});
|
|
112
|
-
upstreamSock.on('error', () => socket.destroy());
|
|
113
|
-
socket.on('error', () => upstreamSock.destroy());
|
|
114
|
-
};
|
|
115
|
-
return (req, socket, head) => { void run(req, socket, head); };
|
|
116
|
-
}
|
|
1
|
+
"use strict";var U=Object.defineProperty;var g=(l,i)=>U(l,"name",{value:i,configurable:!0});var S=Object.defineProperty,m=g((l,i)=>S(l,"name",{value:i,configurable:!0}),"m");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpgrade=createUpgrade;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),manifest_1=require("../manifest"),edge_1=require("./edge"),trust_1=require("./trust");function createUpgrade(l){const{rt:i,auth:v,upstream:$,hooks:d}=l,k=m(async(e,r,h)=>{(0,trust_1.stripTrust)(e);const _=(e.headers.host||"").split(":")[0],w=(0,edge_1.canonicalPath)(e.url??"/");if(w===void 0){r.destroy();return}const s=(0,manifest_1.matchRoute)(i.table.routes,_,e.url??"/");if(!s){r.destroy();return}const t=d.any?d.upgradeContext(e,r,void 0):void 0;t&&(t.route=s);const o=s.internal?void 0:i.opts.identify?.(e);let f;if(s.internal){const a=v.internal(e,s,void 0);if(!a.ok){r.destroy();return}f=a.caller,t&&(t.caller=f)}else{if(!v.public(e,s,w,o,e.socket.remoteAddress??"",void 0).ok){r.destroy();return}if(t&&(t.claims=o),t&&d.has("afterAuth")&&(await d.run("afterAuth",t),t.responded()))return}if(s.prependPrefix){const a=e.url??"/",n=s.prependPrefix;a!==n&&!a.startsWith(n+"/")&&(e.url=n+a)}if(t&&d.has("onUpgrade")&&(await d.run("onUpgrade",t),t.responded()))return;const A=(0,edge_1.hdr)(e.headers[i.versionHeader]),y=await $.resolveUpstream(s,void 0,A);if(!y||!("url"in y)){r.destroy();return}const x=new URL(y.url),p=net.connect(Number(x.port)||(x.protocol==="https:"?443:80),x.hostname,()=>{const a=new Set(["x-internal-token","x-api-key-id","x-api-key","x-user-id","x-tenant-id","x-user-roles","x-user-permissions","connection","proxy-connection","keep-alive"]),n=[`${e.method} ${e.url} HTTP/1.1`,"Connection: Upgrade"];for(let u=0;u<e.rawHeaders.length;u+=2){const c=e.rawHeaders[u];a.has(c.toLowerCase())||n.push(`${c}: ${e.rawHeaders[u+1]}`)}f&&n.push(`x-api-key-id: ${f}`);const b=i.opts.serviceKeys?.[s.service];if(b&&n.push(`x-internal-token: ${b}`),o){o.sub&&n.push(`x-user-id: ${String(o.sub)}`),o.tid&&n.push(`x-tenant-id: ${String(o.tid)}`);const u=o.roles;Array.isArray(u)&&u.length&&n.push(`x-user-roles: ${u.map(String).join(",")}`);const c=o.perms;Array.isArray(c)&&c.length&&n.push(`x-user-permissions: ${c.map(String).join(",")}`)}p.write(n.join(`\r
|
|
2
|
+
`)+`\r
|
|
3
|
+
\r
|
|
4
|
+
`),h?.length&&p.write(h),r.pipe(p),p.pipe(r)});p.on("error",()=>r.destroy()),r.on("error",()=>p.destroy())},"run");return(e,r,h)=>{k(e,r,h)}}g(createUpgrade,"createUpgrade"),m(createUpgrade,"createUpgrade");
|
|
@@ -10,4 +10,3 @@ export interface UpstreamLayer {
|
|
|
10
10
|
resolveUpstream(route: Route, trace: GwTrace | undefined, pinned?: string): Promise<Upstream | undefined>;
|
|
11
11
|
}
|
|
12
12
|
export declare function createUpstream(opts: ProdGatewayOptions, obs: Observability): UpstreamLayer;
|
|
13
|
-
//# sourceMappingURL=upstream.d.ts.map
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createUpstream = createUpstream;
|
|
4
|
-
function createUpstream(opts, obs) {
|
|
5
|
-
const resolveUpstream = async (route, trace, pinned) => {
|
|
6
|
-
if (route.upstream)
|
|
7
|
-
return { url: route.upstream };
|
|
8
|
-
const opt = {
|
|
9
|
-
...(pinned ? { version: pinned } : {}),
|
|
10
|
-
...(opts.healthChecker ? { isHealthy: (u) => opts.healthChecker.isHealthy(u) } : {}),
|
|
11
|
-
};
|
|
12
|
-
let up = opts.pods?.pick(route.service, opt);
|
|
13
|
-
if (!up && opts.pods?.ensureUp) {
|
|
14
|
-
obs.step(trace, 'ensure-up', { project: route.service });
|
|
15
|
-
if (await opts.pods.ensureUp(route.service))
|
|
16
|
-
up = opts.pods.pick(route.service, opt);
|
|
17
|
-
}
|
|
18
|
-
return up;
|
|
19
|
-
};
|
|
20
|
-
return { resolveUpstream };
|
|
21
|
-
}
|
|
1
|
+
"use strict";var l=Object.defineProperty;var p=(e,t)=>l(e,"name",{value:t,configurable:!0});var u=Object.defineProperty,s=p((e,t)=>u(e,"name",{value:t,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpstream=createUpstream;function createUpstream(e,t){return{resolveUpstream:s(async(r,o,c)=>{if(r.upstream)return{url:r.upstream};const i={...c?{version:c}:{},...e.healthChecker?{isHealthy:s(n=>e.healthChecker.isHealthy(n),"isHealthy")}:{}};let a=e.pods?.pick(r.service,i);return!a&&e.pods?.ensureUp&&(t.step(o,"ensure-up",{project:r.service}),await e.pods.ensureUp(r.service)&&(a=e.pods.pick(r.service,i))),a},"resolveUpstream")}}p(createUpstream,"createUpstream"),s(createUpstream,"createUpstream");
|
|
@@ -43,4 +43,3 @@ export declare function rolloutPodProvider(table: RolloutTable): PodProvider & {
|
|
|
43
43
|
export declare function endpointsPodProvider(endpoints: Record<string, string[]>): PodProvider & {
|
|
44
44
|
update: (next: Record<string, string[]>) => void;
|
|
45
45
|
};
|
|
46
|
-
//# sourceMappingURL=providers-prod.d.ts.map
|
|
@@ -1,232 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.staticManifestProvider = staticManifestProvider;
|
|
4
|
-
exports.fileManifestProvider = fileManifestProvider;
|
|
5
|
-
exports.rolloutPodProvider = rolloutPodProvider;
|
|
6
|
-
exports.endpointsPodProvider = endpointsPodProvider;
|
|
7
|
-
/**
|
|
8
|
-
* Production providers — the environment-specific halves of the gateway that
|
|
9
|
-
* the dev side fills with workspace-scan + unix-socket pools. Here:
|
|
10
|
-
*
|
|
11
|
-
* - manifest source: a JSON file (or a static array), watched + atomically
|
|
12
|
-
* swapped. A registry-backed provider (Redis key-space / k8s ConfigMaps)
|
|
13
|
-
* plugs in behind the same `ManifestProvider` interface later.
|
|
14
|
-
* - pod resolution: `endpointsPodProvider` round-robins per-service upstream
|
|
15
|
-
* URLs (gateway-side LB). When a manifest carries its own `upstream` (a
|
|
16
|
-
* k8s Service URL that load-balances pods itself), the core uses that
|
|
17
|
-
* directly and no PodProvider is needed.
|
|
18
|
-
*
|
|
19
|
-
* Bad input never takes the gateway down: an unparseable manifest file is
|
|
20
|
-
* rejected and the last-good table keeps serving (versioned swap).
|
|
21
|
-
*/
|
|
22
|
-
const node_fs_1 = require("node:fs");
|
|
23
|
-
const manifest_1 = require("./manifest");
|
|
24
|
-
const toSpec = (v) => Array.isArray(v) ? { rules: [], cohorts: v } : { rules: v.rules ?? [], cohorts: v.cohorts ?? [] };
|
|
25
|
-
/** A fixed set of manifests (tests, single-shot config). */
|
|
26
|
-
function staticManifestProvider(manifests) {
|
|
27
|
-
return { list: () => manifests };
|
|
28
|
-
}
|
|
29
|
-
function parseManifests(raw) {
|
|
30
|
-
let data;
|
|
31
|
-
try {
|
|
32
|
-
data = JSON.parse(raw);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
// Accept either a bare array or `{ manifests: [...] }`.
|
|
38
|
-
const arr = Array.isArray(data) ? data : data?.manifests;
|
|
39
|
-
if (!Array.isArray(arr))
|
|
40
|
-
return undefined;
|
|
41
|
-
const ok = arr.filter((m) => !!m && typeof m === 'object' && typeof m.service === 'string' && Array.isArray(m.mounts));
|
|
42
|
-
// A file that parsed but yielded ZERO valid manifests is treated as bad
|
|
43
|
-
// (likely a truncated/half-written deploy) — keep last-good rather than
|
|
44
|
-
// black-hole all traffic.
|
|
45
|
-
return ok.length > 0 ? ok : undefined;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Watch a manifests JSON file; atomically swap the in-memory set on a valid
|
|
49
|
-
* change, reject and keep last-good on a bad one. `watch` notifies subscribers
|
|
50
|
-
* with the new set. Poll fallback covers editors/atomic-rename + platforms
|
|
51
|
-
* where fs.watch is unreliable.
|
|
52
|
-
*/
|
|
53
|
-
function fileManifestProvider(filePath, opts = {}) {
|
|
54
|
-
const onError = opts.onError ?? (() => undefined);
|
|
55
|
-
let current = [];
|
|
56
|
-
let lastMtimeMs = -1;
|
|
57
|
-
const subs = new Set();
|
|
58
|
-
const reload = () => {
|
|
59
|
-
if (!(0, node_fs_1.existsSync)(filePath))
|
|
60
|
-
return;
|
|
61
|
-
let mtime;
|
|
62
|
-
try {
|
|
63
|
-
mtime = (0, node_fs_1.statSync)(filePath).mtimeMs;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (mtime === lastMtimeMs)
|
|
69
|
-
return;
|
|
70
|
-
let raw;
|
|
71
|
-
try {
|
|
72
|
-
raw = (0, node_fs_1.readFileSync)(filePath, 'utf8');
|
|
73
|
-
}
|
|
74
|
-
catch (e) {
|
|
75
|
-
onError(e);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const parsed = parseManifests(raw);
|
|
79
|
-
if (!parsed) {
|
|
80
|
-
onError(new Error(`[gateway] rejecting invalid manifest file ${filePath} — keeping last-good`));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
lastMtimeMs = mtime;
|
|
84
|
-
current = parsed;
|
|
85
|
-
for (const cb of subs) {
|
|
86
|
-
try {
|
|
87
|
-
cb(current);
|
|
88
|
-
}
|
|
89
|
-
catch (e) {
|
|
90
|
-
onError(e);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
reload(); // initial load (best-effort; empty until the file appears)
|
|
95
|
-
let watcher;
|
|
96
|
-
try {
|
|
97
|
-
watcher = (0, node_fs_1.watch)(filePath, { persistent: false }, () => reload());
|
|
98
|
-
}
|
|
99
|
-
catch { /* dir may not exist yet — poll covers it */ }
|
|
100
|
-
const timer = setInterval(reload, opts.pollMs ?? 1000);
|
|
101
|
-
timer.unref?.();
|
|
102
|
-
return {
|
|
103
|
-
list: () => current,
|
|
104
|
-
watch: (cb) => { subs.add(cb); return () => subs.delete(cb); },
|
|
105
|
-
stop: () => { try {
|
|
106
|
-
watcher?.close();
|
|
107
|
-
}
|
|
108
|
-
catch { /* ignore */ } clearInterval(timer); },
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Rollout-aware pod provider: each service is a set of VERSION COHORTS, and
|
|
113
|
-
* `pick` splits traffic across them by weight (smooth weighted round-robin —
|
|
114
|
-
* a 90/10 canary interleaves, never bursts) and round-robins endpoints WITHIN
|
|
115
|
-
* the chosen cohort. `pick(svc, {version})` pins a cohort (sticky canary /
|
|
116
|
-
* debugging). `update` swaps weights/cohorts live — a rollout ramps
|
|
117
|
-
* (10→25→50→100) or rolls back without redeploying the gateway. The returned
|
|
118
|
-
* Upstream carries `version` so the gateway can surface which cohort served.
|
|
119
|
-
*/
|
|
120
|
-
function rolloutPodProvider(table) {
|
|
121
|
-
const svc = new Map();
|
|
122
|
-
const build = (spec) => ({
|
|
123
|
-
cohorts: spec.cohorts.map((c) => ({ ...c, urls: c.urls.slice() })),
|
|
124
|
-
select: (0, manifest_1.smoothWeightedSelector)(spec.cohorts.map((c) => c.weight)),
|
|
125
|
-
rr: spec.cohorts.map(() => -1),
|
|
126
|
-
rules: spec.rules ?? [],
|
|
127
|
-
});
|
|
128
|
-
const load = (t) => {
|
|
129
|
-
for (const [s, v] of Object.entries(t))
|
|
130
|
-
svc.set(s, build(toSpec(v)));
|
|
131
|
-
for (const s of [...svc.keys()])
|
|
132
|
-
if (!t[s])
|
|
133
|
-
svc.delete(s);
|
|
134
|
-
};
|
|
135
|
-
load(table);
|
|
136
|
-
const firstNonEmpty = (st) => st.cohorts.findIndex((c) => c.urls.length > 0);
|
|
137
|
-
return {
|
|
138
|
-
pick(service, opts) {
|
|
139
|
-
const st = svc.get(service);
|
|
140
|
-
if (!st || st.cohorts.length === 0)
|
|
141
|
-
return undefined;
|
|
142
|
-
const isHealthy = opts?.isHealthy;
|
|
143
|
-
// round-robin the chosen cohort, preferring healthy endpoints; undefined if none pass.
|
|
144
|
-
const pickUrl = (ci, gate) => {
|
|
145
|
-
const cohort = st.cohorts[ci];
|
|
146
|
-
if (!cohort || cohort.urls.length === 0)
|
|
147
|
-
return undefined;
|
|
148
|
-
const n = cohort.urls.length;
|
|
149
|
-
for (let i = 0; i < n; i++) {
|
|
150
|
-
st.rr[ci] = (st.rr[ci] + 1) % n;
|
|
151
|
-
const u = cohort.urls[st.rr[ci]];
|
|
152
|
-
if (!gate || gate(u))
|
|
153
|
-
return u;
|
|
154
|
-
}
|
|
155
|
-
return undefined;
|
|
156
|
-
};
|
|
157
|
-
const pinnedCi = opts?.version ? st.cohorts.findIndex((c) => c.version === opts.version && c.urls.length > 0) : -1;
|
|
158
|
-
if (pinnedCi >= 0) {
|
|
159
|
-
const url = pickUrl(pinnedCi, isHealthy) ?? pickUrl(pinnedCi); // fail-open WITHIN the pinned cohort
|
|
160
|
-
return url !== undefined ? { url, version: st.cohorts[pinnedCi].version } : undefined;
|
|
161
|
-
}
|
|
162
|
-
// not pinned (or unknown version) → weighted selection (unchanged contract)
|
|
163
|
-
let ci = st.select();
|
|
164
|
-
if (!st.cohorts[ci] || st.cohorts[ci].urls.length === 0)
|
|
165
|
-
ci = firstNonEmpty(st);
|
|
166
|
-
let url = pickUrl(ci, isHealthy);
|
|
167
|
-
// chosen cohort has no healthy endpoint → try any cohort that does
|
|
168
|
-
if (url === undefined && isHealthy) {
|
|
169
|
-
for (let i = 0; i < st.cohorts.length; i++) {
|
|
170
|
-
const u = pickUrl(i, isHealthy);
|
|
171
|
-
if (u !== undefined) {
|
|
172
|
-
ci = i;
|
|
173
|
-
url = u;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (url === undefined)
|
|
179
|
-
url = pickUrl(ci); // fail-open: everything looks down → still route
|
|
180
|
-
return url !== undefined ? { url, version: st.cohorts[ci].version } : undefined;
|
|
181
|
-
},
|
|
182
|
-
endpoints() {
|
|
183
|
-
const urls = new Set();
|
|
184
|
-
for (const st of svc.values())
|
|
185
|
-
for (const c of st.cohorts)
|
|
186
|
-
for (const u of c.urls)
|
|
187
|
-
urls.add(u);
|
|
188
|
-
return [...urls];
|
|
189
|
-
},
|
|
190
|
-
// first targeting rule that matches the request's attributes (sticky-by-attribute)
|
|
191
|
-
resolveVersion(service, attrs) {
|
|
192
|
-
const st = svc.get(service);
|
|
193
|
-
return st && st.rules.length ? (0, manifest_1.pickTargetedVersion)(st.rules, attrs) : undefined;
|
|
194
|
-
},
|
|
195
|
-
// consistent-hash a stable key to a cohort version (sticky weighted split)
|
|
196
|
-
bucket(service, key) {
|
|
197
|
-
const st = svc.get(service);
|
|
198
|
-
if (!st || st.cohorts.length === 0)
|
|
199
|
-
return undefined;
|
|
200
|
-
const cohort = st.cohorts[(0, manifest_1.bucketByWeight)(`${key}:${service}`, st.cohorts.map((c) => c.weight))];
|
|
201
|
-
return cohort ? { version: cohort.version } : undefined;
|
|
202
|
-
},
|
|
203
|
-
ensureUp: async () => true, // prod services are always-on (k8s keeps them up)
|
|
204
|
-
drop(service, upstream) {
|
|
205
|
-
const st = svc.get(service);
|
|
206
|
-
if (st && 'url' in upstream)
|
|
207
|
-
for (const c of st.cohorts)
|
|
208
|
-
c.urls = c.urls.filter((u) => u !== upstream.url);
|
|
209
|
-
},
|
|
210
|
-
update: load,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Flat single-version load balancing — the common case (no rollout in
|
|
215
|
-
* progress). A thin wrapper over `rolloutPodProvider` with one synthetic
|
|
216
|
-
* cohort per service, so the rollout path is the only code path. The synthetic
|
|
217
|
-
* version is STRIPPED from the result: flat LB carries no `version`, so a
|
|
218
|
-
* response only gets `x-lensmcp-version` (and a `rollout` trace step) when a
|
|
219
|
-
* genuine multi-cohort rollout served it.
|
|
220
|
-
*/
|
|
221
|
-
function endpointsPodProvider(endpoints) {
|
|
222
|
-
const toCohorts = (eps) => Object.fromEntries(Object.entries(eps).map(([s, urls]) => [s, [{ version: '_flat', weight: 1, urls }]]));
|
|
223
|
-
const inner = rolloutPodProvider(toCohorts(endpoints));
|
|
224
|
-
const unversion = (u) => u && 'url' in u ? { url: u.url } : u;
|
|
225
|
-
return {
|
|
226
|
-
...inner,
|
|
227
|
-
pick: (service, opts) => unversion(inner.pick(service, opts)),
|
|
228
|
-
resolveVersion: () => undefined, // flat LB: no targeting rules
|
|
229
|
-
bucket: () => undefined, // flat LB: single version, nothing to stick to
|
|
230
|
-
update: (next) => inner.update(toCohorts(next)),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
1
|
+
"use strict";var b=Object.defineProperty;var p=(s,o)=>b(s,"name",{value:o,configurable:!0});var w=Object.defineProperty,n=p((s,o)=>w(s,"name",{value:o,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.staticManifestProvider=staticManifestProvider,exports.fileManifestProvider=fileManifestProvider,exports.rolloutPodProvider=rolloutPodProvider,exports.endpointsPodProvider=endpointsPodProvider;const node_fs_1=require("node:fs"),manifest_1=require("./manifest"),toSpec=n(s=>Array.isArray(s)?{rules:[],cohorts:s}:{rules:s.rules??[],cohorts:s.cohorts??[]},"toSpec");function staticManifestProvider(s){return{list:n(()=>s,"list")}}p(staticManifestProvider,"staticManifestProvider"),n(staticManifestProvider,"staticManifestProvider");function parseManifests(s){let o;try{o=JSON.parse(s)}catch{return}const a=Array.isArray(o)?o:o?.manifests;if(!Array.isArray(a))return;const f=a.filter(i=>!!i&&typeof i=="object"&&typeof i.service=="string"&&Array.isArray(i.mounts));return f.length>0?f:void 0}p(parseManifests,"parseManifests"),n(parseManifests,"parseManifests");function fileManifestProvider(s,o={}){const a=o.onError??(()=>{});let f=[],i=-1;const t=new Set,e=n(()=>{if(!(0,node_fs_1.existsSync)(s))return;let l;try{l=(0,node_fs_1.statSync)(s).mtimeMs}catch{return}if(l===i)return;let v;try{v=(0,node_fs_1.readFileSync)(s,"utf8")}catch(h){a(h);return}const d=parseManifests(v);if(!d){a(new Error(`[gateway] rejecting invalid manifest file ${s} \u2014 keeping last-good`));return}i=l,f=d;for(const h of t)try{h(f)}catch(c){a(c)}},"reload");e();let r;try{r=(0,node_fs_1.watch)(s,{persistent:!1},()=>e())}catch{}const u=setInterval(e,o.pollMs??1e3);return u.unref?.(),{list:n(()=>f,"list"),watch:n(l=>(t.add(l),()=>t.delete(l)),"watch"),stop:n(()=>{try{r?.close()}catch{}clearInterval(u)},"stop")}}p(fileManifestProvider,"fileManifestProvider"),n(fileManifestProvider,"fileManifestProvider");function rolloutPodProvider(s){const o=new Map,a=n(t=>({cohorts:t.cohorts.map(e=>({...e,urls:e.urls.slice()})),select:(0,manifest_1.smoothWeightedSelector)(t.cohorts.map(e=>e.weight)),rr:t.cohorts.map(()=>-1),rules:t.rules??[]}),"build"),f=n(t=>{for(const[e,r]of Object.entries(t))o.set(e,a(toSpec(r)));for(const e of[...o.keys()])t[e]||o.delete(e)},"load");f(s);const i=n(t=>t.cohorts.findIndex(e=>e.urls.length>0),"firstNonEmpty");return{pick(t,e){const r=o.get(t);if(!r||r.cohorts.length===0)return;const u=e?.isHealthy,l=n((c,g)=>{const y=r.cohorts[c];if(!y||y.urls.length===0)return;const P=y.urls.length;for(let m=0;m<P;m++){r.rr[c]=(r.rr[c]+1)%P;const k=y.urls[r.rr[c]];if(!g||g(k))return k}},"pickUrl"),v=e?.version?r.cohorts.findIndex(c=>c.version===e.version&&c.urls.length>0):-1;if(v>=0){const c=l(v,u)??l(v);return c!==void 0?{url:c,version:r.cohorts[v].version}:void 0}let d=r.select();(!r.cohorts[d]||r.cohorts[d].urls.length===0)&&(d=i(r));let h=l(d,u);if(h===void 0&&u)for(let c=0;c<r.cohorts.length;c++){const g=l(c,u);if(g!==void 0){d=c,h=g;break}}return h===void 0&&(h=l(d)),h!==void 0?{url:h,version:r.cohorts[d].version}:void 0},endpoints(){const t=new Set;for(const e of o.values())for(const r of e.cohorts)for(const u of r.urls)t.add(u);return[...t]},resolveVersion(t,e){const r=o.get(t);return r&&r.rules.length?(0,manifest_1.pickTargetedVersion)(r.rules,e):void 0},bucket(t,e){const r=o.get(t);if(!r||r.cohorts.length===0)return;const u=r.cohorts[(0,manifest_1.bucketByWeight)(`${e}:${t}`,r.cohorts.map(l=>l.weight))];return u?{version:u.version}:void 0},ensureUp:n(async()=>!0,"ensureUp"),drop(t,e){const r=o.get(t);if(r&&"url"in e)for(const u of r.cohorts)u.urls=u.urls.filter(l=>l!==e.url)},update:f}}p(rolloutPodProvider,"rolloutPodProvider"),n(rolloutPodProvider,"rolloutPodProvider");function endpointsPodProvider(s){const o=n(i=>Object.fromEntries(Object.entries(i).map(([t,e])=>[t,[{version:"_flat",weight:1,urls:e}]])),"toCohorts"),a=rolloutPodProvider(o(s)),f=n(i=>i&&"url"in i?{url:i.url}:i,"unversion");return{...a,pick:n((i,t)=>f(a.pick(i,t)),"pick"),resolveVersion:n(()=>{},"resolveVersion"),bucket:n(()=>{},"bucket"),update:n(i=>a.update(o(i)),"update")}}p(endpointsPodProvider,"endpointsPodProvider"),n(endpointsPodProvider,"endpointsPodProvider");
|
|
@@ -70,4 +70,3 @@ export interface RateLimitRedis {
|
|
|
70
70
|
eval(script: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
|
|
71
71
|
}
|
|
72
72
|
export declare function createRedisRateLimiter(client: RateLimitRedis, opts: RedisRateLimiterOptions): RateLimiter;
|
|
73
|
-
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -1,45 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createRateLimiter = createRateLimiter;
|
|
4
|
-
exports.createRedisRateLimiter = createRedisRateLimiter;
|
|
5
|
-
function createRateLimiter(opts) {
|
|
6
|
-
const limit = Math.max(1, opts.limit);
|
|
7
|
-
const windowMs = Math.max(1, opts.windowMs);
|
|
8
|
-
const now = opts.now ?? Date.now;
|
|
9
|
-
const refillPerMs = limit / windowMs;
|
|
10
|
-
const buckets = new Map();
|
|
11
|
-
const refilled = (b, t) => Math.min(limit, b.tokens + (t - b.last) * refillPerMs);
|
|
12
|
-
// Evict buckets that have refilled to full (idle) so the map stays bounded.
|
|
13
|
-
const sweep = setInterval(() => {
|
|
14
|
-
const t = now();
|
|
15
|
-
for (const [k, b] of buckets)
|
|
16
|
-
if (refilled(b, t) >= limit)
|
|
17
|
-
buckets.delete(k);
|
|
18
|
-
}, windowMs);
|
|
19
|
-
sweep.unref?.();
|
|
20
|
-
return {
|
|
21
|
-
check(key) {
|
|
22
|
-
const t = now();
|
|
23
|
-
let b = buckets.get(key);
|
|
24
|
-
if (!b) {
|
|
25
|
-
b = { tokens: limit, last: t };
|
|
26
|
-
buckets.set(key, b);
|
|
27
|
-
}
|
|
28
|
-
b.tokens = refilled(b, t);
|
|
29
|
-
b.last = t;
|
|
30
|
-
if (b.tokens >= 1) {
|
|
31
|
-
b.tokens -= 1;
|
|
32
|
-
return { allowed: true, remaining: Math.floor(b.tokens), retryAfterMs: 0 };
|
|
33
|
-
}
|
|
34
|
-
return { allowed: false, remaining: 0, retryAfterMs: Math.ceil((1 - b.tokens) / refillPerMs) };
|
|
35
|
-
},
|
|
36
|
-
stop() { clearInterval(sweep); },
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
// Atomic token bucket in one round-trip. Stores fractional tokens (Redis keeps the
|
|
40
|
-
// string form) so refill is exact across calls; only the returned remaining/retry
|
|
41
|
-
// are integer-coerced by the Lua→RESP boundary, which is fine.
|
|
42
|
-
const BUCKET_LUA = `
|
|
1
|
+
"use strict";var A=Object.defineProperty;var u=(r,e)=>A(r,"name",{value:e,configurable:!0});var R=Object.defineProperty,a=u((r,e)=>R(r,"name",{value:e,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRateLimiter=createRateLimiter,exports.createRedisRateLimiter=createRedisRateLimiter;function createRateLimiter(r){const e=Math.max(1,r.limit),i=Math.max(1,r.windowMs),n=r.now??Date.now,c=e/i,s=new Map,m=a((l,o)=>Math.min(e,l.tokens+(o-l.last)*c),"refilled"),f=setInterval(()=>{const l=n();for(const[o,t]of s)m(t,l)>=e&&s.delete(o)},i);return f.unref?.(),{check(l){const o=n();let t=s.get(l);return t||(t={tokens:e,last:o},s.set(l,t)),t.tokens=m(t,o),t.last=o,t.tokens>=1?(t.tokens-=1,{allowed:!0,remaining:Math.floor(t.tokens),retryAfterMs:0}):{allowed:!1,remaining:0,retryAfterMs:Math.ceil((1-t.tokens)/c)}},stop(){clearInterval(f)}}}u(createRateLimiter,"createRateLimiter"),a(createRateLimiter,"createRateLimiter");const BUCKET_LUA=`
|
|
43
2
|
local b = redis.call('HMGET', KEYS[1], 't', 's')
|
|
44
3
|
local cap = tonumber(ARGV[1])
|
|
45
4
|
local refill = tonumber(ARGV[2])
|
|
@@ -56,36 +15,4 @@ redis.call('HSET', KEYS[1], 't', tokens, 's', now)
|
|
|
56
15
|
redis.call('PEXPIRE', KEYS[1], ttl)
|
|
57
16
|
local retry = 0
|
|
58
17
|
if ok == 0 then retry = math.ceil((1 - tokens) / refill) end
|
|
59
|
-
return {ok, math.floor(tokens), retry}
|
|
60
|
-
const RL_TIMEOUT = Symbol('rl-timeout');
|
|
61
|
-
const RL_FAIL = Symbol('rl-fail');
|
|
62
|
-
function createRedisRateLimiter(client, opts) {
|
|
63
|
-
const limit = Math.max(1, opts.limit);
|
|
64
|
-
const windowMs = Math.max(1, opts.windowMs);
|
|
65
|
-
const prefix = opts.prefix ?? 'lensmcp:gw:rl:';
|
|
66
|
-
const timeoutMs = Math.max(1, opts.timeoutMs ?? 100);
|
|
67
|
-
const failOpen = opts.failOpen !== false;
|
|
68
|
-
const refillPerMs = limit / windowMs;
|
|
69
|
-
const ttlMs = Math.ceil(windowMs) + 1000; // evict idle buckets shortly after a full refill
|
|
70
|
-
const onFail = () => failOpen
|
|
71
|
-
? { allowed: true, remaining: limit, retryAfterMs: 0 }
|
|
72
|
-
: { allowed: false, remaining: 0, retryAfterMs: windowMs };
|
|
73
|
-
return {
|
|
74
|
-
async check(key) {
|
|
75
|
-
let timer;
|
|
76
|
-
// The eval branch is mapped so it can never reject (→ RL_FAIL) — that keeps a
|
|
77
|
-
// late rejection (after a timeout already won the race) from going unhandled.
|
|
78
|
-
const evalP = Promise.resolve(client.eval(BUCKET_LUA, 1, `${prefix}${key}`, limit, refillPerMs, ttlMs)).then((v) => v, () => RL_FAIL);
|
|
79
|
-
const timeoutP = new Promise((res) => { timer = setTimeout(() => res(RL_TIMEOUT), timeoutMs); });
|
|
80
|
-
const res = await Promise.race([evalP, timeoutP]);
|
|
81
|
-
clearTimeout(timer);
|
|
82
|
-
if (res === RL_TIMEOUT || res === RL_FAIL)
|
|
83
|
-
return onFail();
|
|
84
|
-
const [ok, remaining, retry] = res;
|
|
85
|
-
return ok === 1
|
|
86
|
-
? { allowed: true, remaining: Number(remaining) || 0, retryAfterMs: 0 }
|
|
87
|
-
: { allowed: false, remaining: 0, retryAfterMs: Number(retry) || windowMs };
|
|
88
|
-
},
|
|
89
|
-
stop() { },
|
|
90
|
-
};
|
|
91
|
-
}
|
|
18
|
+
return {ok, math.floor(tokens), retry}`,RL_TIMEOUT=Symbol("rl-timeout"),RL_FAIL=Symbol("rl-fail");function createRedisRateLimiter(r,e){const i=Math.max(1,e.limit),n=Math.max(1,e.windowMs),c=e.prefix??"lensmcp:gw:rl:",s=Math.max(1,e.timeoutMs??100),m=e.failOpen!==!1,f=i/n,l=Math.ceil(n)+1e3,o=a(()=>m?{allowed:!0,remaining:i,retryAfterMs:0}:{allowed:!1,remaining:0,retryAfterMs:n},"onFail");return{async check(t){let M;const w=Promise.resolve(r.eval(BUCKET_LUA,1,`${c}${t}`,i,f,l)).then(d=>d,()=>RL_FAIL),h=new Promise(d=>{M=setTimeout(()=>d(RL_TIMEOUT),s)}),k=await Promise.race([w,h]);if(clearTimeout(M),k===RL_TIMEOUT||k===RL_FAIL)return o();const[b,y,p]=k;return b===1?{allowed:!0,remaining:Number(y)||0,retryAfterMs:0}:{allowed:!1,remaining:0,retryAfterMs:Number(p)||n}},stop(){}}}u(createRedisRateLimiter,"createRedisRateLimiter"),a(createRedisRateLimiter,"createRedisRateLimiter");
|