@lensmcp/cluster 1.21.2 → 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.
Files changed (66) hide show
  1. package/create-webpack-dev.d.ts +45 -0
  2. package/create-webpack-dev.js +1 -1
  3. package/executors/gateway/body-stall.d.ts +37 -0
  4. package/executors/gateway/body-stall.js +1 -0
  5. package/executors/gateway/canonical-path.d.ts +65 -0
  6. package/executors/gateway/canonical-path.js +1 -0
  7. package/executors/gateway/client-ip.d.ts +44 -0
  8. package/executors/gateway/client-ip.js +1 -0
  9. package/executors/gateway/jwks-verify.js +1 -1
  10. package/executors/gateway/main.prod-gateway.js +2 -2
  11. package/executors/gateway/manifest.d.ts +46 -4
  12. package/executors/gateway/manifest.js +1 -1
  13. package/executors/gateway/prod-config.d.ts +74 -0
  14. package/executors/gateway/prod-config.js +1 -0
  15. package/executors/gateway/prod-runtime/access-log.js +1 -1
  16. package/executors/gateway/prod-runtime/app.d.ts +6 -0
  17. package/executors/gateway/prod-runtime/app.js +1 -1
  18. package/executors/gateway/prod-runtime/auth.d.ts +1 -1
  19. package/executors/gateway/prod-runtime/auth.js +1 -1
  20. package/executors/gateway/prod-runtime/body-stall.d.ts +5 -0
  21. package/executors/gateway/prod-runtime/body-stall.js +1 -0
  22. package/executors/gateway/prod-runtime/cors.js +1 -1
  23. package/executors/gateway/prod-runtime/edge.d.ts +19 -3
  24. package/executors/gateway/prod-runtime/edge.js +1 -1
  25. package/executors/gateway/prod-runtime/handler.js +1 -1
  26. package/executors/gateway/prod-runtime/rollout.d.ts +11 -4
  27. package/executors/gateway/prod-runtime/rollout.js +1 -1
  28. package/executors/gateway/prod-runtime/routing.js +1 -1
  29. package/executors/gateway/prod-runtime/server.js +1 -1
  30. package/executors/gateway/prod-runtime/types.d.ts +30 -3
  31. package/executors/gateway/prod-runtime/upgrade.d.ts +25 -0
  32. package/executors/gateway/prod-runtime/upgrade.js +10 -2
  33. package/executors/gateway/prod-runtime/upstream.d.ts +3 -1
  34. package/executors/gateway/prod-runtime/upstream.js +1 -1
  35. package/executors/gateway/providers-prod.d.ts +13 -4
  36. package/executors/gateway/providers-prod.js +1 -1
  37. package/executors/gateway/registry-source.js +1 -1
  38. package/executors/gateway/rollout-ops.js +2 -2
  39. package/executors/gateway/runtime/auth.d.ts +6 -1
  40. package/executors/gateway/runtime/auth.js +1 -1
  41. package/executors/gateway/runtime/control.js +2 -1
  42. package/executors/gateway/runtime/dev-auth.d.ts +6 -2
  43. package/executors/gateway/runtime/dev-auth.js +1 -1
  44. package/executors/gateway/runtime/discovery.js +1 -1
  45. package/executors/gateway/runtime/edge.d.ts +19 -0
  46. package/executors/gateway/runtime/edge.js +1 -1
  47. package/executors/gateway/runtime/handler.js +1 -1
  48. package/executors/gateway/runtime/lens-children.d.ts +7 -0
  49. package/executors/gateway/runtime/lens-children.js +1 -1
  50. package/executors/gateway/runtime/lifecycle.d.ts +69 -7
  51. package/executors/gateway/runtime/lifecycle.js +3 -3
  52. package/executors/gateway/runtime/pod-probe.d.ts +37 -0
  53. package/executors/gateway/runtime/pod-probe.js +1 -0
  54. package/executors/gateway/runtime/proxy.js +1 -1
  55. package/executors/gateway/runtime/scope.d.ts +15 -0
  56. package/executors/gateway/runtime/scope.js +1 -1
  57. package/executors/gateway/runtime/server.js +2 -2
  58. package/executors/gateway/runtime/service-keys.d.ts +42 -1
  59. package/executors/gateway/runtime/service-keys.js +1 -1
  60. package/executors/gateway/runtime/types.d.ts +193 -1
  61. package/executors/gateway/runtime/types.js +1 -1
  62. package/executors/gateway/runtime/upgrade.js +1 -1
  63. package/executors/gateway/runtime/workspace-log.d.ts +7 -0
  64. package/executors/gateway/runtime/workspace-log.js +1 -0
  65. package/main.devserver.js +7 -7
  66. package/package.json +4 -4
@@ -1 +1 @@
1
- "use strict";var q=Object.defineProperty;var p=(a,u)=>q(a,"name",{value:u,configurable:!0});var _=Object.defineProperty,l=p((a,u)=>_(a,"name",{value:u,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createApp=createApp;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),fastify_1=tslib_1.__importDefault(require("fastify")),reply_from_1=tslib_1.__importDefault(require("@fastify/reply-from")),edge_1=require("./edge"),trust_1=require("./trust"),access_log_1=require("./access-log");function createApp(a){const{rt:u,obs:c,cors:k,hooks:f,handler:y,upgrade:T,reqState:g,spanByReq:m,ctxByReq:h}=a,e=u.opts;return{buildApp:l(async()=>{const v=e.server?.keepAliveTimeout??62e4,n=(0,fastify_1.default)({logger:!1,keepAliveTimeout:v,requestTimeout:e.server?.requestTimeout??0,...e.server?.connectionTimeout!==void 0?{connectionTimeout:e.server.connectionTimeout}:{},...e.trustProxyIp?{trustProxy:!0}:{},...e.tls?{https:{key:e.tls.key,cert:e.tls.cert}}:{}});return n.server.headersTimeout=e.server?.headersTimeout??v+1e4,n.removeAllContentTypeParsers(),n.addContentTypeParser("*",(s,r,o)=>o(null,r)),await n.register(reply_from_1.default,{disableRequestLogging:!0,undici:{connections:e.undici?.connections??256,pipelining:e.undici?.pipelining??1,keepAliveTimeout:e.undici?.keepAliveTimeout??6e4,bodyTimeout:e.undici?.bodyTimeout??0,headersTimeout:e.undici?.headersTimeout??6e4,...e.undici?.allowH2?{allowH2:!0}:{},connect:{rejectUnauthorized:e.undici?.rejectUnauthorized??!1,...e.undici?.cert?{cert:e.undici.cert}:{},...e.undici?.key?{key:e.undici.key}:{},...e.undici?.ca?{ca:e.undici.ca}:{}}}}),n.get("/livez",(s,r)=>{r.type("text/plain").send("ok")}),n.get("/readyz",(s,r)=>{const o=u.table.routes.length>0;r.code(o?200:503).type("text/plain").send(o?"ready":"no routes loaded")}),n.get("/statusz",(s,r)=>{if(!e.statusEndpoint||!e.statusToken){r.code(404).type("text/plain").send("not found");return}const o=s.query?.token??(0,edge_1.hdr)(s.raw.headers["x-status-token"])??"",t=Buffer.from(o),i=Buffer.from(e.statusToken);if(t.length!==i.length||!(0,node_crypto_1.timingSafeEqual)(t,i)){r.code(401).type("text/plain").send("unauthorized");return}r.type("application/json").send(JSON.stringify({generatedAt:Date.now(),services:c.buildStatusSnapshot(),endpoints:e.healthChecker?.snapshot()??[]}))}),n.get("/metricsz",(s,r)=>{if(!e.metrics||!e.statusToken){r.code(404).type("text/plain").send("not found");return}const o=s.query?.token??(0,edge_1.hdr)(s.raw.headers["x-status-token"])??"",t=Buffer.from(o),i=Buffer.from(e.statusToken);if(t.length!==i.length||!(0,node_crypto_1.timingSafeEqual)(t,i)){r.code(401).type("text/plain").send("unauthorized");return}r.type("application/json").send(JSON.stringify(e.metrics.snapshot()))}),n.addHook("onRequest",(s,r,o)=>{(0,trust_1.stripTrust)(s.raw),o()}),n.addHook("onSend",(s,r,o,t)=>{k.applyResponseCors(s,r),t(null,o)}),n.addHook("onResponse",(s,r,o)=>{const t=g.get(s);t&&(c.recordEdge(t.route,Date.now()-t.startedAt,r.statusCode??0,t.caller,t.version),c.finish(t.trace,t.route.service,r.statusCode??0,{upstream:t.upLabel,...t.version?{version:t.version}:{},...t.caller?{caller:t.caller}:{}}),g.delete(s));const i=m.get(s);i&&(e.tracing.end(i,r.statusCode??0,{...t?.upLabel?{"upstream.url":t.upLabel}:{},...t?.version?{"lensmcp.version":t.version}:{}}),m.delete(s)),e.metrics?.recordRequest(r.statusCode??0),u.accessLog&&(0,access_log_1.logAccess)(s,r,t);const d=h.get(s);d&&(d.status=r.statusCode??0,f.has("onResponse")&&f.run("onResponse",d),h.delete(s)),o()}),n.all("/",y),n.all("/*",y),n.server.on("upgrade",T),n},"buildApp")}}p(createApp,"createApp"),l(createApp,"createApp");
1
+ "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, fallbackIp: string, trace: GwTrace | undefined): AuthVerdict;
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 d=(a,e)=>f(a,"name",{value:e,configurable:!0});var k=Object.defineProperty,n=d((a,e)=>k(a,"name",{value:e,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),trust_1=require("./trust");function createEdgeAuth(a,e,h,p){return{internal:n((r,u,i)=>{const t=a.keyToProject.get(String(r.headers["x-api-key"]??""));return t?(r.headers["x-api-key-id"]=t,delete r.headers["x-api-key"],e.step(i,"auth",{mode:"x-api-key",ok:!0,caller:t}),{ok:!0,caller:t}):(e.step(i,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."})},"internal"),public:n((r,u,i,t,c,o)=>{const l=a.table.manifestByService.get(u.service),s=l?(0,manifest_1.authRuleForPath)(l,i,r.method??"GET",!1):{mode:u.auth};if(s.mode==="jwt"){if(a.opts.identify){if(!t)return e.step(o,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"}}else try{p("jwt",r)}catch(m){return e.step(o,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:`Unauthorized: ${m.message}`}}return s.rule&&(!t||!(0,manifest_1.evalRule)(s.rule,h.buildAttrs(r,c,t,void 0)))?(e.step(o,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):(e.step(o,"auth",{mode:"jwt",ok:!0}),t&&(0,trust_1.stampIdentity)(r,t),{ok:!0})}return e.step(o,"auth",{mode:s.mode}),{ok:!0}},"publicAuth")}}d(createEdgeAuth,"createEdgeAuth"),n(createEdgeAuth,"createEdgeAuth");
1
+ "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 c=Object.defineProperty;var t=(s,i)=>c(s,"name",{value:i,configurable:!0});var n=Object.defineProperty,a=t((s,i)=>n(s,"name",{value:i,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createCors=createCors;function createCors(s,i){return{setCors:a((r,o)=>{const e=o.headers.origin;if(e&&s.corsCredentialOrigins.has(e.toLowerCase())){r.header("access-control-allow-origin",e),r.header("access-control-allow-credentials","true"),r.header("vary","Origin");return}r.header("access-control-allow-origin",String(e??"*")),r.header("vary","Origin")},"setCors"),applyResponseCors:a((r,o)=>{const e=r.headers.origin;e&&!o.getHeader("access-control-allow-origin")&&(s.corsCredentialOrigins.has(e.toLowerCase())?(o.header("access-control-allow-origin",e),o.header("access-control-allow-credentials","true"),o.header("vary","Origin")):i.get(r)?.proxied||(o.header("access-control-allow-origin",String(e)),o.header("vary","Origin")))},"applyResponseCors")}}t(createCors,"createCors"),a(createCors,"createCors");
1
+ "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
- export declare const ENCODED_STRUCTURE: RegExp;
14
- /** Canonical path for auth matching, or undefined if the path is unsafe (→ 400 / drop). */
15
- export declare const canonicalPath: (rawUrl: string) => string | undefined;
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 c=Object.defineProperty;var a=(r,t)=>c(r,"name",{value:t,configurable:!0});var i=Object.defineProperty,o=a((r,t)=>i(r,"name",{value:t,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.canonicalPath=exports.ENCODED_STRUCTURE=exports.sendError=exports.hdr=exports.eid=void 0;const node_crypto_1=require("node:crypto"),gateway_errors_1=require("../gateway-errors"),eid=o(()=>Date.now().toString(36)+(0,node_crypto_1.randomBytes)(6).toString("hex"),"eid");exports.eid=eid;const hdr=o(r=>typeof r=="string"&&r!==""?r:Array.isArray(r)?r[0]:void 0,"hdr");exports.hdr=hdr;const sendError=o((r,t,e,n,s)=>{const d=(0,gateway_errors_1.ensureRequestId)(t);return r.code(e).header("x-request-id",d).type(gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE).send((0,gateway_errors_1.gatewayErrorBody)(e,n,d,s))},"sendError");exports.sendError=sendError,exports.ENCODED_STRUCTURE=/%2e|%2f|%5c|%00|\\/i;const NEEDS_NORMALIZE=/[%;#\\]/,TRAVERSAL=/(^|\/)\.\.(?=\/|$)/,canonicalPath=o(r=>{const t=r.indexOf("?"),e=t===-1?r:r.slice(0,t);if(e.indexOf("\0")!==-1)return;if(!NEEDS_NORMALIZE.test(e))return TRAVERSAL.test(e)?void 0:e||"/";if(exports.ENCODED_STRUCTURE.test(e))return;let n;try{n=decodeURIComponent(e)}catch{return}if(!(n.includes("\0")||TRAVERSAL.test(n)))return n.split("#")[0].split("/").map(s=>s.split(";")[0]).join("/")||"/"},"canonicalPath");exports.canonicalPath=canonicalPath;
1
+ "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 D=Object.defineProperty;var P=(m,a)=>D(m,"name",{value:a,configurable:!0});var j=Object.defineProperty,v=P((m,a)=>j(m,"name",{value:a,configurable:!0}),"v");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHandler=createHandler;const manifest_1=require("../manifest"),gateway_errors_1=require("../gateway-errors"),edge_1=require("./edge");function createHandler(m){const{rt:a,obs:c,cors:S,auth:O,upstream:_,rollout:w,hooks:l,reqState:N,spanByReq:M,ctxByReq:C}=m,A=!!a.opts.pods?.resolveVersion,H=!!a.opts.cookie,I=!!a.opts.pods?.bucket&&(H||!!a.opts.uidHeader),L=A||H||I,U=v((p,o,r,f)=>{const h=p.raw;if(S.setCors(o,h),h.method==="OPTIONS"){o.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(h.headers["access-control-request-headers"]??"*")).send();return}const i=a.table.routes.filter(n=>n.host&&(0,manifest_1.hostMatches)(n.host,r)&&n.prefix),s=f.split("?")[0];if(i.length>0&&(s==="/health"||s==="/healthz")){o.code(200).type("application/json").send(JSON.stringify({status:"ok",host:r,services:i.map(n=>({service:n.service,prefix:n.prefix}))}));return}if(s==="/.well-known/lensmcp-routes"){o.code(200).type("application/json").send(JSON.stringify({host:r,routes:a.table.routes.filter(n=>!n.host||(0,manifest_1.hostMatches)(n.host,r)).map(n=>({host:n.host??"(default)",service:n.service,prefix:n.prefix,internal:!!n.internal,auth:n.auth}))}));return}(0,edge_1.sendError)(o,h,404,"not_found",i.length>0?`No service mounted at ${f} on ${r}.`:"No gateway route for this hostname.")},"handleNoRoute");return async(p,o)=>{const r=p.raw,f=(r.headers.host||"").split(":")[0],h=r.url??"/",i=c.beginTrace(r);(0,gateway_errors_1.ensureRequestId)(r),c.step(i,"received",{host:f,method:r.method,url:h});const s=l.any?l.httpContext(r,p,o,i):void 0;if(s&&C.set(p,s),s&&l.has("onRequest")&&(await l.run("onRequest",s),s.responded()))return o;if(r.httpVersionMajor<1)return c.finish(i,"gateway",400),(0,edge_1.sendError)(o,r,400,"bad_request","Bad Request");const n=(0,edge_1.canonicalPath)(h);if(n===void 0)return c.step(i,"bad-path",{}),c.finish(i,"gateway",400),(0,edge_1.sendError)(o,r,400,"bad_request","Bad Request");if(a.opts.rateLimiter){const e=await a.opts.rateLimiter.check(w.clientIpOf(r,r.socket?.remoteAddress??p.ip));if(!e.allowed)return o.header("retry-after",String(Math.ceil(e.retryAfterMs/1e3))),c.step(i,"rate-limited",{}),c.finish(i,"gateway",429),(0,edge_1.sendError)(o,r,429,"rate_limited","Too Many Requests")}const t=(0,manifest_1.matchRoute)(a.table.routes,f,h);if(!t)return U(p,o,f,h),o;if(s&&(s.route=t),c.step(i,"route-match",{project:t.service,...t.prefix?{prefix:t.prefix}:{},...t.internal?{internal:!0}:{}}),r.method==="OPTIONS")return S.setCors(o,r),o.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(r.headers["access-control-request-headers"]??"*")).send(),o;const g=a.opts.tracing?.startSpan(r,`gateway ${t.service}`,{"http.request.method":r.method??"GET","url.path":h.split("?")[0],"server.address":f,"lensmcp.service":t.service,...t.internal?{"lensmcp.internal":!0}:{}});if(g&&M.set(p,g),s&&l.has("beforeAuth")&&(await l.run("beforeAuth",s),s.responded()))return o;const b=t.internal?void 0:a.opts.identify?.(r);let E;if(t.internal){const e=O.internal(r,t,i);if(!e.ok)return c.finish(i,t.service,e.status),(0,edge_1.sendError)(o,r,e.status,e.reason,e.detail);E=e.caller,s&&(s.caller=E)}else{const e=O.public(r,t,n,b,p.ip,i);if(!e.ok)return c.finish(i,t.service,e.status),(0,edge_1.sendError)(o,r,e.status,e.reason,e.detail);if(s&&(s.claims=b),s&&l.has("afterAuth")&&(await l.run("afterAuth",s),s.responded()))return o}if(a.opts.serviceKeys?.[t.service]&&(r.headers["x-internal-token"]=a.opts.serviceKeys[t.service]),t.prependPrefix){const e=r.url??"/",d=t.prependPrefix;e!==d&&!e.startsWith(d+"/")&&(r.url=d+e)}const k=(0,edge_1.hdr)(r.headers[a.versionHeader]);let x=k,q;if(L&&!t.internal&&!k){const e=w.resolveStickyKey(r);e.mint&&(q=e.key);const d=A?a.opts.pods.resolveVersion(t.service,w.buildAttrs(r,p.ip,b,e.key)):void 0;if(d)x=d.version,c.step(i,"targeting",{rule:d.rule,version:d.version});else if(e.key){const y=a.opts.pods?.bucket?.(t.service,e.key);y&&(x=y.version,c.step(i,"targeting",{mode:"sticky",version:y.version}))}}const u=await _.resolveUpstream(t,i,x);if(!u)return c.finish(i,t.service,503),(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${t.service} has no available upstream.`);if(!("url"in u))return c.finish(i,t.service,502),(0,edge_1.sendError)(o,r,502,"unavailable",`Service ${t.service}: socket upstreams are not supported by the prod gateway.`);const $=new URL(u.url).origin,T=u.url,R={route:t,caller:E,startedAt:Date.now(),trace:i,version:u.version,upLabel:T};return N.set(p,R),s&&(s.version=u.version),c.step(i,u.version?"rollout":"upstream",{upstream:T,...u.version?{version:u.version,...k?{pinned:!0}:{}}:{}}),g&&a.opts.tracing.inject(g,r.headers),s&&l.has("beforeForward")&&(await l.run("beforeForward",s),s.responded())?o:o.from(void 0,{getUpstream:v(()=>$,"getUpstream"),rewriteHeaders:v(e=>{if(R.proxied=!0,e["x-lensmcp-upstream"]=T,e["x-request-id"]||(e["x-request-id"]=(0,gateway_errors_1.ensureRequestId)(r)),u.version&&(e["x-lensmcp-version"]=u.version),delete e["x-powered-by"],delete e.connection,delete e["keep-alive"],delete e["proxy-connection"],e.server="LensMCP",q){const d=e["set-cookie"],y=Array.isArray(d)?d:d?[d]:[];e["set-cookie"]=[...y,w.mintCookieHeader(q)]}return e},"rewriteHeaders"),onError:v((e,{error:d})=>{a.opts.pods?.drop?.(t.service,u),s&&l.has("onError")&&(s.error=d,l.run("onError",s)),(0,edge_1.sendError)(e,r,502,"unavailable",`Upstream ${t.service} unavailable.`)},"onError")})}}P(createHandler,"createHandler"),v(createHandler,"createHandler");
1
+ "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 { type Attrs } from '../manifest';
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 fallback. */
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
- /** Assemble the flat attribute bag a rule evaluates against. */
8
- buildAttrs(req: http.IncomingMessage, fallbackIp: string, principal: Record<string, unknown> | undefined, key: string | undefined): Attrs;
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 H=Object.defineProperty;var u=(i,c)=>H(i,"name",{value:c,configurable:!0});var S=Object.defineProperty,r=u((i,c)=>S(i,"name",{value:c,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRollout=createRollout;const node_crypto_1=require("node:crypto"),manifest_1=require("../manifest"),edge_1=require("./edge");function createRollout(i){const c=i.clientIpHeader?.toLowerCase(),l=i.clientIpTrustedProxies??[],y=r(e=>{if(!e)return!1;const t=e.startsWith("::ffff:")?e.slice(7):e;return l.length===0?t==="127.0.0.1"||e==="::1":l.some(o=>(0,manifest_1.matchCidr)(t,o))},"peerTrusted"),f=r((e,t)=>{if(c&&y(e.socket?.remoteAddress??void 0)){const o=(0,edge_1.hdr)(e.headers[c]);if(o)return o}return t},"clientIpOf"),C=(i.attributeHeaders??[]).map(e=>e.toLowerCase()),m=i.uidHeader?.toLowerCase(),d=!!i.cookie,p=i.cookie?.name??"lensmcp_did",a=i.cookie?.secret,k=i.cookie?.domain,I=i.cookie?.maxAge??31536e3;d&&!a&&console.warn("[gateway] device cookie has no secret \u2014 values are UNSIGNED (set cookie.secret to prevent bucket-grinding).");const h=r(e=>(0,node_crypto_1.createHmac)("sha256",a).update(e).digest("base64url"),"signId"),b=r(e=>a?`${e}.${h(e)}`:e,"signedCookie"),x=r(e=>{if(!a)return e||void 0;const t=e.lastIndexOf(".");if(t<0)return;const o=e.slice(0,t),n=Buffer.from(e.slice(t+1)),s=Buffer.from(h(o));return n.length===s.length&&(0,node_crypto_1.timingSafeEqual)(n,s)?o:void 0},"verifyCookie"),_=r((e,t)=>{if(e)for(const o of e.split(";")){const n=o.indexOf("=");if(n>0&&o.slice(0,n).trim()===t)return decodeURIComponent(o.slice(n+1).trim())}},"parseCookie");return{clientIpOf:f,buildAttrs:r((e,t,o,n)=>{const s={...o??{}};s.ip=f(e,t),s.method=(e.method??"GET").toUpperCase(),s.path=(e.url??"/").split("?")[0],n&&(s.device=n);for(const g of C){const v=(0,edge_1.hdr)(e.headers[g]);v!==void 0&&(s["header."+g]=v)}return s},"buildAttrs"),resolveStickyKey:r(e=>{if(d){const t=_(e.headers.cookie,p),o=t?x(t):void 0;if(o)return{key:o,mint:!1}}if(m){const t=(0,edge_1.hdr)(e.headers[m]);if(t)return{key:t,mint:!1}}return d?{key:"d_"+(0,node_crypto_1.randomBytes)(12).toString("base64url"),mint:!0}:{mint:!1}},"resolveStickyKey"),mintCookieHeader:r(e=>{let t=`${p}=${encodeURIComponent(b(e))}; Path=/; Max-Age=${I}; HttpOnly; SameSite=Lax; Secure`;return k&&(t+=`; Domain=${k}`),t},"mintCookieHeader")}}u(createRollout,"createRollout"),r(createRollout,"createRollout");
1
+ "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 d=Object.defineProperty;var u=(e,o)=>d(e,"name",{value:o,configurable:!0});var h=Object.defineProperty,r=u((e,o)=>h(e,"name",{value:o,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRouteTable=createRouteTable;const manifest_1=require("../manifest");function createRouteTable(e,o){const n=[],c=new Map;let i=e.manifests.list();const l=r(a=>{i=a;const t=(0,manifest_1.buildRouteTable)(a);n.length=0,n.push(...t),c.clear();for(const s of a)c.set(s.service,s)},"apply"),f=r(a=>{if(l(a),e.healthChecker){const t=new Set;for(const s of n)s.upstream&&t.add(s.upstream);for(const s of e.pods?.endpoints?.()??[])t.add(s);e.healthChecker.track([...t])}o.emit("info",`gateway routes: ${n.length} on ${i.length} services`,"gateway-up",{kind:"gateway-up",routes:n.map(t=>({host:t.host??"(default)",service:t.service,internal:!!t.internal}))})},"rebuild");l(i);const p=e.manifests.watch?.(a=>f(a));return{routes:n,manifestByService:c,rebuild:f,manifests:r(()=>i,"manifests"),stop:r(()=>p?.(),"stop")}}u(createRouteTable,"createRouteTable"),r(createRouteTable,"createRouteTable");
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 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");
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) via
139
- * Fastify `trustProxy`; default false socket IP. */
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` enables HMAC signing. */
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 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
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
- `),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");
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
- resolveUpstream(route: Route, trace: GwTrace | undefined, pinned?: string): Promise<Upstream | undefined>;
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 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");
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): PodProvider & {
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[]>): PodProvider & {
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 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");
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,s=u((e,t)=>y(e,"name",{value:t,configurable:!0}),"s");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=s((e,t)=>`${e}:manifest:${t}`,"manifestKey");exports.manifestKey=manifestKey;const rolloutKey=s((e,t)=>`${e}:rollout:${t}`,"rolloutKey");exports.rolloutKey=rolloutKey;const changeChannel=s(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"),s(parseManifest,"parseManifest");function redisRegistrySource(e,t={}){const o=t.prefix??exports.DEFAULT_REGISTRY_PREFIX;let a;const i=`${o}:rollout:`;return{async read(){const c=await e.keys(`${o}:manifest:*`),d=c.length?(await e.mget(...c)).filter(r=>!!r).map(parseManifest).filter(r=>!!r):[],n=await e.keys(`${i}*`),l={};if(n.length){const r=await e.mget(...n);n.forEach((p,f)=>{const h=r[f];if(h)try{l[p.slice(i.length)]=JSON.parse(h)}catch{}})}return{manifests:d,endpoints:l}},async watch(c){return a=e.duplicate(),await a.subscribe((0,exports.changeChannel)(o)),a.on("message",()=>c()),async()=>{try{await a?.quit()}catch{}}},stop(){a?.quit().catch(()=>{})}}}u(redisRegistrySource,"redisRegistrySource"),s(redisRegistrySource,"redisRegistrySource");function createRegistryProviders(e,t={}){const o=t.onError??(()=>{});let a=[];const i=new Set,c=(0,providers_prod_1.rolloutPodProvider)({});let d,n;const l=s(async()=>{let r;try{r=await e.read()}catch(p){o(p);return}a=r.manifests,c.update(r.endpoints);for(const p of i)try{p(a)}catch(f){o(f)}},"refresh");return{manifests:{list:s(()=>a,"list"),watch:s(r=>(i.add(r),()=>i.delete(r)),"watch")},pods:c,async start(){if(await l(),e.watch)try{d=await e.watch(()=>{l()})}catch(r){o(r)}n=setInterval(()=>{l()},t.pollMs??5e3),n.unref?.()},stop(){d?.(),n&&clearInterval(n),e.stop?.()}}}u(createRegistryProviders,"createRegistryProviders"),s(createRegistryProviders,"createRegistryProviders");
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 u=(t,r)=>d(t,"name",{value:r,configurable:!0});var h=Object.defineProperty,s=u((t,r)=>h(t,"name",{value:r,configurable:!0}),"s");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=s(t=>JSON.parse(JSON.stringify(t)),"clone");function asSpec(t,r){if(r===void 0)return{rules:[],cohorts:[]};if(Array.isArray(r)){if(r.length&&typeof r[0]=="string")throw new Error(`service "${t}" 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??[]}}u(asSpec,"asSpec"),s(asSpec,"asSpec");const findCohort=s((t,r)=>t.cohorts.find(o=>o.version===r),"findCohort");function reqWeight(t){if(!Number.isFinite(t)||t<0)throw new Error(`weight must be a number \u2265 0 (got ${t})`);return t}u(reqWeight,"reqWeight"),s(reqWeight,"reqWeight");function applyRolloutOp(t,r){const o=clone(t),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 n=findCohort(e,r.version);if(!n)throw new Error(`no cohort "${r.version}" on "${r.service}"`);n.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(n=>n.version!==r.version);break}case"promote":{if(!findCohort(e,r.version))throw new Error(`no cohort "${r.version}" on "${r.service}"`);for(const n of e.cohorts)n.weight=n.version===r.version?100:0;break}case"abort":{const n=findCohort(e,r.version);if(!n)throw new Error(`no cohort "${r.version}" on "${r.service}"`);n.weight=0;break}case"add-rule":{validateRule(r.rule),e.rules=[...(e.rules??[]).filter(n=>!r.rule.name||n.name!==r.rule.name),r.rule];break}case"remove-rule":{const n=(e.rules??[]).length;if(e.rules=(e.rules??[]).filter(i=>i.name!==r.name),e.rules.length===n)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}u(applyRolloutOp,"applyRolloutOp"),s(applyRolloutOp,"applyRolloutOp");function validateRule(t){if(!t||typeof t.version!="string"||!t.version)throw new Error("rule needs a non-empty version");if(!t.when||typeof t.when!="object")throw new Error("rule needs a `when` condition");const r=s(o=>{const e=o;if("all"in e||"any"in e||"not"in e){for(const n of["all","any"])Array.isArray(e[n])&&e[n].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(t.when)}u(validateRule,"validateRule"),s(validateRule,"validateRule");function summarizeRollout(t,r){const o=r?[r]:Object.keys(t);if(!o.length)return"(no services in rollout config)";const e=[];for(const n of o){const i=t[n];if(i===void 0){e.push(`${n}: (not present)`);continue}if(Array.isArray(i)&&i.length&&typeof i[0]=="string"){e.push(`${n}: flat LB \u2192 ${i.join(", ")}`);continue}const c=asSpec(n,i),l=c.cohorts.reduce((a,v)=>a+Math.max(0,v.weight),0)||1;e.push(`${n}:`);for(const a of c.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 c.rules??[])e.push(` \u25B8 rule ${(a.name??"(unnamed)").padEnd(16)} \u2192 ${a.version} ${ruleSummary(a.when)}`)}return e.join(`
2
- `)}u(summarizeRollout,"summarizeRollout"),s(summarizeRollout,"summarizeRollout");function ruleSummary(t){const r=t;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):""}`}u(ruleSummary,"ruleSummary"),s(ruleSummary,"ruleSummary");function parseRolloutCommand(t){const[r,...o]=t,e=s((n,i)=>{if(o.length<n)throw new Error(`usage: ${r} ${i}`)},"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":return e(6,"<service> <name> <attr> <op> <value> <version>"),{op:{kind:"add-rule",service:o[0],rule:{name:o[1],when:{attr:o[2],op:o[3],value:o[4]},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`)}}u(parseRolloutCommand,"parseRolloutCommand"),s(parseRolloutCommand,"parseRolloutCommand");
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
- public(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
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