@lensmcp/cluster 1.21.3 → 1.21.4
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/create-webpack-dev.d.ts +45 -0
- package/create-webpack-dev.js +1 -1
- package/executors/gateway/body-stall.d.ts +37 -0
- package/executors/gateway/body-stall.js +1 -0
- package/executors/gateway/canonical-path.d.ts +65 -0
- package/executors/gateway/canonical-path.js +1 -0
- package/executors/gateway/client-ip.d.ts +44 -0
- package/executors/gateway/client-ip.js +1 -0
- package/executors/gateway/jwks-verify.js +1 -1
- package/executors/gateway/main.prod-gateway.js +2 -2
- package/executors/gateway/manifest.d.ts +27 -3
- package/executors/gateway/manifest.js +1 -1
- package/executors/gateway/prod-config.d.ts +74 -0
- package/executors/gateway/prod-config.js +1 -0
- package/executors/gateway/prod-runtime/access-log.js +1 -1
- package/executors/gateway/prod-runtime/app.d.ts +6 -0
- package/executors/gateway/prod-runtime/app.js +1 -1
- package/executors/gateway/prod-runtime/auth.d.ts +1 -1
- package/executors/gateway/prod-runtime/auth.js +1 -1
- package/executors/gateway/prod-runtime/body-stall.d.ts +5 -0
- package/executors/gateway/prod-runtime/body-stall.js +1 -0
- package/executors/gateway/prod-runtime/cors.js +1 -1
- package/executors/gateway/prod-runtime/edge.d.ts +19 -3
- package/executors/gateway/prod-runtime/edge.js +1 -1
- package/executors/gateway/prod-runtime/handler.js +1 -1
- package/executors/gateway/prod-runtime/rollout.d.ts +11 -4
- package/executors/gateway/prod-runtime/rollout.js +1 -1
- package/executors/gateway/prod-runtime/routing.js +1 -1
- package/executors/gateway/prod-runtime/server.js +1 -1
- package/executors/gateway/prod-runtime/types.d.ts +30 -3
- package/executors/gateway/prod-runtime/upgrade.d.ts +25 -0
- package/executors/gateway/prod-runtime/upgrade.js +10 -2
- package/executors/gateway/prod-runtime/upstream.d.ts +3 -1
- package/executors/gateway/prod-runtime/upstream.js +1 -1
- package/executors/gateway/providers-prod.d.ts +13 -4
- package/executors/gateway/providers-prod.js +1 -1
- package/executors/gateway/registry-source.js +1 -1
- package/executors/gateway/rollout-ops.js +2 -2
- package/executors/gateway/runtime/auth.d.ts +6 -1
- package/executors/gateway/runtime/auth.js +1 -1
- package/executors/gateway/runtime/control.js +2 -1
- package/executors/gateway/runtime/dev-auth.d.ts +6 -2
- package/executors/gateway/runtime/dev-auth.js +1 -1
- package/executors/gateway/runtime/discovery.js +1 -1
- package/executors/gateway/runtime/edge.d.ts +19 -0
- package/executors/gateway/runtime/edge.js +1 -1
- package/executors/gateway/runtime/handler.js +1 -1
- package/executors/gateway/runtime/lens-children.d.ts +7 -0
- package/executors/gateway/runtime/lens-children.js +1 -1
- package/executors/gateway/runtime/lifecycle.d.ts +69 -7
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/pod-probe.d.ts +37 -0
- package/executors/gateway/runtime/pod-probe.js +1 -0
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/scope.d.ts +15 -0
- package/executors/gateway/runtime/scope.js +1 -1
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/service-keys.d.ts +42 -1
- package/executors/gateway/runtime/service-keys.js +1 -1
- package/executors/gateway/runtime/types.d.ts +193 -1
- package/executors/gateway/runtime/types.js +1 -1
- package/executors/gateway/runtime/upgrade.js +1 -1
- package/executors/gateway/runtime/workspace-log.d.ts +7 -0
- package/executors/gateway/runtime/workspace-log.js +1 -0
- package/main.devserver.js +7 -7
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var x=Object.defineProperty;var h=(c,a)=>x(c,"name",{value:a,configurable:!0});var C=Object.defineProperty,u=h((c,a)=>C(c,"name",{value:a,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createApp=createApp;const tslib_1=require("tslib"),fastify_1=tslib_1.__importDefault(require("fastify")),reply_from_1=tslib_1.__importDefault(require("@fastify/reply-from")),edge_1=require("./edge"),trust_1=require("./trust"),body_stall_1=require("./body-stall"),access_log_1=require("./access-log");function createApp(c){const{rt:a,obs:l,cors:k,hooks:y,handler:v,upgrade:_,reqState:m,spanByReq:g,ctxByReq:T}=c,e=a.opts;return{buildApp:u(async()=>{const b=e.server?.keepAliveTimeout??62e4,s=(0,fastify_1.default)({logger:!1,keepAliveTimeout:b,requestTimeout:e.server?.requestTimeout??0,...e.server?.connectionTimeout!==void 0?{connectionTimeout:e.server.connectionTimeout}:{},...e.trustProxyIp?{trustProxy:Math.max(1,Math.floor(e.trustProxyHops??1))}:{},...e.tls?{https:{key:e.tls.key,cert:e.tls.cert}}:{}});s.server.headersTimeout=e.server?.headersTimeout??6e4,e.server?.connectionsCheckingInterval!==void 0&&(s.server.connectionsCheckingInterval=e.server.connectionsCheckingInterval),e.server?.maxHeadersCount!==void 0&&(s.server.maxHeadersCount=e.server.maxHeadersCount),e.server?.maxConnections!==void 0&&(s.server.maxConnections=e.server.maxConnections),s.removeAllContentTypeParsers(),s.addContentTypeParser("*",(r,t,n)=>n(null,t)),await s.register(reply_from_1.default,{disableRequestLogging:!0,undici:{connections:e.undici?.connections??256,pipelining:e.undici?.pipelining??1,keepAliveTimeout:e.undici?.keepAliveTimeout??6e4,bodyTimeout:e.undici?.bodyTimeout??0,headersTimeout:e.undici?.headersTimeout??6e4,...e.undici?.allowH2?{allowH2:!0}:{},connect:{rejectUnauthorized:e.undici?.rejectUnauthorized??!1,...e.undici?.cert?{cert:e.undici.cert}:{},...e.undici?.key?{key:e.undici.key}:{},...e.undici?.ca?{ca:e.undici.ca}:{}}}}),s.get("/livez",(r,t)=>{t.header("cache-control",edge_1.NO_STORE).type("text/plain").send("ok")}),s.get("/readyz",(r,t)=>{const n=a.table.routes.length>0;t.code(n?200:503).header("cache-control",edge_1.NO_STORE).type("text/plain").send(n?"ready":"no routes loaded")});const f=u(r=>(0,edge_1.tokenMatches)(r.query?.token??(0,edge_1.hdr)(r.raw.headers["x-status-token"]),e.statusToken),"opToken");s.get("/statusz",(r,t)=>{if(!e.statusEndpoint||!e.statusToken){t.code(404).header("cache-control",edge_1.NO_STORE).type("text/plain").send("not found");return}if(!f(r)){t.code(401).header("cache-control",edge_1.NO_STORE).type("text/plain").send("unauthorized");return}t.header("cache-control",edge_1.NO_STORE).type("application/json").send(JSON.stringify({generatedAt:Date.now(),services:l.buildStatusSnapshot(),endpoints:e.healthChecker?.snapshot()??[]}))}),s.get("/metricsz",(r,t)=>{if(!e.metrics||!e.statusToken){t.code(404).header("cache-control",edge_1.NO_STORE).type("text/plain").send("not found");return}if(!f(r)){t.code(401).header("cache-control",edge_1.NO_STORE).type("text/plain").send("unauthorized");return}t.header("cache-control",edge_1.NO_STORE).type("application/json").send(JSON.stringify(e.metrics.snapshot()))});const p=(0,body_stall_1.createBodyStallGuard)(e.server?.bodyStallMs??6e4);return s.addHook("onClose",(r,t)=>{p.stop(),t()}),s.addHook("onRequest",(r,t,n)=>{(0,trust_1.stripTrust)(r.raw),p.arm(r.raw),p.armResponse(t.raw),n()}),s.addHook("onSend",(r,t,n,o)=>{k.applyResponseCors(r,t),o(null,n)}),s.addHook("onResponse",(r,t,n)=>{const o=m.get(r);o&&(l.recordEdge(o.route,Date.now()-o.startedAt,t.statusCode??0,o.caller,o.version),l.finish(o.trace,o.route.service,t.statusCode??0,{upstream:o.upLabel,...o.version?{version:o.version}:{},...o.caller?{caller:o.caller}:{}}),m.delete(r));const d=g.get(r);d&&(e.tracing.end(d,t.statusCode??0,{...o?.upLabel?{"upstream.url":o.upLabel}:{},...o?.version?{"lensmcp.version":o.version}:{}}),g.delete(r)),e.metrics?.recordRequest(t.statusCode??0),a.accessLog&&(0,access_log_1.logAccess)(r,t,o);const i=T.get(r);i&&(i.status=t.statusCode??0,y.has("onResponse")&&y.run("onResponse",i),T.delete(r)),n()}),s.setErrorHandler((r,t,n)=>{const o=r,d=o.message??String(r);l.emit("error",`gateway request error: ${d}`,`gateway-error:${o.code??"exception"}`,{kind:"gateway-request-error",error:d,...o.code?{code:o.code}:{}});const i=typeof o.statusCode=="number"&&o.statusCode>=400&&o.statusCode<600?o.statusCode:500;(0,edge_1.sendError)(n,t.raw,i,i===400?"bad_request":"internal")}),s.setNotFoundHandler((r,t)=>{(0,edge_1.sendError)(t,r.raw,404,"not_found","No gateway route for this hostname.")}),s.all("/",v),s.all("/*",v),s.server.on("upgrade",_),s},"buildApp")}}h(createApp,"createApp"),u(createApp,"createApp");
|
|
@@ -24,6 +24,6 @@ export interface AuthVerdict {
|
|
|
24
24
|
}
|
|
25
25
|
export interface EdgeAuth {
|
|
26
26
|
internal(req: http.IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
|
|
27
|
-
public(req: http.IncomingMessage, route: Route, cpath: string, claims: Record<string, unknown> | undefined,
|
|
27
|
+
public(req: http.IncomingMessage, route: Route, cpath: string, claims: Record<string, unknown> | undefined, clientIp: string, trace: GwTrace | undefined): AuthVerdict;
|
|
28
28
|
}
|
|
29
29
|
export declare function createEdgeAuth(rt: ProdRuntime, obs: Observability, rollout: RolloutLayer, authenticate: (mode: AuthMode, req: http.IncomingMessage) => void): EdgeAuth;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var f=Object.defineProperty;var
|
|
1
|
+
"use strict";var f=Object.defineProperty;var l=(i,e)=>f(i,"name",{value:e,configurable:!0});var k=Object.defineProperty,o=l((i,e)=>k(i,"name",{value:e,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),trust_1=require("./trust");function createEdgeAuth(i,e,p,c){return{internal:o((r,d,s)=>{const t=r.headers["x-api-key"],u=typeof t=="string"?t:Array.isArray(t)?t[0]:void 0,a=u?i.keyToProject.get(u):void 0;return a?(r.headers["x-api-key-id"]=a,delete r.headers["x-api-key"],e.step(s,"auth",{mode:"x-api-key",ok:!0,caller:a}),{ok:!0,caller:a}):(e.step(s,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."})},"internal"),public:o((r,d,s,t,u,a)=>{const h=i.table.manifestByService.get(d.service),n=h?(0,manifest_1.authRuleForPath)(h,s,r.method??"GET",!1):{mode:d.auth};if(n.mode==="jwt"){if(i.opts.identify){if(!t)return e.step(a,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"}}else try{c("jwt",r)}catch(m){return e.step(a,"auth",{mode:"jwt",ok:!1,reason:m.message}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"}}return n.rule&&(!t||!(0,manifest_1.evalRule)(n.rule,p.buildAttrs(r,u,t,void 0)))?(e.step(a,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):(e.step(a,"auth",{mode:"jwt",ok:!0}),t&&(0,trust_1.stampIdentity)(r,t),{ok:!0})}return e.step(a,"auth",{mode:n.mode}),{ok:!0}},"publicAuth")}}l(createEdgeAuth,"createEdgeAuth"),o(createEdgeAuth,"createEdgeAuth");
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stall guard lives in `../body-stall` — SHARED with the dev gateway (issues/dev-gateway/12) so the
|
|
3
|
+
* two proxies bound a stalled transfer the same way. Re-exported so every prod import keeps its address.
|
|
4
|
+
*/
|
|
5
|
+
export { createBodyStallGuard, type BodyStallGuard } from '../body-stall';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var o=Object.defineProperty;var l=(r,t)=>o(r,"name",{value:t,configurable:!0});var a=Object.defineProperty,e=l((r,t)=>a(r,"name",{value:t,configurable:!0}),"e");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createBodyStallGuard=void 0;var body_stall_1=require("../body-stall");Object.defineProperty(exports,"createBodyStallGuard",{enumerable:!0,get:e(function(){return body_stall_1.createBodyStallGuard},"get")});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var i=Object.defineProperty;var n=(s,a)=>i(s,"name",{value:a,configurable:!0});var c=Object.defineProperty,t=n((s,a)=>c(s,"name",{value:a,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createCors=createCors;const addVaryOrigin=t(s=>{const a=s.getHeader("vary"),e=Array.isArray(a)?a.join(", "):typeof a=="string"?a:"";if(e.trim()==="*")return;const o=e.split(",").map(r=>r.trim()).filter(Boolean);o.some(r=>r.toLowerCase()==="origin")||s.header("vary",[...o,"Origin"].join(", "))},"addVaryOrigin");function createCors(s,a){return{setCors:t((e,o)=>{const r=o.headers.origin;if(r&&s.corsCredentialOrigins.has(r.toLowerCase())){e.header("access-control-allow-origin",r),e.header("access-control-allow-credentials","true"),addVaryOrigin(e);return}e.header("access-control-allow-origin",String(r??"*")),addVaryOrigin(e)},"setCors"),applyResponseCors:t((e,o)=>{const r=e.headers.origin;r&&!o.getHeader("access-control-allow-origin")&&(s.corsCredentialOrigins.has(r.toLowerCase())?(o.header("access-control-allow-origin",r),o.header("access-control-allow-credentials","true"),addVaryOrigin(o)):a.get(e)?.proxied||(o.header("access-control-allow-origin",String(r)),addVaryOrigin(o)))},"applyResponseCors")}}n(createCors,"createCors"),t(createCors,"createCors");
|
|
@@ -10,6 +10,22 @@ export declare const hdr: (v: string | string[] | undefined) => string | undefin
|
|
|
10
10
|
* `detail` is the dev-facing hint (the old plain-text reason), never shown to end users.
|
|
11
11
|
*/
|
|
12
12
|
export declare const sendError: (reply: ReplyLike, req: http.IncomingMessage, status: number, reason: GatewayErrorReason, detail?: string) => ReplyLike;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/** Cache directive for everything the gateway answers itself (errors, probes, discovery). */
|
|
14
|
+
export declare const NO_STORE = "no-store";
|
|
15
|
+
/**
|
|
16
|
+
* Constant-time compare of a presented operator token against the configured one. Shared by
|
|
17
|
+
* `/statusz`, `/metricsz` and the route-discovery view so all three fail closed identically:
|
|
18
|
+
* no token configured ⇒ never a match, whatever the caller sends.
|
|
19
|
+
*/
|
|
20
|
+
export declare const tokenMatches: (presented: string | undefined, expected: string | undefined) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Append a token to a raw response `Vary` header without destroying what the upstream set.
|
|
23
|
+
* `Vary: *` already varies on everything; an existing token is not duplicated.
|
|
24
|
+
*/
|
|
25
|
+
export declare const addVary: (headers: Record<string, unknown>, token: string) => void;
|
|
26
|
+
export { ENCODED_STRUCTURE, canonicalize, canonicalPath, RAW_TARGET, rawTargetOf, rememberRawTarget, type CanonicalTarget, } from '../canonical-path';
|
|
27
|
+
/** Render an operator-supplied URL for a LOG: origin + path only. Vendor OTLP endpoints
|
|
28
|
+
* routinely carry the token in userinfo (`https://<token>@otlp.vendor.io/v1/traces`) or a
|
|
29
|
+
* query string, and boot logs are widely readable precisely because they are not expected
|
|
30
|
+
* to hold secrets. */
|
|
31
|
+
export declare const safeUrl: (raw: string | undefined) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var p=Object.defineProperty;var i=(e,r)=>p(e,"name",{value:r,configurable:!0});var c=Object.defineProperty,t=i((e,r)=>c(e,"name",{value:r,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.safeUrl=exports.rememberRawTarget=exports.rawTargetOf=exports.RAW_TARGET=exports.canonicalPath=exports.canonicalize=exports.ENCODED_STRUCTURE=exports.addVary=exports.tokenMatches=exports.NO_STORE=exports.sendError=exports.hdr=exports.eid=void 0;const node_crypto_1=require("node:crypto"),gateway_errors_1=require("../gateway-errors"),eid=t(()=>Date.now().toString(36)+(0,node_crypto_1.randomBytes)(6).toString("hex"),"eid");exports.eid=eid;const hdr=t(e=>typeof e=="string"&&e!==""?e:Array.isArray(e)?e[0]:void 0,"hdr");exports.hdr=hdr;const sendError=t((e,r,o,n,s)=>{const a=(0,gateway_errors_1.ensureRequestId)(r);return e.code(o).header("x-request-id",a).header("cache-control",exports.NO_STORE).type(gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE).send((0,gateway_errors_1.gatewayErrorBody)(o,n,a,s))},"sendError");exports.sendError=sendError,exports.NO_STORE="no-store";const tokenMatches=t((e,r)=>{if(!r)return!1;const o=Buffer.from(e??""),n=Buffer.from(r);return o.length===n.length&&(0,node_crypto_1.timingSafeEqual)(o,n)},"tokenMatches");exports.tokenMatches=tokenMatches;const addVary=t((e,r)=>{const o=e.vary,n=Array.isArray(o)?o.join(", "):typeof o=="string"?o:"";if(n.trim()==="*")return;const s=n.split(",").map(a=>a.trim()).filter(Boolean);s.some(a=>a.toLowerCase()===r.toLowerCase())||(e.vary=[...s,r].join(", "))},"addVary");exports.addVary=addVary;var canonical_path_1=require("../canonical-path");Object.defineProperty(exports,"ENCODED_STRUCTURE",{enumerable:!0,get:t(function(){return canonical_path_1.ENCODED_STRUCTURE},"get")}),Object.defineProperty(exports,"canonicalize",{enumerable:!0,get:t(function(){return canonical_path_1.canonicalize},"get")}),Object.defineProperty(exports,"canonicalPath",{enumerable:!0,get:t(function(){return canonical_path_1.canonicalPath},"get")}),Object.defineProperty(exports,"RAW_TARGET",{enumerable:!0,get:t(function(){return canonical_path_1.RAW_TARGET},"get")}),Object.defineProperty(exports,"rawTargetOf",{enumerable:!0,get:t(function(){return canonical_path_1.rawTargetOf},"get")}),Object.defineProperty(exports,"rememberRawTarget",{enumerable:!0,get:t(function(){return canonical_path_1.rememberRawTarget},"get")});const safeUrl=t(e=>{if(!e)return"(unset)";try{const r=new URL(e);return`${r.protocol}//${r.host}${r.pathname}`}catch{return"(set)"}},"safeUrl");exports.safeUrl=safeUrl;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var J=Object.defineProperty;var H=(y,n)=>J(y,"name",{value:n,configurable:!0});var G=Object.defineProperty,v=H((y,n)=>G(y,"name",{value:n,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(y){const{rt:n,obs:d,cors:_,auth:N,upstream:L,rollout:w,hooks:p,reqState:U,spanByReq:$,ctxByReq:D}=y,M=!!n.opts.pods?.resolveVersion,q=!!n.opts.cookie?.secret,C=!!n.opts.pods?.bucket&&(q||!!n.opts.uidHeader),V=M||q||C,B=n.opts.upstreamRetries??1,F=v(l=>(0,edge_1.tokenMatches)(l.query?.token??(0,edge_1.hdr)(l.raw.headers["x-status-token"]),n.opts.statusToken),"statusTokenMatches"),K=v((l,o,e,f,m)=>{const a=l.raw;if(_.setCors(o,a),o.header("cache-control",edge_1.NO_STORE),a.method==="OPTIONS"){o.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(a.headers["access-control-request-headers"]??"*")).send();return}const s=n.table.routes.filter(i=>i.host&&(0,manifest_1.hostMatches)(i.host,e)&&i.prefix),h=m;if(s.length>0&&(h==="/health"||h==="/healthz")){o.code(200).type("application/json").send(JSON.stringify({status:"ok",host:e,services:s.map(i=>({service:i.service,prefix:i.prefix}))}));return}if(h==="/.well-known/lensmcp-routes"){if(!F(l)){(0,edge_1.sendError)(o,a,404,"not_found","No gateway route for this hostname.");return}o.code(200).type("application/json").send(JSON.stringify({host:e,routes:n.table.routes.filter(i=>!i.host||(0,manifest_1.hostMatches)(i.host,e)).map(i=>({host:i.host??"(default)",service:i.service,prefix:i.prefix,internal:!!i.internal,auth:i.auth}))}));return}(0,edge_1.sendError)(o,a,404,"not_found",s.length>0?`No service mounted at ${f} on ${e}.`:"No gateway route for this hostname.")},"handleNoRoute");return async(l,o)=>{const e=l.raw,f=(e.headers.host||"").split(":")[0],m=e.url??"/",a=d.beginTrace(e);(0,gateway_errors_1.ensureRequestId)(e),d.step(a,"received",{host:f,method:e.method,url:m});const s=p.any?p.httpContext(e,l,o,a):void 0;if(s&&D.set(l,s),s&&p.has("onRequest")&&(await p.run("onRequest",s),s.responded()))return o;if(e.httpVersionMajor<1)return d.finish(a,"gateway",400),(0,edge_1.sendError)(o,e,400,"bad_request","Bad Request");const h=(0,edge_1.canonicalize)(m);if(h===void 0)return d.step(a,"bad-path",{}),d.finish(a,"gateway",400),(0,edge_1.sendError)(o,e,400,"bad_request","Bad Request");const i=h.policy,b=h.path+h.query;b!==m&&(0,edge_1.rememberRawTarget)(e,m),e.url=b;const j=e.socket?.remoteAddress??l.ip,T=w.clientIpOf(e,j);if(n.opts.rateLimiter){const r=await n.opts.rateLimiter.check(T);if(!r.allowed)return o.header("retry-after",String(Math.ceil(r.retryAfterMs/1e3))),d.step(a,"rate-limited",{}),d.finish(a,"gateway",429),(0,edge_1.sendError)(o,e,429,"rate_limited","Too Many Requests")}const t=(0,manifest_1.matchRoute)(n.table.routes,f,i+h.query);if(!t)return K(l,o,f,b,i),o;if(s&&(s.route=t),d.step(a,"route-match",{project:t.service,...t.prefix?{prefix:t.prefix}:{},...t.internal?{internal:!0}:{}}),e.method==="OPTIONS")return _.setCors(o,e),o.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(e.headers["access-control-request-headers"]??"*")).send(),o;const k=n.opts.tracing?.startSpan(e,`gateway ${t.service}`,{"http.request.method":e.method??"GET","url.path":b.split("?")[0],"server.address":f,"lensmcp.service":t.service,...t.internal?{"lensmcp.internal":!0}:{}});if(k&&$.set(l,k),s&&p.has("beforeAuth")&&(await p.run("beforeAuth",s),s.responded()))return o;const O=t.internal?void 0:n.opts.identify?.(e);let S;if(t.internal){const r=N.internal(e,t,a);if(!r.ok)return d.finish(a,t.service,r.status),(0,edge_1.sendError)(o,e,r.status,r.reason,r.detail);S=r.caller,s&&(s.caller=S)}else{const r=N.public(e,t,i,O,T,a);if(!r.ok)return d.finish(a,t.service,r.status),(0,edge_1.sendError)(o,e,r.status,r.reason,r.detail);if(s&&(s.claims=O),s&&p.has("afterAuth")&&(await p.run("afterAuth",s),s.responded()))return o}if(e.headers["x-forwarded-for"]=w.forwardedFor(e,j),e.headers["x-forwarded-proto"]=n.opts.tls?"https":"http",e.headers["x-forwarded-host"]=e.headers.host??f,n.opts.serviceKeys?.[t.service]&&(e.headers["x-internal-token"]=n.opts.serviceKeys[t.service]),t.prependPrefix){const r=e.url??"/",c=t.prependPrefix;r!==c&&!r.startsWith(c+"/")&&(e.url=c+r)}const x=(0,edge_1.hdr)(e.headers[n.versionHeader]);let P=x,R=!1,E;if(V&&!t.internal&&!x){const r=w.resolveStickyKey(e);r.mint&&(E=r.key);const c=M?n.opts.pods.resolveVersion(t.service,w.buildAttrs(e,T,O,r.key)):void 0;if(c)P=c.version,R=!0,d.step(a,"targeting",{rule:c.rule,version:c.version});else if(r.key){const g=n.opts.pods?.bucket?.(t.service,r.key);g&&(P=g.version,R=!0,d.step(a,"targeting",{mode:"sticky",version:g.version}))}}const u=await L.resolveUpstream(t,a,P,R);if(!u)return d.finish(a,t.service,503),(0,edge_1.sendError)(o,e,503,"unavailable",`Service ${t.service} has no available upstream.`);if(!("url"in u))return d.finish(a,t.service,502),(0,edge_1.sendError)(o,e,502,"unavailable",`Service ${t.service}: socket upstreams are not supported by the prod gateway.`);const z=new URL(u.url).origin,A=u.url,I={route:t,caller:S,startedAt:Date.now(),trace:a,version:u.version,upLabel:A};return U.set(l,I),s&&(s.version=u.version),d.step(a,u.version?"rollout":"upstream",{upstream:A,...u.version?{version:u.version,...x?{pinned:!0}:{}}:{}}),k&&n.opts.tracing.inject(k,e.headers),s&&p.has("beforeForward")&&(await p.run("beforeForward",s),s.responded())?o:o.from(void 0,{getUpstream:v(()=>z,"getUpstream"),retriesCount:B,rewriteHeaders:v(r=>{if(I.proxied=!0,a&&(r["x-lensmcp-upstream"]=A),r["x-request-id"]||(r["x-request-id"]=(0,gateway_errors_1.ensureRequestId)(e)),u.version&&(r["x-lensmcp-version"]=u.version),(x||u.version)&&(0,edge_1.addVary)(r,n.versionHeader),(E||C&&q)&&(0,edge_1.addVary)(r,"Cookie"),delete r["x-powered-by"],delete r.connection,delete r["keep-alive"],delete r["proxy-connection"],r.server="LensMCP",E){const c=r["set-cookie"],g=Array.isArray(c)?c:c?[c]:[];r["set-cookie"]=[...g,w.mintCookieHeader(E)]}return r},"rewriteHeaders"),onError:v((r,{error:c})=>{n.opts.pods?.drop?.(t.service,u),s&&p.has("onError")&&(s.error=c,p.run("onError",s)),(0,edge_1.sendError)(r,e,502,"unavailable",`Upstream ${t.service} unavailable.`)},"onError")})}}H(createHandler,"createHandler"),v(createHandler,"createHandler");
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type * as http from 'node:http';
|
|
2
|
-
import {
|
|
2
|
+
import type { Attrs } from '../manifest';
|
|
3
3
|
import type { ProdGatewayOptions } from './types';
|
|
4
4
|
export interface RolloutLayer {
|
|
5
|
-
/** Resolve the client IP: the trusted front-proxy header if the peer is trusted, else the
|
|
5
|
+
/** Resolve the client IP: the trusted front-proxy header if the peer is trusted, else the
|
|
6
|
+
* configured number of trusted `X-Forwarded-For` hops, else the socket address. */
|
|
6
7
|
clientIpOf(req: http.IncomingMessage, fallback: string): string;
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
/** The `X-Forwarded-For` value to send upstream: the trusted chain plus this hop, or just
|
|
9
|
+
* this hop when no proxy is trusted (the client's own chain is then fiction). */
|
|
10
|
+
forwardedFor(req: http.IncomingMessage, peer: string): string;
|
|
11
|
+
/** Assemble the flat attribute bag a rule evaluates against. `clientIp` is the address the
|
|
12
|
+
* caller ALREADY resolved with {@link clientIpOf} — re-resolving here would re-read
|
|
13
|
+
* `X-Forwarded-For`, which the gateway rewrites before forwarding, so a rule would see the
|
|
14
|
+
* gateway's own appended hop instead of the client. */
|
|
15
|
+
buildAttrs(req: http.IncomingMessage, clientIp: string, principal: Record<string, unknown> | undefined, key: string | undefined): Attrs;
|
|
9
16
|
/** Resolve the sticky key: valid cookie → UID header → freshly minted (cookie clients). */
|
|
10
17
|
resolveStickyKey(req: http.IncomingMessage): {
|
|
11
18
|
key?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var b=Object.defineProperty;var d=(o,s)=>b(o,"name",{value:s,configurable:!0});var H=Object.defineProperty,c=d((o,s)=>H(o,"name",{value:s,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRollout=createRollout;const node_crypto_1=require("node:crypto"),client_ip_1=require("../client-ip"),edge_1=require("./edge");function createRollout(o){const s=(0,client_ip_1.createClientIpResolver)({...o.clientIpHeader?{clientIpHeader:o.clientIpHeader}:{},...o.clientIpTrustedProxies?{clientIpTrustedProxies:o.clientIpTrustedProxies}:{},trustProxyHops:o.trustProxyIp?Math.max(1,Math.floor(o.trustProxyHops??1)):0}),h=s.clientIpOf,I=s.forwardedFor,g=(o.attributeHeaders??[]).map(e=>e.toLowerCase()),l=o.uidHeader?.toLowerCase(),a=o.cookie?.secret,u=!!o.cookie&&!!a,p=o.cookie?.name??"lensmcp_did",f=o.cookie?.domain,x=o.cookie?.maxAge??31536e3;o.cookie&&!a&&console.warn("[gateway] device cookie has NO secret \u2014 sticky cookie bucketing is DISABLED (set cookie.secret / LENSMCP_GW_COOKIE_SECRET to enable it). An unsigned cookie is attacker-choosable.");const m=c(e=>(0,node_crypto_1.createHmac)("sha256",a).update(e).digest("base64url"),"signId"),C=c(e=>`${e}.${m(e)}`,"signedCookie"),v=c(e=>{if(!a)return;const r=e.lastIndexOf(".");if(r<0)return;const t=e.slice(0,r),n=Buffer.from(e.slice(r+1)),i=Buffer.from(m(t));return n.length===i.length&&(0,node_crypto_1.timingSafeEqual)(n,i)?t:void 0},"verifyCookie"),_=c((e,r)=>{if(e)for(const t of e.split(";")){const n=t.indexOf("=");if(n>0&&t.slice(0,n).trim()===r){const i=t.slice(n+1).trim();try{return decodeURIComponent(i)}catch{return}}}},"parseCookie");return{clientIpOf:h,forwardedFor:I,buildAttrs:c((e,r,t,n)=>{const i={...t??{}};i.ip=r,i.method=(e.method??"GET").toUpperCase(),i.path=(e.url??"/").split("?")[0],n&&(i.device=n);for(const k of g){const y=(0,edge_1.hdr)(e.headers[k]);y!==void 0&&(i["header."+k]=y)}return i},"buildAttrs"),resolveStickyKey:c(e=>{if(u){const r=_(e.headers.cookie,p),t=r?v(r):void 0;if(t)return{key:t,mint:!1}}if(l){const r=(0,edge_1.hdr)(e.headers[l]);if(r)return{key:r,mint:!1}}return u?{key:"d_"+(0,node_crypto_1.randomBytes)(12).toString("base64url"),mint:!0}:{mint:!1}},"resolveStickyKey"),mintCookieHeader:c(e=>{let r=`${p}=${encodeURIComponent(C(e))}; Path=/; Max-Age=${x}; HttpOnly; SameSite=Lax; Secure`;return f&&(r+=`; Domain=${f}`),r},"mintCookieHeader")}}d(createRollout,"createRollout"),c(createRollout,"createRollout");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var m=Object.defineProperty;var i=(a,n)=>m(a,"name",{value:n,configurable:!0});var d=Object.defineProperty,r=i((a,n)=>d(a,"name",{value:n,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRouteTable=createRouteTable;const manifest_1=require("../manifest");function createRouteTable(a,n){const s=[],c=new Map;let o=a.manifests.list();const u=r(e=>{o=e;const t=(0,manifest_1.buildRouteTable)(e);s.length=0,s.push(...t),c.clear();for(const p of e)c.set(p.service,p)},"apply"),l=r(()=>{if(!a.healthChecker)return;const e=new Set;for(const t of s)t.upstream&&e.add(t.upstream);for(const t of a.pods?.endpoints?.()??[])e.add(t);a.healthChecker.track([...e])},"trackUpstreams"),f=r(e=>{u(e),l(),n.emit("info",`gateway routes: ${s.length} on ${o.length} services`,"gateway-up",{kind:"gateway-up",routes:s.map(t=>({host:t.host??"(default)",service:t.service,internal:!!t.internal}))})},"rebuild");u(o);const h=a.manifests.watch?.(e=>f(e));return{routes:s,manifestByService:c,rebuild:f,trackUpstreams:l,manifests:r(()=>o,"manifests"),stop:r(()=>h?.(),"stop")}}i(createRouteTable,"createRouteTable"),r(createRouteTable,"createRouteTable");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var A=Object.defineProperty;var h=(e,a)=>A(e,"name",{value:a,configurable:!0});var R=Object.defineProperty,n=h((e,a)=>R(e,"name",{value:a,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 a=e.ports?.length?e.ports:[e.tls?8443:8080],j=e.authenticate??(t=>{if(t==="jwt")throw new Error("jwt route but no authenticator configured")}),C=(e.versionHeader??"x-lensmcp-version").toLowerCase(),P=!!e.accessLog,L=new Set((e.corsCredentialOrigins??[]).map(t=>t.toLowerCase())),d=new Map;for(const[t,s]of Object.entries(e.serviceKeys??{}))typeof s=="string"&&s.trim()!==""?d.set(s,t):console.warn(`[gateway] service key for "${t}" is empty \u2014 east-west auth for that service is DISABLED (all internal routes 401) until a key is set.`);const w={},r=(0,observability_1.createObservability)(e,()=>w.current.routes),o=(0,routing_1.createRouteTable)(e,r);w.current=o;const i={opts:e,table:o,keyToProject:d,versionHeader:C,accessLog:P,corsCredentialOrigins:L},u=(0,rollout_1.createRollout)(e),y=(0,upstream_1.createUpstream)(e,r),g=(0,auth_1.createEdgeAuth)(i,r,u,j),c=(0,hooks_1.createHooks)(e.hooks??{}),p=new WeakMap,b=new WeakMap,m=new WeakMap,f=(0,cors_1.createCors)(i,p),O=(0,handler_1.createHandler)({rt:i,obs:r,cors:f,auth:g,upstream:y,rollout:u,hooks:c,reqState:p,spanByReq:b,ctxByReq:m}),M=(0,upgrade_1.createUpgrade)({rt:i,obs:r,auth:g,upstream:y,hooks:c,rollout:u}),{buildApp:S}=(0,app_1.createApp)({rt:i,obs:r,cors:f,hooks:c,handler:O,upgrade:M,reqState:p,spanByReq:b,ctxByReq:m}),x=r.startFlusher();o.rebuild(o.manifests());const k=[],q=[];for(const t of a){const s=await S();await s.listen({port:t,host:"0.0.0.0"});const l=s.server.address(),_=typeof l=="object"&&l?l.port:t;q.push(_),console.log(`[gateway] listening on ${e.tls?"https":"http"}://0.0.0.0:${_}`),k.push(s)}e.healthChecker?.start();let v=!1;return{ports:q,routes:n(()=>o.routes,"routes"),status:r.buildStatusSnapshot,refreshUpstreams:o.trackUpstreams,stop:n(async()=>{v||(v=!0,clearInterval(x),o.stop(),e.healthChecker?.stop(),e.rateLimiter?.stop(),e.metrics?.stop(),r.emit("info","gateway down","gateway-down",{kind:"gateway-down"}),await Promise.all(k.map(t=>t.close())))},"stop")}}h(startProdGateway,"startProdGateway"),n(startProdGateway,"startProdGateway");
|
|
@@ -113,12 +113,27 @@ export interface ProdGatewayOptions {
|
|
|
113
113
|
* ABOVE the GCP external ALB's 600s backend keep-alive so the gateway never
|
|
114
114
|
* closes a pooled connection the LB still considers live (the classic
|
|
115
115
|
* intermittent-502 race). `requestTimeout` default 0 (disabled) for streaming. */
|
|
116
|
+
/** `bodyStallMs` bounds INBOUND PROGRESS, not request duration: a request that has
|
|
117
|
+
* transferred nothing for this long is aborted (default 60 s; `0` disables). It is the
|
|
118
|
+
* bound `requestTimeout: 0` deliberately gives up — without it a trickled body pins an
|
|
119
|
+
* upstream pool connection indefinitely. A slow-but-progressing upload is never touched. */
|
|
120
|
+
/** `headersTimeout` bounds the HEADER phase (default 60000 — Node's own, and the only bound
|
|
121
|
+
* on a slowloris that never completes its headers, since `bodyStallMs` arms only after them).
|
|
122
|
+
* `maxHeadersCount` / `maxConnections` are opt-in ceilings for a directly-exposed edge. */
|
|
116
123
|
server?: {
|
|
117
124
|
keepAliveTimeout?: number;
|
|
118
125
|
requestTimeout?: number;
|
|
119
126
|
connectionTimeout?: number;
|
|
120
127
|
headersTimeout?: number;
|
|
128
|
+
bodyStallMs?: number;
|
|
129
|
+
maxHeadersCount?: number;
|
|
130
|
+
maxConnections?: number;
|
|
131
|
+
connectionsCheckingInterval?: number;
|
|
121
132
|
};
|
|
133
|
+
/** Replays of an idempotent upstream request whose connection failed. Default 1 — which also
|
|
134
|
+
* disables `@fastify/reply-from`'s built-in 503 storm (10 replays of any GET that draws a
|
|
135
|
+
* 503, delayed by the upstream's own `Retry-After`). 0 = never replay. */
|
|
136
|
+
upstreamRetries?: number;
|
|
122
137
|
/** Opt-in one-JSON-line-per-request access log to stdout, shaped for Cloud
|
|
123
138
|
* Logging (severity + time + message). Default off (zero overhead). */
|
|
124
139
|
accessLog?: boolean;
|
|
@@ -135,11 +150,17 @@ export interface ProdGatewayOptions {
|
|
|
135
150
|
identify?: (req: http.IncomingMessage) => Record<string, unknown> | undefined;
|
|
136
151
|
/** Allowlist of request headers exposed to rules as `header.<name>` attributes. */
|
|
137
152
|
attributeHeaders?: string[];
|
|
138
|
-
/** Take the `ip` attribute from `X-Forwarded-For` (behind a trusted LB)
|
|
139
|
-
*
|
|
153
|
+
/** Take the `ip` attribute from `X-Forwarded-For` (behind a trusted LB); default false →
|
|
154
|
+
* socket IP. See {@link trustProxyHops} for WHICH entry of the chain is read. */
|
|
140
155
|
trustProxyIp?: boolean;
|
|
156
|
+
/** How many proxy hops in front of the gateway are trusted to have appended to
|
|
157
|
+
* `X-Forwarded-For`. Default 1 (the nearest proxy), so the address read is the one THAT proxy
|
|
158
|
+
* observed. Trusting every hop — which is what `trustProxy: true` means — would make the
|
|
159
|
+
* attribute the leftmost entry, i.e. a value any client can write for itself. */
|
|
160
|
+
trustProxyHops?: number;
|
|
141
161
|
/** Gateway-issued device cookie for sticky bucketing (anonymous-friendly).
|
|
142
|
-
* Omit to disable cookie stickiness. `secret`
|
|
162
|
+
* Omit to disable cookie stickiness. `secret` is REQUIRED — an unsigned cookie is a value the
|
|
163
|
+
* client chooses, so without one the cookie is refused and stickiness stays off. */
|
|
143
164
|
cookie?: {
|
|
144
165
|
name?: string;
|
|
145
166
|
secret?: string;
|
|
@@ -156,6 +177,10 @@ export interface ProdGatewayHandle {
|
|
|
156
177
|
ports: number[];
|
|
157
178
|
routes: () => Route[];
|
|
158
179
|
status: () => ServiceStatus[];
|
|
180
|
+
/** Re-derive the health-checked upstream set. Call after an ENDPOINTS reload (a rollout
|
|
181
|
+
* ramp / cohort add) — the manifest watch already does this on its own changes, and
|
|
182
|
+
* without it a new cohort's endpoints are never probed, only assumed healthy. */
|
|
183
|
+
refreshUpstreams: () => void;
|
|
159
184
|
stop: () => Promise<void>;
|
|
160
185
|
}
|
|
161
186
|
/** The reply surface {@link sendError} uses — structural so BOTH the HTTP/1 and HTTP/2 fastify reply
|
|
@@ -193,6 +218,8 @@ export interface RouteTable {
|
|
|
193
218
|
routes: Route[];
|
|
194
219
|
manifestByService: Map<string, RouteManifest>;
|
|
195
220
|
rebuild(next: RouteManifest[]): void;
|
|
221
|
+
/** Re-derive + re-track the health-checked upstream set (manifest URLs ∪ pooled endpoints). */
|
|
222
|
+
trackUpstreams(): void;
|
|
196
223
|
manifests(): RouteManifest[];
|
|
197
224
|
stop(): void;
|
|
198
225
|
}
|
|
@@ -3,18 +3,43 @@
|
|
|
3
3
|
* request is rebuilt from `req.rawHeaders` verbatim — minus the gateway-owned
|
|
4
4
|
* trust headers + the client's hop-by-hop control — plus the validated caller +
|
|
5
5
|
* target token + verified identity. Reuses the shared auth + upstream layers.
|
|
6
|
+
*
|
|
7
|
+
* Three properties this path has to supply for itself, because it does NOT go
|
|
8
|
+
* through undici/Fastify like the HTTP handler does:
|
|
9
|
+
*
|
|
10
|
+
* - **header-value validation.** We write the handshake to a raw socket, so a
|
|
11
|
+
* CR/LF anywhere in a forwarded value would inject headers the upstream reads
|
|
12
|
+
* as gateway-authoritative. `validHeaderValue` is undici's own field-value
|
|
13
|
+
* grammar; a violation destroys the socket rather than forwarding a
|
|
14
|
+
* half-trusted handshake.
|
|
15
|
+
* - **identity parity.** Identity is NEVER re-derived from claims here: it is
|
|
16
|
+
* read back from `req.headers`, where the shared `stampIdentity` put it (and
|
|
17
|
+
* only where the HTTP path would have — inside the `jwt` branch of
|
|
18
|
+
* `auth.public`). One stamp, one guard, no divergence to drift.
|
|
19
|
+
* - **TLS.** `https:`/`wss:` upstreams get `tls.connect` with the same connect
|
|
20
|
+
* options (CA / client cert / verification) the undici pool uses, so an mTLS
|
|
21
|
+
* deployment is not silently downgraded on this path.
|
|
22
|
+
*
|
|
23
|
+
* The upstream response head is parsed before anything is piped: a raw
|
|
24
|
+
* bidirectional pipe is only legitimate once the upstream has answered `101` and
|
|
25
|
+
* left HTTP semantics. Any other status is relayed to the client and the sockets
|
|
26
|
+
* are closed — never tunnelled.
|
|
6
27
|
*/
|
|
7
28
|
import * as net from 'node:net';
|
|
8
29
|
import type * as http from 'node:http';
|
|
9
30
|
import type { ProdRuntime } from './types';
|
|
31
|
+
import type { Observability } from './observability';
|
|
10
32
|
import type { EdgeAuth } from './auth';
|
|
11
33
|
import type { UpstreamLayer } from './upstream';
|
|
12
34
|
import type { HooksLayer } from './hooks';
|
|
35
|
+
import type { RolloutLayer } from './rollout';
|
|
13
36
|
export interface UpgradeDeps {
|
|
14
37
|
rt: ProdRuntime;
|
|
38
|
+
obs: Observability;
|
|
15
39
|
auth: EdgeAuth;
|
|
16
40
|
upstream: UpstreamLayer;
|
|
17
41
|
hooks: HooksLayer;
|
|
42
|
+
rollout: RolloutLayer;
|
|
18
43
|
}
|
|
19
44
|
export type UpgradeListener = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
|
|
20
45
|
export declare function createUpgrade(deps: UpgradeDeps): UpgradeListener;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var V=Object.defineProperty;var A=(p,s)=>V(p,"name",{value:s,configurable:!0});var F=Object.defineProperty,u=A((p,s)=>F(p,"name",{value:s,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpgrade=createUpgrade;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),tls=tslib_1.__importStar(require("node:tls")),manifest_1=require("../manifest"),edge_1=require("./edge"),trust_1=require("./trust"),validHeaderValue=u(p=>/^[\t\x20-\x7e\x80-\xff]*$/.test(p),"validHeaderValue"),validToken=u(p=>/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(p),"validToken"),FORWARDED_HEADERS=["x-forwarded-for","x-forwarded-proto","x-forwarded-host"],MAX_HEAD_BYTES=64*1024,HANDSHAKE_TIMEOUT_MS=3e4;function createUpgrade(p){const{rt:s,obs:g,auth:S,upstream:z,hooks:y,rollout:E}=p,l=u((e,t,i,c)=>{g.emit("warning",`gateway upgrade rejected (${t})${i?` for ${i}`:""}`,`gateway-upgrade:${i??"(none)"}:${t}`,{kind:"gateway-upgrade-rejected",reason:t,...i?{project:i}:{},...c}),e.destroy()},"reject"),I=u((e,t,i="")=>{try{e.end(`HTTP/1.1 ${t}\r
|
|
2
|
+
${i}Connection: close\r
|
|
3
|
+
Content-Length: 0\r
|
|
4
|
+
\r
|
|
5
|
+
`)}catch{e.destroy()}},"refuse"),K=u(async(e,t,i)=>{(0,trust_1.stripTrust)(e);const c=(e.headers.host||"").split(":")[0],m=(0,edge_1.canonicalize)(e.url??"/");if(m===void 0){l(t,"bad-path");return}const U=m.policy;e.url=m.path+m.query;const r=(0,manifest_1.matchRoute)(s.table.routes,c,U+m.query);if(!r){l(t,"no-route",void 0,{host:c});return}const R=e.socket?.remoteAddress??"",q=E.clientIpOf(e,R);if(s.opts.rateLimiter){const a=await s.opts.rateLimiter.check(q);if(!a.allowed){g.emit("warning",`gateway upgrade rate-limited for ${r.service}`,`gateway-upgrade:${r.service}:rate-limited`,{kind:"gateway-upgrade-rejected",reason:"rate-limited",project:r.service}),I(t,"429 Too Many Requests",`Retry-After: ${Math.ceil(a.retryAfterMs/1e3)}\r
|
|
6
|
+
`);return}}const n=y.any?y.upgradeContext(e,t,void 0):void 0;n&&(n.route=r);const P=r.internal?void 0:s.opts.identify?.(e);let T;if(r.internal){const a=S.internal(e,r,void 0);if(!a.ok){l(t,"auth-failed",r.service);return}T=a.caller,n&&(n.caller=T)}else{if(!S.public(e,r,U,P,q,void 0).ok){l(t,"auth-failed",r.service);return}if(n&&(n.claims=P),n&&y.has("afterAuth")&&(await y.run("afterAuth",n),n.responded()))return}if(e.headers["x-forwarded-for"]=E.forwardedFor(e,R),e.headers["x-forwarded-proto"]=s.opts.tls?"https":"http",e.headers["x-forwarded-host"]=e.headers.host??c,s.opts.serviceKeys?.[r.service]&&(e.headers["x-internal-token"]=s.opts.serviceKeys[r.service]),r.prependPrefix){const a=e.url??"/",o=r.prependPrefix;a!==o&&!a.startsWith(o+"/")&&(e.url=o+a)}if(n&&y.has("onUpgrade")&&(await y.run("onUpgrade",n),n.responded()))return;const N=(0,edge_1.hdr)(e.headers[s.versionHeader]),x=await z.resolveUpstream(r,void 0,N);if(!x||!("url"in x)){l(t,"no-upstream",r.service);return}const f=new URL(x.url),B=new Set([...trust_1.TRUST_HEADERS,"x-api-key","connection","proxy-connection","keep-alive",...FORWARDED_HEADERS]),C=e.method??"GET",k=e.url??"/";if(!validToken(C)||!validHeaderValue(k)||k.includes(" ")){l(t,"bad-request-line",r.service);return}const _=[`${C} ${k} HTTP/1.1`,"Connection: Upgrade"];for(let a=0;a<e.rawHeaders.length;a+=2){const o=e.rawHeaders[a],$=e.rawHeaders[a+1]??"";if(!B.has(o.toLowerCase())){if(!validToken(o)||!validHeaderValue($)){l(t,"bad-header",r.service,{header:o.toLowerCase()});return}_.push(`${o}: ${$}`)}}for(const a of[...trust_1.TRUST_HEADERS,...FORWARDED_HEADERS]){const o=e.headers[a];if(!(typeof o!="string"||o==="")){if(!validHeaderValue(o)){l(t,"bad-identity-value",r.service,{header:a});return}_.push(`${a}: ${o}`)}}const b=f.protocol==="https:"||f.protocol==="wss:",D=Number(f.port)||(b?443:80),h=s.opts.undici,d=b?tls.connect({port:D,host:f.hostname,...net.isIP(f.hostname)?{}:{servername:f.hostname},rejectUnauthorized:h?.rejectUnauthorized??!1,...h?.cert?{cert:h.cert}:{},...h?.key?{key:h.key}:{},...h?.ca?{ca:h.ca}:{}}):net.connect(D,f.hostname);let v=!1;const w=u(a=>{v||(v=!0,clearTimeout(j),g.emit("warning",`gateway upgrade failed (${a}) for ${r.service}`,`gateway-upgrade:${r.service}:${a}`,{kind:"gateway-upgrade-rejected",reason:a,project:r.service}),d.destroy(),t.destroy())},"abort"),j=setTimeout(()=>w("handshake-timeout"),HANDSHAKE_TIMEOUT_MS);j.unref?.(),d.on("error",()=>w("upstream-error")),t.on("error",()=>w("client-error")),d.on(b?"secureConnect":"connect",()=>{d.write(_.join(`\r
|
|
2
7
|
`)+`\r
|
|
3
8
|
\r
|
|
4
|
-
`),
|
|
9
|
+
`),i?.length&&d.write(i)});const M=[];let O=0;const L=u(a=>{if(v)return;M.push(a),O+=a.length;const o=Buffer.concat(M),$=o.indexOf(`\r
|
|
10
|
+
\r
|
|
11
|
+
`);if($===-1){O>MAX_HEAD_BYTES&&w("oversized-response-head");return}v=!0,clearTimeout(j),d.off("data",L);const H=Number(/^HTTP\/1\.[01] (\d{3})/.exec(o.subarray(0,o.indexOf(`\r
|
|
12
|
+
`)).toString("latin1"))?.[1]);if(H!==101){g.emit("warning",`gateway upgrade refused upstream (${H||"malformed"}) for ${r.service}`,`gateway-upgrade:${r.service}:upstream-refused`,{kind:"gateway-upgrade-rejected",reason:"upstream-refused",project:r.service,status:H||0}),t.end(o.subarray(0,$+4)),d.destroy();return}g.recordEdge(r,0,101,T,x.version),t.write(o),d.pipe(t),t.pipe(d)},"onHead");d.on("data",L),d.on("close",()=>{v||w("upstream-closed")})},"run");return(e,t,i)=>{K(e,t,i).catch(c=>{g.emit("error",`gateway upgrade error: ${c?.message??String(c)}`,"gateway-upgrade:(none):exception",{kind:"gateway-upgrade-rejected",reason:"exception",error:c?.message??String(c)});try{t.destroy()}catch{}})}}A(createUpgrade,"createUpgrade"),u(createUpgrade,"createUpgrade");
|
|
@@ -7,6 +7,8 @@ import type { Route, Upstream } from '../manifest';
|
|
|
7
7
|
import type { GwTrace, ProdGatewayOptions } from './types';
|
|
8
8
|
import type { Observability } from './observability';
|
|
9
9
|
export interface UpstreamLayer {
|
|
10
|
-
|
|
10
|
+
/** `allowDrained` marks a version the GATEWAY chose (ABAC rule / sticky bucket) rather than one
|
|
11
|
+
* the client asked for — only the former may land on a drained (weight-0) cohort. */
|
|
12
|
+
resolveUpstream(route: Route, trace: GwTrace | undefined, pinned?: string, allowDrained?: boolean): Promise<Upstream | undefined>;
|
|
11
13
|
}
|
|
12
14
|
export declare function createUpstream(opts: ProdGatewayOptions, obs: Observability): UpstreamLayer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var v=Object.defineProperty;var p=(e,t)=>v(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,i,n)=>{if(r.upstream)return{url:r.upstream};const c={...i?{version:i}:{},...n?{allowDrained:!0}:{},...e.healthChecker?{isHealthy:s(l=>e.healthChecker.isHealthy(l),"isHealthy")}:{}};let a=e.pods?.pick(r.service,c);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,c))),a},"resolveUpstream")}}p(createUpstream,"createUpstream"),s(createUpstream,"createUpstream");
|
|
@@ -4,8 +4,13 @@ export interface RolloutSpec {
|
|
|
4
4
|
rules?: TargetRule[];
|
|
5
5
|
cohorts: VersionCohort[];
|
|
6
6
|
}
|
|
7
|
-
/** Per-service rollout table — a bare cohort array (no rules) or a full spec. */
|
|
8
|
-
export type RolloutTable = Record<string, VersionCohort[] | RolloutSpec>;
|
|
7
|
+
/** Per-service rollout table — a flat URL list, a bare cohort array (no rules), or a full spec. */
|
|
8
|
+
export type RolloutTable = Record<string, string[] | VersionCohort[] | RolloutSpec>;
|
|
9
|
+
/** Is this endpoints table a ROLLOUT table (cohorts / a full spec) rather than flat `{svc:[url]}`?
|
|
10
|
+
* BOTH documented rollout shapes count — `{svc:[{version,weight,urls}]}` and the targeted
|
|
11
|
+
* `{svc:{rules,cohorts}}`. Checks EVERY entry, not just the first, so a mixed file classifies
|
|
12
|
+
* correctly (a flat entry compiles to a synthetic single cohort, so mixing is well-defined). */
|
|
13
|
+
export declare const isRolloutShape: (eps: Record<string, unknown>) => boolean;
|
|
9
14
|
/** A fixed set of manifests (tests, single-shot config). */
|
|
10
15
|
export declare function staticManifestProvider(manifests: RouteManifest[]): ManifestProvider;
|
|
11
16
|
/**
|
|
@@ -29,7 +34,9 @@ export declare function fileManifestProvider(filePath: string, opts?: {
|
|
|
29
34
|
* (10→25→50→100) or rolls back without redeploying the gateway. The returned
|
|
30
35
|
* Upstream carries `version` so the gateway can surface which cohort served.
|
|
31
36
|
*/
|
|
32
|
-
export declare function rolloutPodProvider(table: RolloutTable
|
|
37
|
+
export declare function rolloutPodProvider(table: RolloutTable, opts?: {
|
|
38
|
+
onError?: (e: unknown) => void;
|
|
39
|
+
}): PodProvider & {
|
|
33
40
|
update: (next: RolloutTable) => void;
|
|
34
41
|
};
|
|
35
42
|
/**
|
|
@@ -40,6 +47,8 @@ export declare function rolloutPodProvider(table: RolloutTable): PodProvider & {
|
|
|
40
47
|
* response only gets `x-lensmcp-version` (and a `rollout` trace step) when a
|
|
41
48
|
* genuine multi-cohort rollout served it.
|
|
42
49
|
*/
|
|
43
|
-
export declare function endpointsPodProvider(endpoints: Record<string, string[]
|
|
50
|
+
export declare function endpointsPodProvider(endpoints: Record<string, string[]>, opts?: {
|
|
51
|
+
onError?: (e: unknown) => void;
|
|
52
|
+
}): PodProvider & {
|
|
44
53
|
update: (next: Record<string, string[]>) => void;
|
|
45
54
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var V=Object.defineProperty;var g=(t,o)=>V(t,"name",{value:o,configurable:!0});var P=Object.defineProperty,n=g((t,o)=>P(t,"name",{value:o,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isRolloutShape=void 0,exports.staticManifestProvider=staticManifestProvider,exports.fileManifestProvider=fileManifestProvider,exports.rolloutPodProvider=rolloutPodProvider,exports.endpointsPodProvider=endpointsPodProvider;const node_fs_1=require("node:fs"),manifest_1=require("./manifest"),FLAT="_flat",isCohortList=n(t=>Array.isArray(t)&&t.every(o=>!!o&&typeof o=="object"&&Array.isArray(o.urls)),"isCohortList"),isUrlList=n(t=>Array.isArray(t)&&t.every(o=>typeof o=="string"),"isUrlList"),isSpec=n(t=>!!t&&typeof t=="object"&&!Array.isArray(t)&&isCohortList(t.cohorts),"isSpec"),isRolloutShape=n(t=>Object.values(t).some(o=>isSpec(o)||isCohortList(o)&&o.length>0),"isRolloutShape");exports.isRolloutShape=isRolloutShape;const isValidEntry=n(t=>isSpec(t)||isCohortList(t)||isUrlList(t),"isValidEntry"),toSpec=n(t=>Array.isArray(t)?isUrlList(t)?{rules:[],cohorts:[{version:FLAT,weight:1,urls:t}]}:{rules:[],cohorts:t}:{rules:t.rules??[],cohorts:t.cohorts??[]},"toSpec");function staticManifestProvider(t){return{list:n(()=>t,"list")}}g(staticManifestProvider,"staticManifestProvider"),n(staticManifestProvider,"staticManifestProvider");function parseManifests(t){let o;try{o=JSON.parse(t)}catch{return}const l=Array.isArray(o)?o:o?.manifests;if(!Array.isArray(l))return;const d=l.filter(f=>!!f&&typeof f=="object"&&typeof f.service=="string"&&Array.isArray(f.mounts));return d.length>0?d:void 0}g(parseManifests,"parseManifests"),n(parseManifests,"parseManifests");function fileManifestProvider(t,o={}){const l=o.onError??(()=>{});let d=[],f=-1;const w=new Set,A=n(()=>{if(!(0,node_fs_1.existsSync)(t))return;let u;try{u=(0,node_fs_1.statSync)(t).mtimeMs}catch{return}if(u===f)return;let M;try{M=(0,node_fs_1.readFileSync)(t,"utf8")}catch(e){l(e);return}const s=parseManifests(M);if(!s){l(new Error(`[gateway] rejecting invalid manifest file ${t} \u2014 keeping last-good`));return}f=u,d=s;for(const e of w)try{e(d)}catch(r){l(r)}},"reload");A();let m;try{m=(0,node_fs_1.watch)(t,{persistent:!1},()=>A())}catch{}const b=setInterval(A,o.pollMs??1e3);return b.unref?.(),{list:n(()=>d,"list"),watch:n(u=>(w.add(u),()=>w.delete(u)),"watch"),stop:n(()=>{try{m?.close()}catch{}clearInterval(b)},"stop")}}g(fileManifestProvider,"fileManifestProvider"),n(fileManifestProvider,"fileManifestProvider");function rolloutPodProvider(t,o={}){const l=new Map,d=o.onError??(()=>{}),f=n(s=>({cohorts:s.cohorts.map(e=>({...e,urls:e.urls.slice()})),select:(0,manifest_1.smoothWeightedSelector)(s.cohorts.map(e=>e.weight)),rr:s.cohorts.map(()=>-1),rules:s.rules??[]}),"build"),w=n(s=>{const e=new Map,r=[];for(const[c,a]of Object.entries(s??{})){const h=n(()=>{const S=l.get(c);S&&e.set(c,S),r.push(c)},"keepLastGood");if(!isValidEntry(a)){h();continue}try{e.set(c,f(toSpec(a)))}catch{h()}}r.length&&d(new Error(`[gateway] rollout config: ignoring invalid entr${r.length>1?"ies":"y"} for ${r.join(", ")} \u2014 keeping last-good`)),l.clear();for(const[c,a]of e)l.set(c,a)},"load");w(t);const A=n(s=>s.cohorts.findIndex(e=>e.urls.length>0),"firstNonEmpty"),m=3e4,b=3e5,u=new Map,M=n(s=>{const e=u.get(s);return!!e&&Date.now()<e.until},"isEjected");return{pick(s,e){const r=l.get(s);if(!r||r.cohorts.length===0)return;const c=e?.isHealthy,a=c||u.size>0?i=>!M(i)&&(c?c(i):!0):void 0,h=n((i,p)=>{const j=r.cohorts[i];if(!j||j.urls.length===0)return;const E=j.urls.length;for(let _=0;_<E;_++){r.rr[i]=(r.rr[i]+1)%E;const L=j.urls[r.rr[i]];if(!p||p(L))return L}},"pickUrl"),S=n(i=>i.urls.length>0&&(e?.allowDrained===!0||i.weight>0),"pinnable"),k=e?.version?r.cohorts.findIndex(i=>i.version===e.version&&S(i)):-1;if(k>=0){const i=h(k,a)??h(k);if(i===void 0)return;const p=r.cohorts[k].version;return p===FLAT?{url:i}:{url:i,version:p}}let v=r.select();(!r.cohorts[v]||r.cohorts[v].urls.length===0)&&(v=A(r));let y=h(v,a);if(y===void 0&&a)for(let i=0;i<r.cohorts.length;i++){const p=h(i,a);if(p!==void 0){v=i,y=p;break}}if(y===void 0&&(y=h(v)),y===void 0)return;const x=r.cohorts[v]?.version;return x===void 0||x===FLAT?{url:y}:{url:y,version:x}},endpoints(){const s=new Set;for(const e of l.values())for(const r of e.cohorts)for(const c of r.urls)s.add(c);return[...s]},resolveVersion(s,e){const r=l.get(s);return r&&r.rules.length?(0,manifest_1.pickTargetedVersion)(r.rules,e):void 0},bucket(s,e){const r=l.get(s);if(!r||r.cohorts.length===0)return;const c=r.cohorts[(0,manifest_1.bucketByWeight)(`${e}:${s}`,r.cohorts.map(a=>a.weight))];return c&&c.version!==FLAT?{version:c.version}:void 0},ensureUp:n(async()=>!0,"ensureUp"),drop(s,e){if(!("url"in e))return;const r=Date.now(),c=u.get(e.url),a=c&&r<c.until+b?c.strikes+1:1;u.set(e.url,{until:r+Math.min(m*2**(a-1),b),strikes:a})},update:w}}g(rolloutPodProvider,"rolloutPodProvider"),n(rolloutPodProvider,"rolloutPodProvider");function endpointsPodProvider(t,o={}){const l=rolloutPodProvider(t,o);return{...l,resolveVersion:n(()=>{},"resolveVersion"),bucket:n(()=>{},"bucket"),update:n(d=>l.update(d),"update")}}g(endpointsPodProvider,"endpointsPodProvider"),n(endpointsPodProvider,"endpointsPodProvider");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var g=Object.defineProperty;var u=(e,t)=>g(e,"name",{value:t,configurable:!0});var y=Object.defineProperty,
|
|
1
|
+
"use strict";var g=Object.defineProperty;var u=(e,t)=>g(e,"name",{value:t,configurable:!0});var y=Object.defineProperty,n=u((e,t)=>y(e,"name",{value:t,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.changeChannel=exports.rolloutKey=exports.manifestKey=exports.DEFAULT_REGISTRY_PREFIX=void 0,exports.redisRegistrySource=redisRegistrySource,exports.createRegistryProviders=createRegistryProviders;const providers_prod_1=require("./providers-prod");exports.DEFAULT_REGISTRY_PREFIX="lensmcp:gw";const manifestKey=n((e,t)=>`${e}:manifest:${t}`,"manifestKey");exports.manifestKey=manifestKey;const rolloutKey=n((e,t)=>`${e}:rollout:${t}`,"rolloutKey");exports.rolloutKey=rolloutKey;const changeChannel=n(e=>`${e}:changed`,"changeChannel");exports.changeChannel=changeChannel;function parseManifest(e){try{const t=JSON.parse(e);return t&&typeof t.service=="string"&&Array.isArray(t.mounts)?t:void 0}catch{return}}u(parseManifest,"parseManifest"),n(parseManifest,"parseManifest");function redisRegistrySource(e,t={}){const a=t.prefix??exports.DEFAULT_REGISTRY_PREFIX;let s;const c=`${a}:rollout:`;return{async read(){const i=await e.keys(`${a}:manifest:*`),d=i.length?(await e.mget(...i)).filter(r=>!!r).map(parseManifest).filter(r=>!!r):[],o=await e.keys(`${c}*`),l={};if(o.length){const r=await e.mget(...o);o.forEach((p,h)=>{const f=r[h];if(f)try{l[p.slice(c.length)]=JSON.parse(f)}catch{}})}return{manifests:d,endpoints:l}},async watch(i){return s=e.duplicate(),await s.subscribe((0,exports.changeChannel)(a)),s.on("message",()=>i()),async()=>{try{await s?.quit()}catch{}}},stop(){s?.quit().catch(()=>{})}}}u(redisRegistrySource,"redisRegistrySource"),n(redisRegistrySource,"redisRegistrySource");function createRegistryProviders(e,t={}){const a=t.onError??(()=>{});let s=[];const c=new Set,i=(0,providers_prod_1.rolloutPodProvider)({},{onError:a});let d,o;const l=n(async()=>{let r;try{r=await e.read()}catch(p){a(p);return}if(s.length>0&&r.manifests.length===0){a(new Error("[gateway] registry read came back EMPTY while serving routes \u2014 keeping last-good (set the registry to a single manifest to drain deliberately)"));return}s=r.manifests,(Object.keys(r.endpoints).length>0||s.length===0)&&i.update(r.endpoints);for(const p of c)try{p(s)}catch(h){a(h)}},"refresh");return{manifests:{list:n(()=>s,"list"),watch:n(r=>(c.add(r),()=>c.delete(r)),"watch")},pods:i,async start(){if(await l(),e.watch)try{d=await e.watch(()=>{l()})}catch(r){a(r)}o=setInterval(()=>{l()},t.pollMs??5e3),o.unref?.()},stop(){d?.(),o&&clearInterval(o),e.stop?.()}}}u(createRegistryProviders,"createRegistryProviders"),n(createRegistryProviders,"createRegistryProviders");
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var d=Object.defineProperty;var
|
|
2
|
-
`)}
|
|
1
|
+
"use strict";var d=Object.defineProperty;var c=(n,r)=>d(n,"name",{value:r,configurable:!0});var h=Object.defineProperty,i=c((n,r)=>h(n,"name",{value:r,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.applyRolloutOp=applyRolloutOp,exports.summarizeRollout=summarizeRollout,exports.parseRolloutCommand=parseRolloutCommand;const RULE_OPS=new Set(["eq","ne","in","nin","prefix","suffix","contains","regex","exists","gt","lt","cidr"]),clone=i(n=>JSON.parse(JSON.stringify(n)),"clone");function asSpec(n,r){if(r===void 0)return{rules:[],cohorts:[]};if(Array.isArray(r)){if(r.length&&typeof r[0]=="string")throw new Error(`service "${n}" is a flat endpoint list; convert it to cohorts first (e.g. add-cohort)`);return{rules:[],cohorts:r}}return{rules:r.rules??[],cohorts:r.cohorts??[]}}c(asSpec,"asSpec"),i(asSpec,"asSpec");const findCohort=i((n,r)=>n.cohorts.find(o=>o.version===r),"findCohort");function reqWeight(n){if(!Number.isFinite(n)||n<0)throw new Error(`weight must be a number \u2265 0 (got ${n})`);return n}c(reqWeight,"reqWeight"),i(reqWeight,"reqWeight");function applyRolloutOp(n,r){const o=clone(n),e=asSpec(r.service,o[r.service]);switch(r.kind){case"add-cohort":{if(findCohort(e,r.version))throw new Error(`cohort "${r.version}" already exists on "${r.service}" \u2014 use set-weight`);if(!r.urls.length)throw new Error("add-cohort needs at least one upstream url");e.cohorts.push({version:r.version,weight:reqWeight(r.weight),urls:r.urls.slice()});break}case"set-weight":{const t=findCohort(e,r.version);if(!t)throw new Error(`no cohort "${r.version}" on "${r.service}"`);t.weight=reqWeight(r.weight);break}case"remove-cohort":{if(!findCohort(e,r.version))throw new Error(`no cohort "${r.version}" on "${r.service}"`);e.cohorts=e.cohorts.filter(t=>t.version!==r.version);break}case"promote":{if(!findCohort(e,r.version))throw new Error(`no cohort "${r.version}" on "${r.service}"`);for(const t of e.cohorts)t.weight=t.version===r.version?100:0;break}case"abort":{const t=findCohort(e,r.version);if(!t)throw new Error(`no cohort "${r.version}" on "${r.service}"`);t.weight=0;break}case"add-rule":{validateRule(r.rule),e.rules=[...(e.rules??[]).filter(t=>!r.rule.name||t.name!==r.rule.name),r.rule];break}case"remove-rule":{const t=(e.rules??[]).length;if(e.rules=(e.rules??[]).filter(s=>s.name!==r.name),e.rules.length===t)throw new Error(`no rule named "${r.name}" on "${r.service}"`);break}}return o[r.service]=e.rules&&e.rules.length?{rules:e.rules,cohorts:e.cohorts}:e.cohorts,o}c(applyRolloutOp,"applyRolloutOp"),i(applyRolloutOp,"applyRolloutOp");function validateRule(n){if(!n||typeof n.version!="string"||!n.version)throw new Error("rule needs a non-empty version");if(!n.when||typeof n.when!="object")throw new Error("rule needs a `when` condition");const r=i(o=>{const e=o;if("all"in e||"any"in e||"not"in e){for(const t of["all","any"])Array.isArray(e[t])&&e[t].forEach(r);e.not&&r(e.not);return}if(typeof e.attr!="string"||!RULE_OPS.has(e.op))throw new Error(`invalid rule condition (attr/op): ${JSON.stringify(e)}`)},"walk");r(n.when)}c(validateRule,"validateRule"),i(validateRule,"validateRule");function summarizeRollout(n,r){const o=r?[r]:Object.keys(n);if(!o.length)return"(no services in rollout config)";const e=[];for(const t of o){const s=n[t];if(s===void 0){e.push(`${t}: (not present)`);continue}if(Array.isArray(s)&&s.length&&typeof s[0]=="string"){e.push(`${t}: flat LB \u2192 ${s.join(", ")}`);continue}const u=asSpec(t,s),l=u.cohorts.reduce((a,v)=>a+Math.max(0,v.weight),0)||1;e.push(`${t}:`);for(const a of u.cohorts)e.push(` \u2022 ${a.version.padEnd(16)} weight ${String(a.weight).padStart(3)} (${(Math.max(0,a.weight)/l*100).toFixed(0)}%) ${a.urls.length} url(s)`);for(const a of u.rules??[])e.push(` \u25B8 rule ${(a.name??"(unnamed)").padEnd(16)} \u2192 ${a.version} ${ruleSummary(a.when)}`)}return e.join(`
|
|
2
|
+
`)}c(summarizeRollout,"summarizeRollout"),i(summarizeRollout,"summarizeRollout");function ruleSummary(n){const r=n;return"all"in r?"("+r.all.map(ruleSummary).join(" AND ")+")":"any"in r?"("+r.any.map(ruleSummary).join(" OR ")+")":"not"in r?"NOT "+ruleSummary(r.not):`${r.attr} ${r.op}${r.value!==void 0?" "+JSON.stringify(r.value):""}`}c(ruleSummary,"ruleSummary"),i(ruleSummary,"ruleSummary");function parseRolloutCommand(n){const[r,...o]=n,e=i((t,s)=>{if(o.length<t)throw new Error(`usage: ${r} ${s}`)},"need");switch(r){case"status":return{status:o[0]};case"add-cohort":return e(4,"<service> <version> <weight> <url> [url...]"),{op:{kind:"add-cohort",service:o[0],version:o[1],weight:Number(o[2]),urls:o.slice(3)}};case"set-weight":return e(3,"<service> <version> <weight>"),{op:{kind:"set-weight",service:o[0],version:o[1],weight:Number(o[2])}};case"remove-cohort":return e(2,"<service> <version>"),{op:{kind:"remove-cohort",service:o[0],version:o[1]}};case"promote":return e(2,"<service> <version>"),{op:{kind:"promote",service:o[0],version:o[1]}};case"abort":return e(2,"<service> <version>"),{op:{kind:"abort",service:o[0],version:o[1]}};case"add-rule":{e(6,"<service> <name> <attr> <op> <value> <version>");const t=o[3],s=o[4]??"",u=t==="in"||t==="nin"?s.split(",").map(l=>l.trim()).filter(Boolean):s;return{op:{kind:"add-rule",service:o[0],rule:{name:o[1],when:{attr:o[2],op:t,value:u},version:o[5]}}}}case"remove-rule":return e(2,"<service> <name>"),{op:{kind:"remove-rule",service:o[0],name:o[1]}};default:throw new Error(`unknown command "${r??""}". commands: status, add-cohort, set-weight, remove-cohort, promote, abort, add-rule, remove-rule`)}}c(parseRolloutCommand,"parseRolloutCommand"),i(parseRolloutCommand,"parseRolloutCommand");
|
|
@@ -3,7 +3,12 @@ import type { Observability } from './observability';
|
|
|
3
3
|
import type { JwtVerifier } from '../jwks-verify';
|
|
4
4
|
export interface EdgeAuth {
|
|
5
5
|
internal(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* `cpath` is the DECODED canonical path the caller already derived (`canonicalize(...).policy`) — the
|
|
8
|
+
* one string routing, policy and forwarding share (issues/dev-gateway/11). A caller that has none
|
|
9
|
+
* gets it re-derived from `req.url`, so the override is never matched against a raw target again.
|
|
10
|
+
*/
|
|
11
|
+
public(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined, cpath?: string): AuthVerdict;
|
|
7
12
|
}
|
|
8
13
|
/**
|
|
9
14
|
* HOST-AWARE verifier selection — a SHARED multi-workspace daemon fronts several
|