@lensmcp/cluster 1.24.1 → 1.24.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/checkout-cache.d.ts +5 -0
- package/checkout-cache.js +1 -0
- package/create-webpack-dev.d.ts +1 -1
- package/create-webpack-dev.js +1 -1
- package/devserver-notifier.d.ts +1 -1
- package/devserver-notifier.js +1 -1
- package/devserver-transport.d.ts +41 -0
- package/devserver-transport.js +1 -0
- package/executors/gateway/manifest.js +1 -1
- package/executors/gateway/runtime/auth.d.ts +7 -0
- package/executors/gateway/runtime/auth.js +1 -1
- package/executors/gateway/runtime/chooser.d.ts +2 -1
- package/executors/gateway/runtime/chooser.js +23 -10
- package/executors/gateway/runtime/control.d.ts +14 -0
- package/executors/gateway/runtime/control.js +2 -2
- package/executors/gateway/runtime/discovery.d.ts +15 -2
- package/executors/gateway/runtime/discovery.js +1 -1
- package/executors/gateway/runtime/edge.d.ts +7 -0
- package/executors/gateway/runtime/edge.js +1 -1
- package/executors/gateway/runtime/guest-replay.d.ts +22 -0
- package/executors/gateway/runtime/guest-replay.js +2 -0
- package/executors/gateway/runtime/instance.d.ts +104 -0
- package/executors/gateway/runtime/instance.js +2 -0
- package/executors/gateway/runtime/lens-children.js +1 -1
- package/executors/gateway/runtime/lifecycle.d.ts +2 -0
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/pod-probe.d.ts +17 -0
- package/executors/gateway/runtime/pod-probe.js +1 -1
- package/executors/gateway/runtime/proxy.d.ts +14 -0
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/registration-guards.js +1 -1
- package/executors/gateway/runtime/route-registry.d.ts +7 -0
- package/executors/gateway/runtime/scope.d.ts +18 -0
- package/executors/gateway/runtime/scope.js +1 -1
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/service-keys.d.ts +12 -3
- package/executors/gateway/runtime/service-keys.js +1 -1
- package/executors/gateway/runtime/shared-build.d.ts +1 -1
- package/executors/gateway/runtime/shared-build.js +3 -3
- package/executors/gateway/runtime/types.d.ts +47 -0
- package/executors/gateway/runtime/types.js +1 -1
- package/executors/gateway/runtime/upgrade.d.ts +13 -8
- package/executors/gateway/runtime/upgrade.js +1 -1
- package/executors/gateway/runtime/workspace-middleware.d.ts +38 -0
- package/executors/gateway/runtime/workspace-middleware.js +1 -0
- package/executors/gateway/runtime/workspace-registry.d.ts +7 -0
- package/executors/serve/serve.impl.js +1 -1
- package/executors/trust/trust.impl.js +3 -3
- package/inspector-secret.d.ts +6 -0
- package/inspector-secret.js +1 -1
- package/main.devserver.d.ts +4 -38
- package/main.devserver.js +10 -10
- package/package.json +4 -4
- package/shared-build/host.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var G=Object.defineProperty;var j=(t,c)=>G(t,"name",{value:c,configurable:!0});var K=Object.defineProperty,s=j((t,c)=>K(t,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),body_stall_1=require("../body-stall"),types_1=require("./types"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys");function isConnError(t){const c=t.code??"";return c==="ECONNREFUSED"||c==="ECONNRESET"||c==="ETIMEDOUT"||c==="EPIPE"||c==="EHOSTUNREACH"||c==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(t.message)}j(isConnError,"isConnError"),s(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(t){if(!IDEMPOTENT_METHODS.has((t.method??"GET").toUpperCase())||t.headers["transfer-encoding"]!==void 0)return!1;const c=t.headers["content-length"];return!(c!==void 0&&String(c).trim()!=="0")}j(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(t,c){return c.headersSent||c.writableEnded||c.destroyed===!0||t.aborted===!0}j(edgeGone,"edgeGone"),s(edgeGone,"edgeGone");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),UPSTREAM_CONNECTION_HEADERS=["connection","keep-alive","proxy-connection"],httpProxy=require("http-proxy");function declaredFirstByteMs(t){const c=Number(t);return Number.isFinite(c)&&c>0?c:types_1.POD_RESPONSE_TIMEOUT_MS}j(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");function armFirstByteGuard(t){let c,$=!1,q=!1,w=(0,body_stall_1.requestBodyArriving)(t.req)?void 0:Date.now();const O=s(()=>{w??=Date.now()},"onBodyEnd");w===void 0&&t.req.once("end",O);const U=s(()=>(w===void 0&&!(0,body_stall_1.requestBodyArriving)(t.req)&&(w=Date.now()),w===void 0?0:Date.now()-w),"heldMs"),D=s(S=>{c=setTimeout(A,S),c.unref?.()},"arm"),A=s(()=>{if($||!t.pending())return;const S=U();if(t.ceilingMs<=0){if(w!==void 0&&S>=t.firstByteMs){t.onWedged(`no response within ${S}ms (wedged socket) \u2014 evicted + respawning`);return}D(w===void 0?t.firstByteMs:t.firstByteMs-S);return}(0,pod_probe_1.probePodAlive)(t.socketPath,void 0,t.serviceKey).then(x=>{if($||!t.pending())return;const T=U();if(!x){t.onWedged(`no response within ${T}ms and no answer on ${types_1.POD_ALIVE_PATH} (wedged socket) \u2014 evicted + respawning`);return}if(T>=t.ceilingMs){t.onCeiling(`alive on ${types_1.POD_ALIVE_PATH} but no response within ${T}ms (ceiling ${t.ceilingMs}ms) \u2014 giving up on this request, pod kept`,x);return}!q&&w!==void 0&&(q=!0,t.onSlow(T,x)),D(Math.max(t.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))})},"onDeadline");return D(t.firstByteMs),{clear:s(()=>{$=!0,c&&clearTimeout(c),t.req.removeListener("end",O)},"clear")}}j(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(t,c,$){const{recordEdge:q,finishTrace:w,traceStep:O,emit:U}=c,D=s(async e=>{if(!e.lens||!t.lensSupervisor)return"unsupported";let r;const o=new Promise(p=>{r=setTimeout(()=>p("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),g=t.lensSupervisor.recover(e.lens,"proxy-connection").then(p=>p?"ready":"failed").catch(()=>"failed"),h=await Promise.race([g,o]);r&&clearTimeout(r);const m=t.lensSupervisor.status(e.lens);return h==="failed"&&(!m||m.state==="ready")?"unsupported":h},"waitForManagedRecovery"),A=s((e,r,o,g,h,m)=>{if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}const p=e.lens?t.lensSupervisor?.status(e.lens):void 0,E=m==="timeout"||p?.state==="starting"||p?.state==="replacing",v=(0,gateway_errors_1.ensureRequestId)(r),d=e.lens?{project:e.project,reason:p?.reason??"connection-failure",generation:p?.generation??0,...E?{retryAfterMs:2e3}:{}}:void 0,n=E?503:502,a=E?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${v} project=${e.project} target=${String(g)} state=${p?.state??"unmanaged"} generation=${p?.generation??0}: ${h.message}`),U("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:v,project:e.project,target:String(g),state:p?.state??"unmanaged",generation:p?.generation,error:h.message}),(0,edge_1.sendError)(o,r,n,a,E?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,d,E?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),S=(0,body_stall_1.createBodyStallGuard)(t.bodyStallMs??types_1.BODY_STALL_MS),x={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},T=new http.Agent(x),I=new(require("node:https")).Agent(x),L=s(e=>typeof e=="string"&&e.startsWith("https:")?I:T,"agentForTarget"),C=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:T}),N=s((e,r,o)=>{const g=s(()=>{o()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",g),e.once("close",()=>r.removeListener("close",g))},"propagateEdgeCancel");C.on("proxyReq",(e,r,o)=>{for(const g of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(g);e.setHeader("connection","keep-alive"),N(e,o,()=>o.writableEnded)}),C.on("proxyRes",(e,r,o)=>{const g=r.__lensmcpPod;g&&(e.headers["x-lensmcp-pod"]=g),delete e.headers["x-powered-by"],e.headers.server="LensMCP",o&&N(e,o,()=>o.writableEnded),o&&S.armResponse(o)});const H=s((e,r,o,g)=>{(0,edge_1.stampForwardingHeaders)(t,r),S.arm(r);const h={},m=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[i,f]of Object.entries(r.headers))f===void 0||i.startsWith(":")||HOP_BY_HOP.has(i)||m&&(i==="content-length"||i==="transfer-encoding")||(h[i]=f);let p=http,E;if(typeof e=="object")E={socketPath:e.socketPath,path:r.url,method:r.method,headers:h,agent:T};else{const i=new URL(e),f=i.protocol==="https:";p=f?require("node:https"):http,h.host=i.host,E={protocol:i.protocol,hostname:i.hostname,port:i.port||(f?443:80),path:r.url,method:r.method,headers:h,agent:L(e)}}const v=o;v.__lensmcpErrContained||(v.__lensmcpErrContained=!0,o.on("error",()=>{}));let d;const n=s(()=>{o.writableEnded||u.destroy()},"onReqGone"),a=s(()=>{d&&!d.destroyed&&d.destroy()},"onEdgeClose"),l=s(()=>{r.removeListener("error",n),r.removeListener("aborted",n),r.removeListener("close",n),o.removeListener("close",a)},"detach"),u=p.request(E,i=>{if(d=i,g.onResponse(),o.headersSent||o.writableEnded||o.destroyed){i.destroy();return}const f={};for(const[P,y]of Object.entries(i.headers))y===void 0||HOP_BY_HOP.has(P)||(f[P]=y);delete f["x-powered-by"],f.server="LensMCP";const R=r.__lensmcpPod;R&&(f["x-lensmcp-pod"]=R);try{o.writeHead(i.statusCode??502,f)}catch(P){console.error("[gateway] h2 relay: bad response header, resetting stream:",P.message),i.destroy(),o.destroyed||o.destroy(P);return}i.pipe(o),S.armResponse(o);const k=s(()=>{!o.writableEnded&&!o.destroyed&&o.destroy()},"abortEdge");i.once("error",k),i.once("close",k),o.on("close",a)});u.once("error",i=>g.onError(i)),u.once("close",l),r.on("error",n),r.on("aborted",n),r.on("close",n),m?u.end():r.pipe(u)},"pipeUpstream"),W=s(async(e,r,o)=>{const g=r.__lensmcpTrace;if(e.pool){const d=e.svc;d&&(d.lastUsed=Date.now(),d.inflight+=1,o.on("close",()=>{d.inflight-=1,d.lastUsed=Date.now()}));let n=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs);if(!n&&d&&(O(g,"cold-start",{project:e.project}),await $.ensureUp(d)&&(n=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs))),d&&n&&$.markUp(d,"observed"),d&&$.scaleUp(d),!n){const y=d?.lastErrors?.length?` Service errors: ${d.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${y} The watcher recovers and the next request retries automatically.`);return}const a=path.basename(n,".sock");r.__lensmcpPod=a,O(g,"pod-select",{pod:a,pods:e.pool.socks.length,strategy:"round-robin"});let l=!1;const u=s((y,b="unlink")=>{if(e.pool.socks=e.pool.socks.filter(_=>_!==n),e.pool.idx=-1,b==="unlink")try{fs.unlinkSync(n)}catch{}console.error(`[gateway] pod ${a} of ${e.project}: ${y}`)},"evictPod"),i=armFirstByteGuard({socketPath:n,serviceKey:t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!l&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:s(y=>{l=!0,u(y),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${a} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onCeiling:s(y=>{l=!0,u(y,"keep"),(0,edge_1.sendError)(o,r,504,"unavailable",`Pod ${a} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs.`)},"onCeiling"),onSlow:s((y,b)=>{console.log(`[gateway] pod ${a} of ${e.project}: slow first byte, pod alive (${y}ms, ${b.inflight} in-flight, ${b.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>i.clear());const f=n,R=isReplayableRequest(r);let k=0;const P=s(()=>{k+=1,H({socketPath:f},r,o,{onResponse:s(()=>{i.clear(),l=!0},"onResponse"),onError:s(y=>{if(!l){if(edgeGone(r,o)){i.clear(),l=!0;return}(0,pod_probe_1.podConnectionVerdict)(f,y).then(b=>{if(!l){if(b==="alive"&&k<2&&R&&!edgeGone(r,o)){console.log(`[gateway] pod ${a} of ${e.project}: ${y.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),P();return}if(i.clear(),l=!0,b==="gone"){u(y.message),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${a} of ${e.project} unavailable \u2014 retry.`);return}console.error(`[gateway] pod ${a} of ${e.project}: ${y.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${a} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");P();return}const h=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let m=0,p=!1;const E=isReplayableRequest(r),v=s(()=>{if(edgeGone(r,o))return;const d=e.target;H(d,r,o,{onResponse:s(()=>{},"onResponse"),onError:s(n=>{if(isConnError(n)&&(m+=1),e.lens&&isConnError(n)&&E&&m>=2&&!p&&!edgeGone(r,o)){p=!0,D(e).then(a=>{if(!edgeGone(r,o)){if(a==="ready"){v();return}if(a==="unsupported"&&Date.now()<h){setTimeout(v,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}A(e,r,o,e.target,n,a)}});return}if(isConnError(n)&&E&&Date.now()<h&&!edgeGone(r,o)){setTimeout(v,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}A(e,r,o,d,n)},"onError")})},"tryTcp");v()},"forwardNative");return{proxy:C,forward:s(async(e,r,o)=>{const g=Date.now(),h=r.__lensmcpTrace;if(o.on("close",()=>{q(e,Date.now()-g,o.statusCode??0,r.__lensmcpCaller),w(h,e.project,o.statusCode??0,{...r.__lensmcpPod?{pod:r.__lensmcpPod}:{},...r.__lensmcpCaller?{caller:r.__lensmcpCaller}:{}},e)}),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),(0,edge_1.stampForwardingHeaders)(t,r),S.arm(r),(r.httpVersionMajor??1)>=2){await W(e,r,o);return}if(e.pool){const n=e.svc;n&&(n.lastUsed=Date.now(),n.inflight+=1,o.on("close",()=>{n.inflight-=1,n.lastUsed=Date.now()}));let a=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs);if(!a&&n&&(O(h,"cold-start",{project:e.project}),await $.ensureUp(n)&&(a=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs))),n&&a&&$.markUp(n,"observed"),n&&$.scaleUp(n),!a){const _=n?.lastErrors?.length?` Service errors: ${n.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${_} The watcher recovers and the next request retries automatically.`);return}const l=path.basename(a,".sock");r.__lensmcpPod=l,O(h,"pod-select",{pod:l,pods:e.pool.socks.length,strategy:"round-robin"});let u=!1;const i=s((_,M="unlink")=>{if(e.pool.socks=e.pool.socks.filter(B=>B!==a),e.pool.idx=-1,M==="unlink")try{fs.unlinkSync(a)}catch{}console.error(`[gateway] pod ${l} of ${e.project}: ${_}`)},"evictPod"),f=armFirstByteGuard({socketPath:a,serviceKey:t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!u&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:s(_=>{u=!0,i(_),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${l} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onCeiling:s(_=>{u=!0,i(_,"keep"),(0,edge_1.sendError)(o,r,504,"unavailable",`Pod ${l} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs.`)},"onCeiling"),onSlow:s((_,M)=>{console.log(`[gateway] pod ${l} of ${e.project}: slow first byte, pod alive (${_}ms, ${M.inflight} in-flight, ${M.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>f.clear());const R=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),k=a,P=isReplayableRequest(r);let y=0;const b=s(()=>{y+=1;const _=Date.now();C.web(r,o,{target:{socketPath:k},agent:T,proxyTimeout:R},M=>{if(!u){if(edgeGone(r,o)){f.clear(),u=!0;return}(0,pod_probe_1.podConnectionVerdict)(k,M).then(B=>{if(u)return;const F=Date.now()-_>=R-250;if(B==="alive"&&y<2&&P&&!F&&!edgeGone(r,o)){console.log(`[gateway] pod ${l} of ${e.project}: ${M.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),b();return}if(f.clear(),u=!0,B==="gone"){i(M.message),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${l} of ${e.project} unavailable \u2014 retry.`);return}console.error(`[gateway] pod ${l} of ${e.project}: ${M.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${l} of ${e.project} dropped the connection \u2014 retry.`)})}})},"sendToPod");b();return}const m=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let p=0,E=!1;const v=isReplayableRequest(r),d=s(()=>{if(edgeGone(r,o))return;const n=e.target,a={target:n,autoRewrite:typeof n=="string",changeOrigin:!0,agent:L(n)};C.web(r,o,a,l=>{if(isConnError(l)&&(p+=1),e.lens&&isConnError(l)&&v&&p>=2&&!E&&!edgeGone(r,o)){E=!0,D(e).then(u=>{if(!edgeGone(r,o)){if(u==="ready"){d();return}if(u==="unsupported"&&Date.now()<m){setTimeout(d,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}A(e,r,o,e.target,l,u)}});return}if(isConnError(l)&&v&&Date.now()<m&&!edgeGone(r,o)){setTimeout(d,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}o.destroyed||A(e,r,o,n,l)})},"tryTcp");d()},"forward"),agentForTarget:L,destroyAgents:s(()=>{S.stop(),T.destroy(),I.destroy()},"destroyAgents"),closeProxy:s(()=>{try{C.close?.()}catch{}},"closeProxy")}}j(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
1
|
+
"use strict";var K=Object.defineProperty;var x=(t,c)=>K(t,"name",{value:c,configurable:!0});var G=Object.defineProperty,s=x((t,c)=>G(t,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.wedgedMessage=wedgedMessage,exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),body_stall_1=require("../body-stall"),types_1=require("./types"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys");function isConnError(t){const c=t.code??"";return c==="ECONNREFUSED"||c==="ECONNRESET"||c==="ETIMEDOUT"||c==="EPIPE"||c==="EHOSTUNREACH"||c==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(t.message)}x(isConnError,"isConnError"),s(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(t){if(!IDEMPOTENT_METHODS.has((t.method??"GET").toUpperCase())||t.headers["transfer-encoding"]!==void 0)return!1;const c=t.headers["content-length"];return!(c!==void 0&&String(c).trim()!=="0")}x(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(t,c){return c.headersSent||c.writableEnded||c.destroyed===!0||t.aborted===!0}x(edgeGone,"edgeGone"),s(edgeGone,"edgeGone");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),UPSTREAM_CONNECTION_HEADERS=["connection","keep-alive","proxy-connection"],httpProxy=require("http-proxy");function declaredFirstByteMs(t){const c=Number(t);return Number.isFinite(c)&&c>0?c:types_1.POD_RESPONSE_TIMEOUT_MS}x(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");const secs=s(t=>`${(t/1e3).toFixed(1)}s`,"secs");function wedgedMessage(t,c,E){const U=E.failure==="gone"?"its process is gone":E.failure==="silent"?`it did not answer its liveness check for ${secs(E.silentMs)}`:"it gave no liveness answer";return`Pod ${t} of ${c} sent no response for ${secs(E.heldMs)} and ${U} \u2014 taken out of rotation, and the service is being restarted, so work it was still doing for this request may have been cut short. Retry if the call is safe to repeat.`}x(wedgedMessage,"wedgedMessage"),s(wedgedMessage,"wedgedMessage");function armFirstByteGuard(t){let c,E=!1,U=!1,q,M=0,P=(0,body_stall_1.requestBodyArriving)(t.req)?void 0:Date.now();const b=s(()=>{P??=Date.now()},"onBodyEnd");P===void 0&&t.req.once("end",b);const N=s(()=>(P===void 0&&!(0,body_stall_1.requestBodyArriving)(t.req)&&(P=Date.now()),P===void 0?0:Date.now()-P),"heldMs"),k=s(j=>{c=setTimeout(C,j),c.unref?.()},"arm"),C=s(()=>{if(E||!t.pending())return;const j=N();if(t.ceilingMs<=0){if(P!==void 0&&j>=t.firstByteMs){t.onWedged(`no response within ${j}ms (wedged socket) \u2014 evicted + respawning`,{heldMs:j,silentMs:0,probes:0,failure:"deadline"});return}k(P===void 0?t.firstByteMs:t.firstByteMs-j);return}(0,pod_probe_1.probePodAliveVerdict)(t.socketPath,void 0,t.serviceKey).then(({alive:D,failure:I})=>{if(E||!t.pending())return;const R=N();if(!D){if(M+=1,I==="silent"&&types_1.POD_WEDGE_CONFIRM_MS>0&&(q??=Date.now(),Date.now()-q<types_1.POD_WEDGE_CONFIRM_MS)){k(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}const O=q===void 0?0:Date.now()-q,L=I==="gone"?"its socket no longer accepts (the pod process is gone)":I==="silent"?`nothing answered on ${types_1.POD_ALIVE_PATH} for ${O}ms (${M} probe${M===1?"":"s"})`:`no liveness answer on ${types_1.POD_ALIVE_PATH}`;t.onWedged(`no response within ${R}ms and ${L} (wedged socket) \u2014 evicted + respawning`,{heldMs:R,silentMs:O,probes:M,failure:I??"no-route"});return}if(q=void 0,R>=t.ceilingMs){t.onCeiling(`alive on ${types_1.POD_ALIVE_PATH} but no response within ${R}ms (ceiling ${t.ceilingMs}ms) \u2014 giving up on this request, pod kept`,D);return}!U&&P!==void 0&&(U=!0,t.onSlow(R,D)),k(Math.max(t.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))})},"onDeadline");return k(t.firstByteMs),{clear:s(()=>{E=!0,c&&clearTimeout(c),t.req.removeListener("end",b)},"clear")}}x(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(t,c,E){const{recordEdge:U,finishTrace:q,traceStep:M,emit:P}=c,b=s((e,r,o,p)=>{const h=p.kind==="pod-evicted";P(h?"error":"warning",`${h?"pod evicted":"request given up"}: ${e.project} (${o}) \u2014 ${p.status}`,`cluster-pod-incident:${e.project}`,{kind:p.kind,project:e.project,pod:o,status:p.status,reason:p.reason,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0],...p.detail??{}})},"reportPodIncident"),N=s(async e=>{if(!e.lens||!t.lensSupervisor)return"unsupported";let r;const o=new Promise(g=>{r=setTimeout(()=>g("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),p=t.lensSupervisor.recover(e.lens,"proxy-connection").then(g=>g?"ready":"failed").catch(()=>"failed"),h=await Promise.race([p,o]);r&&clearTimeout(r);const w=t.lensSupervisor.status(e.lens);return h==="failed"&&(!w||w.state==="ready")?"unsupported":h},"waitForManagedRecovery"),k=s((e,r,o,p,h,w)=>{if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}const g=e.lens?t.lensSupervisor?.status(e.lens):void 0,v=w==="timeout"||g?.state==="starting"||g?.state==="replacing",S=(0,gateway_errors_1.ensureRequestId)(r),l=e.lens?{project:e.project,reason:g?.reason??"connection-failure",generation:g?.generation??0,...v?{retryAfterMs:2e3}:{}}:void 0,n=v?503:502,i=v?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${S} project=${e.project} target=${String(p)} state=${g?.state??"unmanaged"} generation=${g?.generation??0}: ${h.message}`),P("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:S,project:e.project,target:String(p),state:g?.state??"unmanaged",generation:g?.generation,error:h.message}),(0,edge_1.sendError)(o,r,n,i,v?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,l,v?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),C=(0,body_stall_1.createBodyStallGuard)(t.bodyStallMs??types_1.BODY_STALL_MS),j={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},D=new http.Agent(j),I=new(require("node:https")).Agent(j),R=s(e=>typeof e=="string"&&e.startsWith("https:")?I:D,"agentForTarget"),O=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:D}),L=s((e,r,o)=>{const p=s(()=>{o()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",p),e.once("close",()=>r.removeListener("close",p))},"propagateEdgeCancel");O.on("proxyReq",(e,r,o)=>{for(const p of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(p);e.setHeader("connection","keep-alive"),L(e,o,()=>o.writableEnded)}),O.on("proxyRes",(e,r,o)=>{const p=r.__lensmcpPod;p&&(e.headers["x-lensmcp-pod"]=p),delete e.headers["x-powered-by"],e.headers.server="LensMCP",o&&L(e,o,()=>o.writableEnded),o&&C.armResponse(o)});const W=s((e,r,o,p)=>{(0,edge_1.stampForwardingHeaders)(t,r),C.arm(r);const h={},w=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[a,f]of Object.entries(r.headers))f===void 0||a.startsWith(":")||HOP_BY_HOP.has(a)||w&&(a==="content-length"||a==="transfer-encoding")||(h[a]=f);let g=http,v;if(typeof e=="object")v={socketPath:e.socketPath,path:r.url,method:r.method,headers:h,agent:D};else{const a=new URL(e),f=a.protocol==="https:";g=f?require("node:https"):http,h.host=a.host,v={protocol:a.protocol,hostname:a.hostname,port:a.port||(f?443:80),path:r.url,method:r.method,headers:h,agent:R(e)}}const S=o;S.__lensmcpErrContained||(S.__lensmcpErrContained=!0,o.on("error",()=>{}));let l;const n=s(()=>{o.writableEnded||y.destroy()},"onReqGone"),i=s(()=>{l&&!l.destroyed&&l.destroy()},"onEdgeClose"),d=s(()=>{r.removeListener("error",n),r.removeListener("aborted",n),r.removeListener("close",n),o.removeListener("close",i)},"detach"),y=g.request(v,a=>{if(l=a,p.onResponse(),o.headersSent||o.writableEnded||o.destroyed){a.destroy();return}const f={};for(const[T,u]of Object.entries(a.headers))u===void 0||HOP_BY_HOP.has(T)||(f[T]=u);delete f["x-powered-by"],f.server="LensMCP";const B=r.__lensmcpPod;B&&(f["x-lensmcp-pod"]=B);try{o.writeHead(a.statusCode??502,f)}catch(T){console.error("[gateway] h2 relay: bad response header, resetting stream:",T.message),a.destroy(),o.destroyed||o.destroy(T);return}a.pipe(o),C.armResponse(o);const A=s(()=>{!o.writableEnded&&!o.destroyed&&o.destroy()},"abortEdge");a.once("error",A),a.once("close",A),o.on("close",i)});y.once("error",a=>p.onError(a)),y.once("close",d),r.on("error",n),r.on("aborted",n),r.on("close",n),w?y.end():r.pipe(y)},"pipeUpstream"),F=s(async(e,r,o)=>{const p=r.__lensmcpTrace;if(e.pool){const l=e.svc;l&&(l.lastUsed=Date.now(),l.inflight+=1,o.on("close",()=>{l.inflight-=1,l.lastUsed=Date.now()}));let n=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs);if(!n&&l&&(M(p,"cold-start",{project:e.project}),await E.ensureUp(l)&&(n=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs))),l&&n&&E.markUp(l,"observed"),l&&E.scaleUp(l),!n){const u=l?.lastErrors?.length?` Service errors: ${l.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${u} The watcher recovers and the next request retries automatically.`);return}const i=path.basename(n,".sock");r.__lensmcpPod=i,M(p,"pod-select",{pod:i,pods:e.pool.socks.length,strategy:"round-robin"});let d=!1;const y=s((u,$="unlink")=>{if(e.pool.socks=e.pool.socks.filter(m=>m!==n),e.pool.idx=-1,$==="unlink")try{fs.unlinkSync(n)}catch{}console.error(`[gateway] pod ${i} of ${e.project}: ${u}`)},"evictPod"),a=armFirstByteGuard({socketPath:n,serviceKey:t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!d&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:s((u,$)=>{d=!0,y(u),b(e,r,i,{kind:"pod-evicted",status:502,reason:u,detail:$}),(0,edge_1.sendError)(o,r,502,"unavailable",wedgedMessage(i,e.project,$))},"onWedged"),onCeiling:s(u=>{d=!0,y(u,"keep"),b(e,r,i,{kind:"request-gave-up",status:504,reason:u}),(0,edge_1.sendError)(o,r,504,"unavailable",`Pod ${i} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((u,$)=>{console.log(`[gateway] pod ${i} of ${e.project}: slow first byte, pod alive (${u}ms, ${$.inflight} in-flight, ${$.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>a.clear());const f=n,B=isReplayableRequest(r);let A=0;const T=s(()=>{A+=1,W({socketPath:f},r,o,{onResponse:s(()=>{a.clear(),d=!0},"onResponse"),onError:s(u=>{if(!d){if(edgeGone(r,o)){a.clear(),d=!0;return}(0,pod_probe_1.podConnectionVerdict)(f,u).then($=>{if(!d){if($==="alive"&&A<2&&B&&!edgeGone(r,o)){console.log(`[gateway] pod ${i} of ${e.project}: ${u.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),T();return}if(a.clear(),d=!0,$==="gone"){y(u.message),b(e,r,i,{kind:"pod-evicted",status:502,reason:`${u.message} \u2014 its socket no longer accepts (the pod process is gone)`}),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${i} of ${e.project} is gone (${u.message}) \u2014 retry.`);return}console.error(`[gateway] pod ${i} of ${e.project}: ${u.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${i} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");T();return}const h=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let w=0,g=!1;const v=isReplayableRequest(r),S=s(()=>{if(edgeGone(r,o))return;const l=e.target;W(l,r,o,{onResponse:s(()=>{},"onResponse"),onError:s(n=>{if(isConnError(n)&&(w+=1),e.lens&&isConnError(n)&&v&&w>=2&&!g&&!edgeGone(r,o)){g=!0,N(e).then(i=>{if(!edgeGone(r,o)){if(i==="ready"){S();return}if(i==="unsupported"&&Date.now()<h){setTimeout(S,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}k(e,r,o,e.target,n,i)}});return}if(isConnError(n)&&v&&Date.now()<h&&!edgeGone(r,o)){setTimeout(S,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}k(e,r,o,l,n)},"onError")})},"tryTcp");S()},"forwardNative");return{proxy:O,forward:s(async(e,r,o)=>{const p=Date.now(),h=r.__lensmcpTrace;if(o.on("close",()=>{U(e,Date.now()-p,o.statusCode??0,r.__lensmcpCaller),q(h,e.project,o.statusCode??0,{...r.__lensmcpPod?{pod:r.__lensmcpPod}:{},...r.__lensmcpCaller?{caller:r.__lensmcpCaller}:{}},e)}),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),(0,edge_1.stampForwardingHeaders)(t,r),C.arm(r),(r.httpVersionMajor??1)>=2){await F(e,r,o);return}if(e.pool){const n=e.svc;n&&(n.lastUsed=Date.now(),n.inflight+=1,o.on("close",()=>{n.inflight-=1,n.lastUsed=Date.now()}));let i=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs);if(!i&&n&&(M(h,"cold-start",{project:e.project}),await E.ensureUp(n)&&(i=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs))),n&&i&&E.markUp(n,"observed"),n&&E.scaleUp(n),!i){const m=n?.lastErrors?.length?` Service errors: ${n.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${m} The watcher recovers and the next request retries automatically.`);return}const d=path.basename(i,".sock");r.__lensmcpPod=d,M(h,"pod-select",{pod:d,pods:e.pool.socks.length,strategy:"round-robin"});let y=!1;const a=s((m,_="unlink")=>{if(e.pool.socks=e.pool.socks.filter(H=>H!==i),e.pool.idx=-1,_==="unlink")try{fs.unlinkSync(i)}catch{}console.error(`[gateway] pod ${d} of ${e.project}: ${m}`)},"evictPod"),f=armFirstByteGuard({socketPath:i,serviceKey:t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!y&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:s((m,_)=>{y=!0,a(m),b(e,r,d,{kind:"pod-evicted",status:502,reason:m,detail:_}),(0,edge_1.sendError)(o,r,502,"unavailable",wedgedMessage(d,e.project,_))},"onWedged"),onCeiling:s(m=>{y=!0,a(m,"keep"),b(e,r,d,{kind:"request-gave-up",status:504,reason:m}),(0,edge_1.sendError)(o,r,504,"unavailable",`Pod ${d} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((m,_)=>{console.log(`[gateway] pod ${d} of ${e.project}: slow first byte, pod alive (${m}ms, ${_.inflight} in-flight, ${_.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>f.clear());const B=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),A=i,T=isReplayableRequest(r);let u=0;const $=s(()=>{u+=1;const m=Date.now();O.web(r,o,{target:{socketPath:A},agent:D,proxyTimeout:B},_=>{if(!y){if(edgeGone(r,o)){f.clear(),y=!0;return}(0,pod_probe_1.podConnectionVerdict)(A,_).then(H=>{if(y)return;const V=Date.now()-m>=B-250;if(H==="alive"&&u<2&&T&&!V&&!edgeGone(r,o)){console.log(`[gateway] pod ${d} of ${e.project}: ${_.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),$();return}if(f.clear(),y=!0,H==="gone"){a(_.message),b(e,r,d,{kind:"pod-evicted",status:502,reason:`${_.message} \u2014 its socket no longer accepts (the pod process is gone)`}),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${d} of ${e.project} is gone (${_.message}) \u2014 retry.`);return}console.error(`[gateway] pod ${d} of ${e.project}: ${_.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${d} of ${e.project} dropped the connection \u2014 retry.`)})}})},"sendToPod");$();return}const w=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let g=0,v=!1;const S=isReplayableRequest(r),l=s(()=>{if(edgeGone(r,o))return;const n=e.target,i={target:n,autoRewrite:typeof n=="string",changeOrigin:!0,agent:R(n)};O.web(r,o,i,d=>{if(isConnError(d)&&(g+=1),e.lens&&isConnError(d)&&S&&g>=2&&!v&&!edgeGone(r,o)){v=!0,N(e).then(y=>{if(!edgeGone(r,o)){if(y==="ready"){l();return}if(y==="unsupported"&&Date.now()<w){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}k(e,r,o,e.target,d,y)}});return}if(isConnError(d)&&S&&Date.now()<w&&!edgeGone(r,o)){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}o.destroyed||k(e,r,o,n,d)})},"tryTcp");l()},"forward"),agentForTarget:R,destroyAgents:s(()=>{C.stop(),D.destroy(),I.destroy()},"destroyAgents"),closeProxy:s(()=>{try{O.close?.()}catch{}},"closeProxy")}}x(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var d=Object.defineProperty;var n=(e,o)=>d(e,"name",{value:o,configurable:!0});var u=Object.defineProperty,r=n((e,o)=>u(e,"name",{value:o,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sameRoot=void 0,exports.canonicalRoot=canonicalRoot,exports.hostConflicts=hostConflicts,exports.refuseRegistration=refuseRegistration,exports.refuseUnregistration=refuseUnregistration;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));function canonicalRoot(e){try{return fs.realpathSync(e)}catch{return path.resolve(e)}}n(canonicalRoot,"canonicalRoot"),r(canonicalRoot,"canonicalRoot");const sameRoot=r((e,o)=>canonicalRoot(e)===canonicalRoot(o),"sameRoot");exports.sameRoot=sameRoot;function claim(e){return e.host?`${e.host.toLowerCase()} ${e.prefix??""}`:e.apex?`* ${e.apex.toLowerCase()}`:void 0}n(claim,"claim"),r(claim,"claim");function hostConflicts(e,o){const s=[],t=new Set;for(const i of e.keys()){if(i===o.wsKey)continue;const a=e.get(i);if(!a)continue;const f=new Set(a.routes.map(claim));for(const c of o.routes){const h=claim(c);!h||!f.has(h)||t.has(h)||(t.add(h),s.push({host:c.host??`(default for ${c.apex})`,...c.prefix?{prefix:c.prefix}:{},heldBy:i,heldByRoot:a.root}))}}return s}n(hostConflicts,"hostConflicts"),r(hostConflicts,"hostConflicts");const describeConflict=r(e=>`${e.host}${e.prefix??""} (served by '${e.heldBy}')`,"describeConflict");function refuseRegistration(e,o){const s=e.get(o.wsKey);if(s&&!(0,exports.sameRoot)(s.root,o.root))return{error:"key-in-use",heldByRoot:s.root,message:`'${o.wsKey}' is already served from ${s.root}. ${o.root} is another checkout that derives the same key, and two checkouts cannot share a key or hosts behind one gateway: registering this one would replace the other's routes. To serve it beside the other under hosts of its own, run \`lensmcp instance start\` in ${o.root}; otherwise run \`lensmcp gateway stop\` in ${s.root} first.`};const t=hostConflicts(e,o);if(t.length>0){const i=t.slice(0,6).map(describeConflict).join(", "),a=t.length>6?`, and ${t.length-6} more`:"";return{error:"host-conflict",conflicts:t,message:`'${o.wsKey}' declares ${t.length===1?"a host":`${t.length} hosts`} another workspace already serves: ${i}${a}. The workspace registered first would win every request to ${t.length===1?"it":"them"}, so this registration is refused. Two workspaces behind one gateway need different hosts \u2014 for a second checkout of a workspace, \`lensmcp instance start\` in it serves it under hosts named after it.`}}}n(refuseRegistration,"refuseRegistration"),r(refuseRegistration,"refuseRegistration");function refuseUnregistration(e,o,s){const t=e.get(o.wsKey);if(t&&o.root&&!(0,exports.sameRoot)(t.root,o.root))return{error:"not-your-registration",heldByRoot:t.root,message:`'${o.wsKey}' is served from ${t.root}, not from ${o.root} \u2014 nothing of this checkout is registered, so nothing was removed.`};if(t&&s!==void 0&&o.wsKey===s)return{error:"daemon-workspace",heldByRoot:t.root,message:`'${o.wsKey}' is the workspace that owns the gateway \u2014 unregistering it would leave :443 serving without it. Stop the gateway instead: \`lensmcp gateway stop\` in ${t.root}.`}}n(refuseUnregistration,"refuseUnregistration"),r(refuseUnregistration,"refuseUnregistration");
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { InstanceRouting } from './instance';
|
|
1
2
|
import type { Route, ServiceCtl } from './types';
|
|
2
3
|
export interface WorkspaceFragment {
|
|
3
4
|
/** The workspace key (`readLensScope(root).key`). */
|
|
4
5
|
wsKey: string;
|
|
6
|
+
/** Where its pods' sockets live under `$TMPDIR` when that is not `wsKey` (`readLensScope(root).sockNs`). */
|
|
7
|
+
sockNs?: string;
|
|
5
8
|
/** Absolute workspace root — the fragment's provenance (for the chooser + reaping). */
|
|
6
9
|
root: string;
|
|
7
10
|
/** Every route this workspace contributes (host + path mounts, internal.*, its default catch-all, and —
|
|
@@ -9,6 +12,10 @@ export interface WorkspaceFragment {
|
|
|
9
12
|
routes: Route[];
|
|
10
13
|
/** The pod-service controllers this workspace owns (scale-from-zero lifecycle is per-service). */
|
|
11
14
|
services: ServiceCtl[];
|
|
15
|
+
/** Set when this workspace is a worktree INSTANCE of another (instance.ts): its name, apexes and variables. */
|
|
16
|
+
instance?: InstanceRouting;
|
|
17
|
+
/** Its own gateway target's `middleware` (relative to `root`), run on its routes (workspace-middleware.ts). */
|
|
18
|
+
middleware?: string;
|
|
12
19
|
}
|
|
13
20
|
/** A live map of `wsKey → fragment`; `merged()` is the flat table the pipeline matches against. */
|
|
14
21
|
export declare class RouteRegistry {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type InstanceDecl } from './instance';
|
|
1
2
|
/** The base domain for the lens dashboard host — the longest common dotted
|
|
2
3
|
* suffix of the declared cluster hosts (e.g. ['api.tetros.ai.local',
|
|
3
4
|
* 'tetros.ai.local'] → 'tetros.ai.local'). Falls back to the first host, then
|
|
@@ -16,7 +17,24 @@ export interface LensScope {
|
|
|
16
17
|
*/
|
|
17
18
|
mcpHttpPort: number;
|
|
18
19
|
basePath: string;
|
|
20
|
+
/**
|
|
21
|
+
* The folder under `$TMPDIR` its pods' sockets live in (`$TMPDIR/<sockNs>/<service>-devserver/`). The key,
|
|
22
|
+
* unless `.lensmcp/config.json` pins a shorter `sockNs`: a unix socket path has 103 usable characters on
|
|
23
|
+
* macOS, and with a 49-character `$TMPDIR` FoodGuard's longest pod socket already took 101 under the key
|
|
24
|
+
* `foodguard`. A worktree instance (`foodguard-<name>`) cannot fit under its key, so it gets a short one.
|
|
25
|
+
*/
|
|
26
|
+
sockNs: string;
|
|
27
|
+
/**
|
|
28
|
+
* Set when this checkout is a worktree INSTANCE of another workspace (`.lensmcp/config.json` → `instance`, written
|
|
29
|
+
* by `lensmcp instance start`): its hosts are rewritten under its name (instance.ts). Only honoured when the
|
|
30
|
+
* config's `key` is the instance key `<baseKey>-<name>` — a half-edited file is served as an ordinary workspace.
|
|
31
|
+
*/
|
|
32
|
+
instance?: InstanceDecl;
|
|
19
33
|
}
|
|
34
|
+
/** A `sockNs` the lens accepts: one short path segment. Anything else falls back to the key. */
|
|
35
|
+
export declare const SOCK_NS_PATTERN: RegExp;
|
|
36
|
+
/** The socket namespace a config pins, or `key` when it pins none (or one that is not a single short segment). */
|
|
37
|
+
export declare function sockNamespace(key: string, pinned: unknown): string;
|
|
20
38
|
/** Per-workspace lens scope (key + ports + mount path). Prefers the
|
|
21
39
|
* CLI-written `.lensmcp/config.json` (so the gateway agrees with `lensmcp gateway`
|
|
22
40
|
* / `lensmcp dashboard` / the MCP shim); falls back to deriving from the workspace. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var p=Object.defineProperty;var c=(r,t)=>p(r,"name",{value:t,configurable:!0});var a=Object.defineProperty,o=c((r,t)=>a(r,"name",{value:t,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.SOCK_NS_PATTERN=void 0,exports.baseDomainOf=baseDomainOf,exports.sockNamespace=sockNamespace,exports.readLensScope=readLensScope,exports.deriveMcpHttpPort=deriveMcpHttpPort,exports.readSourceSetExclude=readSourceSetExclude,exports.readSourceSetOptions=readSourceSetOptions,exports.lensKeyFrom=lensKeyFrom,exports.lensSlug=lensSlug;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),instance_1=require("./instance");function baseDomainOf(r){const t=r.filter(n=>!!n&&!n.startsWith("*."));if(t.length===0)return"local";const e=t.reduce((n,i)=>i.length<n.length?i:n);if(t.every(n=>n===e||n.endsWith("."+e)))return e;const s=e.split(".");return s.slice(-Math.min(s.length,3)).join(".")}c(baseDomainOf,"baseDomainOf"),o(baseDomainOf,"baseDomainOf");const BASE_PORTS={dashboard:4321,mcpHttp:4500};function hashToRange(r,t){let e=2166136261;for(let s=0;s<r.length;s++)e^=r.charCodeAt(s),e=Math.imul(e,16777619);return Math.abs(e|0)%t}c(hashToRange,"hashToRange"),o(hashToRange,"hashToRange"),exports.SOCK_NS_PATTERN=/^[a-z0-9][a-z0-9-]{0,31}$/;function sockNamespace(r,t){return typeof t=="string"&&exports.SOCK_NS_PATTERN.test(t)?t:r}c(sockNamespace,"sockNamespace"),o(sockNamespace,"sockNamespace");function readLensScope(r){try{const e=JSON.parse(fs.readFileSync(path.join(r,".lensmcp","config.json"),"utf8"));if(e&&typeof e.key=="string"){const s=(0,instance_1.parseInstanceDecl)(e.instance),n=s&&(0,instance_1.instanceKey)(s.baseKey,s.name)===e.key?s:void 0;return{key:e.key,dashboardPort:e.ports?.dashboard??BASE_PORTS.dashboard,mcpHttpPort:e.ports?.mcpHttp??deriveMcpHttpPort(e.key),basePath:e.dashboardBasePath??"/"+e.key,sockNs:sockNamespace(n?(0,instance_1.instanceSockNs)(e.key):e.key,e.sockNs),...n?{instance:n}:{}}}}catch{}const t=lensKeyFrom(r);return{key:t,dashboardPort:BASE_PORTS.dashboard,mcpHttpPort:deriveMcpHttpPort(t),basePath:"/"+t,sockNs:t}}c(readLensScope,"readLensScope"),o(readLensScope,"readLensScope");function deriveMcpHttpPort(r){return BASE_PORTS.mcpHttp+hashToRange(r,200)}c(deriveMcpHttpPort,"deriveMcpHttpPort"),o(deriveMcpHttpPort,"deriveMcpHttpPort");function readSourceSetExclude(r){return readSourceSetOptions(r).exclude}c(readSourceSetExclude,"readSourceSetExclude"),o(readSourceSetExclude,"readSourceSetExclude");function readSourceSetOptions(r){const t={exclude:new Set,ignoreTests:!0};try{const e=JSON.parse(fs.readFileSync(path.join(r,".lensmcp","config.json"),"utf8")),s=e?.sourceSet?.exclude;Array.isArray(s)&&(t.exclude=new Set(s.filter(n=>typeof n=="string"&&n.length>0&&!n.includes("/")))),e?.sourceSet?.ignoreTests===!1&&(t.ignoreTests=!1)}catch{}return t}c(readSourceSetOptions,"readSourceSetOptions"),o(readSourceSetOptions,"readSourceSetOptions");function lensKeyFrom(r){try{const t=JSON.parse(fs.readFileSync(path.join(r,"package.json"),"utf8"));if(t?.name)return lensSlug(t.name)}catch{}return lensSlug(path.basename(r))}c(lensKeyFrom,"lensKeyFrom"),o(lensKeyFrom,"lensKeyFrom");function lensSlug(r){return r.toLowerCase().replace(/^@[^/]+\//,"").replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,"")||"workspace"}c(lensSlug,"lensSlug"),o(lensSlug,"lensSlug");
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var ue=Object.defineProperty;var T=(e,t)=>ue(e,"name",{value:t,configurable:!0});var le=Object.defineProperty,n=T((e,t)=>le(e,"name",{value:t,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.retirePreviousWorkspace=retirePreviousWorkspace,exports.resolveMaxConnections=resolveMaxConnections,exports.h2BudgetFromEnv=h2BudgetFromEnv,exports.createH2Governor=createH2Governor,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),control_1=require("./control"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),client_ip_1=require("../client-ip"),workspace_log_1=require("./workspace-log"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),process_names_1=require("./process-names"),project_graph_store_1=require("./project-graph-store"),serve_launch_1=require("./serve-launch"),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),types_1=require("./types");function insecureLocalFetch(e,t){return new Promise((u,E)=>{const a=(e.startsWith("https:")?require("node:https"):require("node:http")).request(e,{method:"GET",headers:t?.headers,rejectUnauthorized:!1},g=>{const d=[];g.on("data",p=>d.push(p)),g.on("end",()=>{const p=g.statusCode??0,l=Buffer.concat(d).toString("utf8");u({ok:p>=200&&p<300,status:p,json:n(async()=>JSON.parse(l),"json")})})});a.on("error",E),a.end()})}T(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(e){const t=process.env.LENSMCP_GW_JWKS_URL,u=process.env.LENSMCP_GW_JWT_ISS,E=new Map;let a;const g=n((d,p)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${d}${p}`);const l=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:d,...u?{issuer:u}:{},fetchImpl:insecureLocalFetch});return l.refresh().catch(s=>console.warn("[gateway] edge JWKS warm failed (will retry):",s.message)),l},"make");return{for(d){if(t)return a??=g(t,"");const p=d.split(":")[0].toLowerCase();let l;for(const i of e()){const h=i.host;if(typeof h!="string"||!/^auth\./.test(h)||h.startsWith("internal."))continue;const C=h.slice(5);(p===C||p.endsWith(`.${C}`))&&(!l||C.length>l.length)&&(l=C)}if(!l)return;let s=E.get(l);return s||(s=g(`https://auth.${l}/jwks.json`,` (auto-derived for apex ${l})`),E.set(l,s)),s},stop(){a?.stop();for(const d of E.values())d.stop()}}}T(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(e,t){return e&&exports.BENIGN_SOCKET_ERRNOS.has(e)?{status:null,report:!1,cause:"client-abort"}:e==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:e==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:e==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:e?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:e??t??"unknown"}}T(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(e=1e4){const t=new Map;return u=>{const E=Date.now(),a=t.get(u)??0;return E-a<e?!1:(t.set(u,E),!0)}}T(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(e){e.on("error",t=>{exports.BENIGN_SOCKET_ERRNOS.has(t.code??"")||console.warn(`[gateway] inbound socket error (${t.code??t.message}) \u2014 dropping this connection only`),e.destroy()})}T(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(e,t,u){const E=e.get(t);if(!E)return!1;e.delete(t),u==="re-registered"&&console.log(`[gateway] workspace '${t}' re-registered \u2014 retiring the children of its previous registration first.`);try{E()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${t}' (${u}): ${a.message}`)}return!0}T(retirePreviousWorkspace,"retirePreviousWorkspace"),n(retirePreviousWorkspace,"retirePreviousWorkspace"),exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=512,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=64,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=4096,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=3e5;function parseBudget(e,t){if(e===void 0||e.trim()==="")return t;const u=Number(e);return!Number.isFinite(u)||u<0?t:Math.floor(u)}T(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(e){return parseBudget(e,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}T(resolveMaxConnections,"resolveMaxConnections"),n(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(e=process.env){return{maxSessions:parseBudget(e.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(e.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(e.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}T(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(e,t){const{NGHTTP2_ENHANCE_YOUR_CALM:u,NGHTTP2_REFUSED_STREAM:E}=require("node:http2").constants,a=new Map;let g=0;const d=n(s=>{s.idle&&(clearTimeout(s.idle),s.idle=void 0)},"disarm"),p=n((s,i)=>{d(i),!(e.idleSessionMs<=0||i.open>0)&&(i.idle=setTimeout(()=>{s.destroyed||s.closed||(t("session-reclaimed",{idleMs:e.idleSessionMs,sessions:a.size}),a.delete(s),s.close())},e.idleSessionMs),i.idle.unref?.())},"arm"),l=n(()=>{for(const[s,i]of a)if(!(i.open>0||s.destroyed))return d(i),a.delete(s),t("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:e.maxSessions}),s.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(s){if(e.maxSessions>0&&a.size>=e.maxSessions&&!l()){t("session-refused",{sessions:a.size,maxSessions:e.maxSessions,streams:g});try{s.goaway(u)}catch{}try{s.close()}catch{}return!1}const i={open:0};return a.set(s,i),p(s,i),s.once("close",()=>{d(i),a.delete(s)}),!0},admitStream(s,i){const h=a.get(s);if(!h)return!0;if(e.maxStreams>0&&g>=e.maxStreams){t("stream-refused",{streams:g,maxStreams:e.maxStreams,sessions:a.size});try{i.close(E)}catch{}return!1}return g+=1,h.open+=1,d(h),i.once("close",()=>{g=Math.max(0,g-1),h.open=Math.max(0,h.open-1),h.open===0&&a.has(s)&&p(s,h)}),!0},dispose(){for(const s of a.values())d(s);a.clear()}}}T(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(e,t){const u=e.https!==!1,E=e.ports?.length?e.ports:[443],a=e.scanTtlMs??1500,g=e.startupTimeoutMs??12e4,d=e.sweepMs??1e4,p=e.trafficFlushMs??5e3;let l=!1;const s=process.env.LENSMCP_EVENT_FILE??path.join(t.root,".lensmcp","events.jsonl"),i=(0,scope_1.readLensScope)(t.root).key,h=t.projectsConfigurations?.projects??{},C=(0,discovery_1.projectSourceRoots)(t.root,h,t.projectDependencies),{routes:I,services:v}=(0,discovery_1.discoverRoutes)(t.root,h,void 0,i,C);if(I.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const H=new route_registry_1.RouteRegistry;H.register({wsKey:i,root:t.root,routes:I,services:v});const R=[];let U=n(()=>{},"refreshCert");const B=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,H),U()},"rebuildRoutes");B();const{serviceKeys:Q,keyToProject:Z,keyToWorkspace:ee}=(0,service_keys_1.loadServiceKeys)(t.root,v,i),re=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...e.trustProxyHops!==void 0?{trustProxyHops:e.trustProxyHops}:{},...e.clientIpHeader?{clientIpHeader:e.clientIpHeader}:{},...e.clientIpTrustedProxies?{clientIpTrustedProxies:e.clientIpTrustedProxies}:{}}),y={root:t.root,wsKey:i,eventFile:s,routes:R,registry:H,rebuildRoutes:B,services:v,serviceKeys:Q,keyToProject:Z,keyToWorkspace:ee,clientIp:re,bodyStallMs:e.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:g,sweepMs:d,trafficFlushMs:p,https:u,projectRoots:Object.fromEntries(Object.entries(t.projectsConfigurations?.projects??{}).map(([r,o])=>[r,path.resolve(t.root,o.root)])),projectSourceRoots:C,...t.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(t.projectGraphStore)}:{},...e.sharedCompiler!==void 0?{sharedCompiler:e.sharedCompiler}:{},stopped:n(()=>l,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(v,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!t.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,e.sharedCompiler),label:i||path.basename(t.root),lens:{key:i,dashboard:e.dashboard!==!1,mcp:e.mcp!==!1&&I.some(r=>!!r.lens)}}));const f=(0,observability_1.createObservability)(s,{trafficFlushMs:p}),te=createClientErrorLogGate(),P=h2BudgetFromEnv(),se=createClientErrorLogGate(3e4),$=createH2Governor(P,(r,o)=>{se(r)&&console.warn(`[gateway] h2 aggregate budget: ${r} (${JSON.stringify(o)}). These are the FRONT DOOR's ceilings, not an upstream's \u2014 raise/disable with LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS / LENSMCP_MAX_CONNECTIONS (0 = unbounded).`),f.emit(r==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${r}`,`gateway-h2-budget:${r}`,{kind:"h2-aggregate-budget",event:r,project:"gateway",...o})}),N=(0,lifecycle_1.createServiceLayer)(y,f),O=(0,lens_children_1.createLensChildren)(y,f,e),G=new Map;let Y;try{Y=(0,control_1.startControlServer)({rt:y,killService:n((r,o)=>N.killSpawned(r,o),"killService"),onRegister:n(async r=>{retirePreviousWorkspace(G,r.wsKey,"re-registered");const o=await O.hostWorkspaceDashboard(r),w=await O.hostWorkspaceApps(r);G.set(r.wsKey,()=>{try{o?.()}finally{w()}})},"onRegister"),onUnregister:n(r=>{retirePreviousWorkspace(G,r.wsKey,"unregistered"),N.stopBuildHost(r.root)},"onUnregister")})}catch(r){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",r.message)}const b=(0,proxy_1.createProxy)(y,f,N),X=buildEdgeVerifierPool(()=>y.routes),K=(0,auth_1.createEdgeAuth)(y,f,X);let j;if(e.middleware){const r=require(path.resolve(t.root,e.middleware));j=r.default??r}const oe=n(r=>{if(j)try{j(r.req)}catch(o){f.traceStep(r.trace,"auth",{mode:"middleware",ok:!1,reason:o.message}),r.route&&f.finishTrace(r.trace,r.route.project,401),r.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${o.message}`)}},"builtinMiddleware"),J=e.hooks??{},V=(0,hooks_1.createHooks)({...J,afterAuth:[oe,...J.afterAuth??[]]}),z=(0,handler_1.createHandler)({rt:y,obs:f,auth:K,proxy:b,hooks:V}),ne=(0,upgrade_1.createUpgrade)({rt:y,auth:K,proxy:b,hooks:V}),ae=f.startEdgeFlusher(),ie=N.startSweeper();await O.bootSingletonsAndApps();for(const r of v)r.decl.eager&&N.ensureUp(r);let x,q,L;if(u){const r=require("../../../basic-ssl"),o=(0,service_keys_1.gatewayCacheDir)(t.root);L=n(()=>r.getCertificateSync(o,"gateway",y.routes.map(w=>w.host).filter(w=>!!w)),"mintCert"),x=L(),q=r.caCertPath()}const W=[],k=[],D=e.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const r of E){const o=x?require("node:http2").createSecureServer({key:x,cert:x,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:e.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:e.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(m,_)=>z(m,_)):http.createServer(z);o.on("upgrade",ne),o.on("error",m=>console.error(`[gateway] port ${r}: ${m.code}`)),o.on("connection",guardInboundSocket),o.on("secureConnection",guardInboundSocket),o.on("tlsClientError",(m,_)=>_.destroy()),o.on("clientError",(m,_)=>{const c=m,S=classifyClientError(c.code,c.message);if(S.report&&te(S.cause)){const M=_.remoteAddress??"unknown",A=S.status===null?"dropped":String(S.status);console.warn(`[gateway] inbound H1 clientError from ${M}: ${S.cause} (${c.code??c.message}${c.bytesParsed!==void 0?`, bytesParsed=${c.bytesParsed}`:""}) \u2192 ${A}. This status is the GATEWAY's, not the upstream service's.`),f.emit("warning",`inbound clientError: ${S.cause} \u2192 ${A}`,`gateway-client-error:${S.cause}`,{kind:"gateway-client-error",cause:S.cause,code:c.code??null,status:S.status,peer:M,bytesParsed:c.bytesParsed??null})}S.status!==null&&_.writable&&_.end(`HTTP/1.1 ${S.status} ${CLIENT_ERROR_REASON[S.status]}\r
|
|
1
|
+
"use strict";var Se=Object.defineProperty;var T=(s,r)=>Se(s,"name",{value:r,configurable:!0});var _e=Object.defineProperty,n=T((s,r)=>_e(s,"name",{value:r,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.retirePreviousWorkspace=retirePreviousWorkspace,exports.resolveMaxConnections=resolveMaxConnections,exports.h2BudgetFromEnv=h2BudgetFromEnv,exports.createH2Governor=createH2Governor,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),control_1=require("./control"),workspace_middleware_1=require("./workspace-middleware"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),client_ip_1=require("../client-ip"),workspace_log_1=require("./workspace-log"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),process_names_1=require("./process-names"),project_graph_store_1=require("./project-graph-store"),serve_launch_1=require("./serve-launch"),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),types_1=require("./types");function insecureLocalFetch(s,r){return new Promise((d,h)=>{const a=(s.startsWith("https:")?require("node:https"):require("node:http")).request(s,{method:"GET",headers:r?.headers,rejectUnauthorized:!1},y=>{const _=[];y.on("data",S=>_.push(S)),y.on("end",()=>{const S=y.statusCode??0,p=Buffer.concat(_).toString("utf8");d({ok:S>=200&&S<300,status:S,json:n(async()=>JSON.parse(p),"json")})})});a.on("error",h),a.end()})}T(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(s){const r=process.env.LENSMCP_GW_JWKS_URL,d=process.env.LENSMCP_GW_JWT_ISS,h=new Map;let a;const y=n((_,S)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${_}${S}`);const p=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:_,...d?{issuer:d}:{},fetchImpl:insecureLocalFetch});return p.refresh().catch(o=>console.warn("[gateway] edge JWKS warm failed (will retry):",o.message)),p},"make");return{for(_){if(r)return a??=y(r,"");const S=_.split(":")[0].toLowerCase();let p;for(const c of s()){const i=c.host;if(typeof i!="string"||!/^auth\./.test(i)||i.startsWith("internal."))continue;const A=i.slice(5);(S===A||S.endsWith(`.${A}`))&&(!p||A.length>p.length)&&(p=A)}if(!p)return;let o=h.get(p);return o||(o=y(`https://auth.${p}/jwks.json`,` (auto-derived for apex ${p})`),h.set(p,o)),o},stop(){a?.stop();for(const _ of h.values())_.stop()}}}T(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(s,r){return s&&exports.BENIGN_SOCKET_ERRNOS.has(s)?{status:null,report:!1,cause:"client-abort"}:s==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:s==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:s==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:s?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:s??r??"unknown"}}T(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(s=1e4){const r=new Map;return d=>{const h=Date.now(),a=r.get(d)??0;return h-a<s?!1:(r.set(d,h),!0)}}T(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(s){s.on("error",r=>{exports.BENIGN_SOCKET_ERRNOS.has(r.code??"")||console.warn(`[gateway] inbound socket error (${r.code??r.message}) \u2014 dropping this connection only`),s.destroy()})}T(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(s,r,d){const h=s.get(r);if(!h)return!1;s.delete(r),d==="re-registered"&&console.log(`[gateway] workspace '${r}' re-registered \u2014 retiring the children of its previous registration first.`);try{h()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${r}' (${d}): ${a.message}`)}return!0}T(retirePreviousWorkspace,"retirePreviousWorkspace"),n(retirePreviousWorkspace,"retirePreviousWorkspace"),exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=512,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=64,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=4096,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=3e5;function parseBudget(s,r){if(s===void 0||s.trim()==="")return r;const d=Number(s);return!Number.isFinite(d)||d<0?r:Math.floor(d)}T(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(s){return parseBudget(s,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}T(resolveMaxConnections,"resolveMaxConnections"),n(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(s=process.env){return{maxSessions:parseBudget(s.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(s.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(s.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}T(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(s,r){const{NGHTTP2_ENHANCE_YOUR_CALM:d,NGHTTP2_REFUSED_STREAM:h}=require("node:http2").constants,a=new Map;let y=0;const _=n(o=>{o.idle&&(clearTimeout(o.idle),o.idle=void 0)},"disarm"),S=n((o,c)=>{_(c),!(s.idleSessionMs<=0||c.open>0)&&(c.idle=setTimeout(()=>{o.destroyed||o.closed||(r("session-reclaimed",{idleMs:s.idleSessionMs,sessions:a.size}),a.delete(o),o.close())},s.idleSessionMs),c.idle.unref?.())},"arm"),p=n(()=>{for(const[o,c]of a)if(!(c.open>0||o.destroyed))return _(c),a.delete(o),r("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:s.maxSessions}),o.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(o){if(s.maxSessions>0&&a.size>=s.maxSessions&&!p()){r("session-refused",{sessions:a.size,maxSessions:s.maxSessions,streams:y});try{o.goaway(d)}catch{}try{o.close()}catch{}return!1}const c={open:0};return a.set(o,c),S(o,c),o.once("close",()=>{_(c),a.delete(o)}),!0},admitStream(o,c){const i=a.get(o);if(!i)return!0;if(s.maxStreams>0&&y>=s.maxStreams){r("stream-refused",{streams:y,maxStreams:s.maxStreams,sessions:a.size});try{c.close(h)}catch{}return!1}return y+=1,i.open+=1,_(i),c.once("close",()=>{y=Math.max(0,y-1),i.open=Math.max(0,i.open-1),i.open===0&&a.has(o)&&S(o,i)}),!0},dispose(){for(const o of a.values())_(o);a.clear()}}}T(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(s,r){const d=s.https!==!1,h=s.ports?.length?s.ports:[443],a=s.scanTtlMs??1500,y=s.startupTimeoutMs??12e4,_=s.sweepMs??1e4,S=s.trafficFlushMs??5e3;let p=!1;const o=process.env.LENSMCP_EVENT_FILE??path.join(r.root,".lensmcp","events.jsonl"),c=(0,scope_1.readLensScope)(r.root),i=c.key,A=c.sockNs!==i?c.sockNs:void 0,Y=r.projectsConfigurations?.projects??{},B=(0,discovery_1.projectSourceRoots)(r.root,Y,r.projectDependencies),{routes:H,services:v,instance:X}=(0,discovery_1.discoverRoutes)(r.root,Y,void 0,i,B,A,c.instance);if(H.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const $=new route_registry_1.RouteRegistry;$.register({wsKey:i,...A?{sockNs:A}:{},root:r.root,routes:H,services:v,...X?{instance:X}:{}});const R=[];let J=n(()=>{},"refreshCert");const V=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,$),J()},"rebuildRoutes");V();const{serviceKeys:te,keyToProject:oe,keyToWorkspace:ne}=(0,service_keys_1.loadServiceKeys)(r.root,v,i),ae=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...s.trustProxyHops!==void 0?{trustProxyHops:s.trustProxyHops}:{},...s.clientIpHeader?{clientIpHeader:s.clientIpHeader}:{},...s.clientIpTrustedProxies?{clientIpTrustedProxies:s.clientIpTrustedProxies}:{}}),m={root:r.root,wsKey:i,...A?{sockNs:A}:{},eventFile:o,routes:R,registry:$,rebuildRoutes:V,services:v,serviceKeys:te,keyToProject:oe,keyToWorkspace:ne,clientIp:ae,bodyStallMs:s.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:y,sweepMs:_,trafficFlushMs:S,https:d,projectRoots:Object.fromEntries(Object.entries(r.projectsConfigurations?.projects??{}).map(([e,t])=>[e,path.resolve(r.root,t.root)])),projectSourceRoots:B,...r.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(r.projectGraphStore)}:{},...s.sharedCompiler!==void 0?{sharedCompiler:s.sharedCompiler}:{},stopped:n(()=>p,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(v,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!r.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,s.sharedCompiler),label:i||path.basename(r.root),lens:{key:i,dashboard:s.dashboard!==!1,mcp:s.mcp!==!1&&H.some(e=>!!e.lens)}}));const w=(0,observability_1.createObservability)(o,{trafficFlushMs:S}),ie=createClientErrorLogGate(),x=h2BudgetFromEnv(),ce=createClientErrorLogGate(3e4),G=createH2Governor(x,(e,t)=>{ce(e)&&console.warn(`[gateway] h2 aggregate budget: ${e} (${JSON.stringify(t)}). These are the FRONT DOOR's ceilings, not an upstream's \u2014 raise/disable with LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS / LENSMCP_MAX_CONNECTIONS (0 = unbounded).`),w.emit(e==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${e}`,`gateway-h2-budget:${e}`,{kind:"h2-aggregate-budget",event:e,project:"gateway",...t})}),N=(0,lifecycle_1.createServiceLayer)(m,w),k=(0,lens_children_1.createLensChildren)(m,w,s),j=new Map;let q;if(s.middleware){const e=require(path.resolve(r.root,s.middleware));q=e.default??e}const W=(0,workspace_middleware_1.createWorkspaceMiddleware)({ownerKey:i,...q?{owner:q}:{},policy:(0,workspace_middleware_1.guestMiddlewarePolicy)()});let O;try{O=(0,control_1.startControlServer)({rt:m,killService:n((e,t)=>N.killSpawned(e,t),"killService"),onRegister:n(async e=>{retirePreviousWorkspace(j,e.wsKey,"re-registered"),W.set(e.wsKey,e.root,e.middleware);const t=await k.hostWorkspaceDashboard(e),f=await k.hostWorkspaceApps(e);j.set(e.wsKey,()=>{try{t?.()}finally{f()}})},"onRegister"),onUnregister:n(e=>{retirePreviousWorkspace(j,e.wsKey,"unregistered"),W.drop(e.wsKey),N.stopBuildHost(e.root)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const b=(0,proxy_1.createProxy)(m,w,N),z=buildEdgeVerifierPool(()=>m.routes),Q=(0,auth_1.createEdgeAuth)(m,w,z),le=n(e=>{const t=e.route?.wsKey,f=W.for(t);if(f)try{const l=t?m.registry.get(t):void 0;f(e.req,{...t?{wsKey:t}:{},...l?{root:l.root}:{},env:l?.instance?.env??{}})}catch(l){w.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:l.message}),e.route&&w.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${l.message}`)}},"builtinMiddleware"),Z=s.hooks??{},ee=(0,hooks_1.createHooks)({...Z,afterAuth:[le,...Z.afterAuth??[]]}),se=(0,handler_1.createHandler)({rt:m,obs:w,auth:Q,proxy:b,hooks:ee}),re=(0,upgrade_1.createUpgrade)({rt:m,auth:Q,proxy:b,hooks:ee,obs:w}),ue=w.startEdgeFlusher(),de=N.startSweeper();await k.bootSingletonsAndApps();for(const e of v)e.decl.eager&&N.ensureUp(e);let P,D,L;if(d){const e=require("../../../basic-ssl"),t=(0,service_keys_1.gatewayCacheDir)(r.root);L=n(()=>e.getCertificateSync(t,"gateway",m.routes.map(f=>f.host).filter(f=>!!f)),"mintCert"),P=L(),D=e.caCertPath()}const F=[],I=[],U=s.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const e of h){const t=P?require("node:http2").createSecureServer({key:P,cert:P,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:s.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:s.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(l,E)=>se(l,E)):http.createServer(se);t.on("upgrade",re),t.on("error",l=>console.error(`[gateway] port ${e}: ${l.code}`)),t.on("connection",guardInboundSocket),t.on("secureConnection",guardInboundSocket),t.on("tlsClientError",(l,E)=>E.destroy()),t.on("clientError",(l,E)=>{const u=l,g=classifyClientError(u.code,u.message);if(g.report&&ie(g.cause)){const C=E.remoteAddress??"unknown",M=g.status===null?"dropped":String(g.status);console.warn(`[gateway] inbound H1 clientError from ${C}: ${g.cause} (${u.code??u.message}${u.bytesParsed!==void 0?`, bytesParsed=${u.bytesParsed}`:""}) \u2192 ${M}. This status is the GATEWAY's, not the upstream service's.`),w.emit("warning",`inbound clientError: ${g.cause} \u2192 ${M}`,`gateway-client-error:${g.cause}`,{kind:"gateway-client-error",cause:g.cause,code:u.code??null,status:g.status,peer:C,bytesParsed:u.bytesParsed??null})}g.status!==null&&E.writable&&E.end(`HTTP/1.1 ${g.status} ${CLIENT_ERROR_REASON[g.status]}\r
|
|
2
2
|
\r
|
|
3
|
-
`),
|
|
3
|
+
`),E.destroy()}),t.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,t.headersTimeout=s.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,U>0&&(t.maxConnections=U);const f=s.connectionsCheckingIntervalMs??types_1.GATEWAY_CONN_CHECK_MS;if(f>0&&(t.connectionsCheckingInterval=f),P!=null){const l=t,{NGHTTP2_CANCEL:E}=require("node:http2").constants;l.on("session",u=>{if(!G.admitSession(u)){u.on("error",()=>{});return}let g=0,C=0;u.on("stream",M=>{G.admitStream(u,M)&&M.once("close",()=>{u.destroyed?C+=1:M.rstCode===E&&(g+=1)})}),u.once("close",()=>{C!==0&&(console.warn(`[gateway] h2 session closed with ${C} stream(s) in flight after ${g} client reset(s) \u2014 every in-flight fetch on that connection failed at once (ERR_HTTP2_PROTOCOL_ERROR in the browser). Reset budget: LENSMCP_H2_RESET_BURST/LENSMCP_H2_RESET_RATE (issues/closed/foodguard-14).`),w.emit("warning",`h2 session lost with ${C} stream(s) in flight`,"gateway-h2-session-lost",{kind:"h2-session-lost",project:"gateway",inflight:C,clientResets:g}))}),u.on("error",M=>{const pe=M.code??M.message;console.warn(`[gateway] h2 session error (${pe}) \u2014 dropping this session only`),u.destroyed||u.destroy()})}),l.on("sessionError",u=>{console.warn(`[gateway] h2 sessionError: ${u.code??u.message}`)})}await new Promise(l=>{t.listen(e,()=>{const E=t.address()?.port??e;I.push(E),console.log(`[gateway] Listening on ${P?"https":"http"}://localhost:${E}`),l()})}),F.push(t)}L&&(J=n(()=>{const e=L();for(const t of F)t.setSecureContext?.({key:e,cert:e})},"refreshCert")),console.log("[gateway] Routes:");for(const e of R){const t=e.pool?`pods@${e.pool.dir}`:e.target;console.log(`[gateway] ${e.host??"(default)"} \u2192 ${t}${e.prependPrefix?` (+${e.prependPrefix})`:""}${e.internal?" [internal: no middleware]":""} [${e.project}]`)}D&&console.log(`[gateway] CA: ${D} \u2014 one-time setup via the trust executor.`);const K=n(e=>e>0?String(e):"unbounded","unbounded");return console.log(`[gateway] front-door budget: \u2264${K(U)} connections, \u2264${K(x.maxSessions)} h2 sessions (each \u2264512 MB session memory, \u2264512 streams), \u2264${K(x.maxStreams)} open streams in total, idle h2 session reclaimed after ${x.idleSessionMs>0?`${Math.round(x.idleSessionMs/1e3)}s`:"never"}. Override: LENSMCP_MAX_CONNECTIONS / LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS (0 = unbounded).`),w.emit("info",`gateway up: ${R.length} routes on ${I.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:I,routes:R.map(e=>({host:e.host??"(default)",project:e.project,mode:e.pool?"pods":"tcp",internal:!!e.internal,prependPrefix:e.prependPrefix}))}),O&&process.env.LENSMCP_GUEST_REPLAY!=="0"&&O.replay().catch(e=>console.warn("[gateway] guest replay failed:",e.message)),{ports:I,routes:R,services:v,stop:n(async()=>{if(!p){p=!0,clearInterval(ue),clearInterval(de),w.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),z.stop(),O?.close(),(0,workspace_registry_1.unregisterWorkspace)(i);for(const e of v)N.killSpawned(e,"gateway shutdown");await N.stopBuildHost(),await k.reapAll(),re.closeTunnels(),await Promise.all(F.map(e=>new Promise(t=>{let f=!1;const l=n(()=>{f||(f=!0,t())},"done"),E=e;e.close(()=>l());try{E.closeIdleConnections?.()}catch{}setTimeout(()=>{try{E.closeAllConnections?.()}catch{}l()},types_1.GATEWAY_STOP_GRACE_MS).unref?.()}))),b.closeProxy(),b.destroyAgents(),G.dispose(),(0,workspace_log_1.closeWorkspaceLogs)()}},"stop"),...m.lensSupervisor?{lensSupervisor:m.lensSupervisor}:{},...m.graphStores?{graphStores:m.graphStores}:{}}}T(startGateway,"startGateway"),n(startGateway,"startGateway");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ServiceCtl } from './types';
|
|
2
|
-
/** The gateway's on-disk cache root — shared by service keys AND the TLS cert cache.
|
|
2
|
+
/** The gateway's on-disk cache root — shared by service keys AND the TLS cert cache. The CHECKOUT's own:
|
|
3
|
+
* `node_modules/.cache/davnx-webpack`, or `.lensmcp/cache/davnx-webpack` when `node_modules` is a symlink to
|
|
4
|
+
* another checkout's — one key file for two copies made each one's keys the other's (checkout-cache.ts). */
|
|
3
5
|
export declare function gatewayCacheDir(root: string): string;
|
|
4
6
|
/**
|
|
5
7
|
* The RUNTIME key for `rt.serviceKeys` — a workspace-qualified project id.
|
|
@@ -19,12 +21,18 @@ export declare function gatewayCacheDir(root: string): string;
|
|
|
19
21
|
export declare function serviceKeyId(wsKey: string | undefined, project: string): string;
|
|
20
22
|
/** Load (or generate + persist) a stable key per pod service, plus the inverse
|
|
21
23
|
* key→project map used to authenticate internal callers. Tolerant of a
|
|
22
|
-
* read-only fs (keys then live only for this process).
|
|
23
|
-
|
|
24
|
+
* read-only fs (keys then live only for this process).
|
|
25
|
+
*
|
|
26
|
+
* `taken` names key VALUES another workspace already holds: those are replaced with fresh ones and persisted
|
|
27
|
+
* (`regenerated`). A copy of a checkout — `cp -R`, or a copy-on-write clone of `node_modules` with its `.cache` —
|
|
28
|
+
* carries the original's key file, and two workspaces with one key value cannot be told apart: adopting it
|
|
29
|
+
* re-attributed the original's internal calls to the copy, which the gateway then refused as cross-workspace. */
|
|
30
|
+
export declare function loadServiceKeys(root: string, services: ServiceCtl[], wsKey?: string, taken?: (key: string) => boolean): {
|
|
24
31
|
serviceKeys: Record<string, string>;
|
|
25
32
|
keyToProject: Map<string, string>;
|
|
26
33
|
keyToWorkspace: Map<string, string>;
|
|
27
34
|
byProject: Record<string, string>;
|
|
35
|
+
regenerated: string[];
|
|
28
36
|
};
|
|
29
37
|
/**
|
|
30
38
|
* Mint + adopt keys for a workspace that JOINS a running daemon.
|
|
@@ -48,4 +56,5 @@ export declare function adoptServiceKeys(target: {
|
|
|
48
56
|
keyToWorkspace?: Map<string, string>;
|
|
49
57
|
}, root: string, services: ServiceCtl[], wsKey?: string): {
|
|
50
58
|
added: string[];
|
|
59
|
+
regenerated: string[];
|
|
51
60
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var l=Object.defineProperty;var
|
|
1
|
+
"use strict";var l=Object.defineProperty;var a=(e,c)=>l(e,"name",{value:c,configurable:!0});var h=Object.defineProperty,y=a((e,c)=>h(e,"name",{value:c,configurable:!0}),"y");Object.defineProperty(exports,"__esModule",{value:!0}),exports.gatewayCacheDir=gatewayCacheDir,exports.serviceKeyId=serviceKeyId,exports.loadServiceKeys=loadServiceKeys,exports.adoptServiceKeys=adoptServiceKeys;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),checkout_cache_1=require("../../../checkout-cache");function gatewayCacheDir(e){return(0,checkout_cache_1.checkoutCacheDir)(e,"davnx-webpack")}a(gatewayCacheDir,"gatewayCacheDir"),y(gatewayCacheDir,"gatewayCacheDir");function serviceKeyId(e,c){return`${e??""} ${c}`}a(serviceKeyId,"serviceKeyId"),y(serviceKeyId,"serviceKeyId");function loadServiceKeys(e,c,p,k){const n=gatewayCacheDir(e),v=path.join(n,"service-keys.json");let r={};try{r=JSON.parse(fs.readFileSync(v,"utf8"))}catch{}let u=!1;const d=[];for(const[o,f]of Object.entries(r))k?.(f)&&(r[o]=(0,node_crypto_1.randomBytes)(16).toString("hex"),u=!0,d.push(o));for(const o of c)r[o.project]||(r[o.project]=(0,node_crypto_1.randomBytes)(16).toString("hex"),u=!0);if(u)try{fs.mkdirSync(n,{recursive:!0}),fs.writeFileSync(v,JSON.stringify(r,null,2))}catch{}const t=p??c[0]?.wsKey,s={};for(const[o,f]of Object.entries(r))s[serviceKeyId(t,o)]=f;const i=new Map(Object.entries(r).map(([o,f])=>[f,o])),j=new Map(t?Object.values(r).map(o=>[o,t]):[]);return{serviceKeys:s,keyToProject:i,keyToWorkspace:j,byProject:r,regenerated:d}}a(loadServiceKeys,"loadServiceKeys"),y(loadServiceKeys,"loadServiceKeys");function adoptServiceKeys(e,c,p,k){const n=k??p[0]?.wsKey,v=y(t=>{const s=e.keyToWorkspace?.get(t);return s!==void 0&&s!==n},"taken"),{byProject:r,regenerated:u}=loadServiceKeys(c,p,n,v),d=[];for(const t of p){const s=serviceKeyId(t.wsKey??n,t.project),i=r[t.project];if(!i||e.serviceKeys[s]===i)continue;e.serviceKeys[s]=i,e.keyToProject.set(i,t.project);const j=t.wsKey??n;j&&e.keyToWorkspace?.set(i,j),d.push(t.project)}return{added:d,regenerated:u}}a(adoptServiceKeys,"adoptServiceKeys"),y(adoptServiceKeys,"adoptServiceKeys");
|
|
@@ -31,7 +31,7 @@ import type { GatewayRuntime, ServiceCtl } from './types';
|
|
|
31
31
|
/** Exits within {@link CRASH_WINDOW_MS} after which the host is given up on. */
|
|
32
32
|
export declare const HOST_MAX_RESTARTS = 3;
|
|
33
33
|
/** The workspace a service belongs to, as the relay needs it: its root, and its key (a registered one's). */
|
|
34
|
-
export type BuildMember = Pick<ServiceCtl, 'root'> & Partial<Pick<ServiceCtl, 'wsKey'>>;
|
|
34
|
+
export type BuildMember = Pick<ServiceCtl, 'root'> & Partial<Pick<ServiceCtl, 'wsKey' | 'sockNs'>>;
|
|
35
35
|
export interface SharedBuildRelay {
|
|
36
36
|
/** Will a runner of `svc` be compiled by its workspace's build host (it is then started with `LENSMCP_SHARED_BUILD=1`)? */
|
|
37
37
|
offeredTo(svc: BuildMember): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`)){const
|
|
1
|
+
"use strict";var A=Object.defineProperty;var y=(r,o)=>A(r,"name",{value:o,configurable:!0});var j=Object.defineProperty,i=y((r,o)=>j(r,"name",{value:o,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.HOST_MAX_RESTARTS=void 0,exports.buildHostScript=buildHostScript,exports.workspaceBuildHostScript=workspaceBuildHostScript,exports.createSharedBuildRelay=createSharedBuildRelay;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),node_module_1=require("node:module"),path=tslib_1.__importStar(require("node:path")),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),pod_env_1=require("./pod-env"),registration_guards_1=require("./registration-guards");exports.HOST_MAX_RESTARTS=3;const CRASH_WINDOW_MS=6e4;function buildHostScript(r=path.join(__dirname,"../../..")){const o=path.join(r,"build-host.js");if(fs.existsSync(o))return{path:o,execArgv:[]};const u=path.join(r,"build-host.ts");if(fs.existsSync(u))try{return{path:u,execArgv:["--require",require.resolve("@swc-node/register")]}}catch{return}}y(buildHostScript,"buildHostScript"),i(buildHostScript,"buildHostScript");function workspaceBuildHostScript(r){try{return{path:(0,node_module_1.createRequire)(path.join(r,"package.json")).resolve("@lensmcp/cluster/build-host.js"),execArgv:[]}}catch{return}}y(workspaceBuildHostScript,"workspaceBuildHostScript"),i(workspaceBuildHostScript,"workspaceBuildHostScript");function createSharedBuildRelay(r,o={}){const u=o.script===void 0?buildHostScript():o.script??void 0,_=o.scriptFor??workspaceBuildHostScript,h=new Map,f=new WeakMap,d=(0,registration_guards_1.canonicalRoot)(r.root),v=i(e=>{const a=(0,registration_guards_1.canonicalRoot)(e.root);let n=h.get(a);return n||(n=a===d?createWorkspaceRelay(r,{root:r.root,...r.wsKey?{wsKey:r.wsKey}:{},...r.sockNs?{sockNs:r.sockNs}:{},eventFile:r.eventFile},u,o):createWorkspaceRelay(r,{root:e.root,...e.wsKey?{wsKey:e.wsKey}:{},...e.sockNs?{sockNs:e.sockNs}:{},eventFile:path.join(e.root,".lensmcp","events.jsonl")},_(e.root),o),h.set(a,n)),n},"relayFor");return{offeredTo:i(e=>(0,protocol_1.sharedCompilerEnabled)(o.gatewayEnv??process.env,r.sharedCompiler)&&v(e).offered(),"offeredTo"),attach(e,a,n){const S=v(e);f.set(n,S),S.attach(`${e.project}#g${a}`,n)},detach:i(e=>f.get(e)?.detach(e),"detach"),hostPid:i((e=r.root)=>h.get((0,registration_guards_1.canonicalRoot)(e))?.hostPid(),"hostPid"),release(e){const a=(0,registration_guards_1.canonicalRoot)(e),n=h.get(a);return h.delete(a),n?.stop()??Promise.resolve()},async stop(){const e=[...h.values()];h.clear(),await Promise.all(e.map(a=>a.stop()))}}}y(createSharedBuildRelay,"createSharedBuildRelay"),i(createSharedBuildRelay,"createSharedBuildRelay");function createWorkspaceRelay(r,o,u,_){const h=_.spawnProcess??node_child_process_1.spawn,f=_.log??(t=>console.log(t)),d=new Map,v=new WeakMap;let e,a=[],n,S=!1;const E=(0,registration_guards_1.canonicalRoot)(o.root)===(0,registration_guards_1.canonicalRoot)(r.root)?"this workspace":`'${o.wsKey??path.basename(o.root)}'`,b=i((t,s)=>{if(!t.connected)return;const c={channel:protocol_1.SHARED_BUILD_CHANNEL,event:s};try{t.send(c,void 0,void 0,()=>{})}catch{}},"toRunner"),R=i(t=>{if(e?.connected)try{e.send(t,void 0,void 0,()=>{})}catch{}},"toHost"),x=i(()=>{if(_.reapStale===!1)return;let t;try{t=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return}for(const s of t.split(`
|
|
2
|
+
`)){const c=s.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!c||!(0,protocol_1.isBuildHostCommandOf)(c[3],o.root))continue;const l=Number(c[1]),p=Number(c[2]);if(!(l===e?.pid||l===process.pid)){try{process.kill(-p,"SIGKILL")}catch{try{process.kill(l,"SIGKILL")}catch{}}f(`[gateway] reaped a stale build host (pid ${l}) of ${E}`)}}},"reapStaleHosts"),N=i(()=>{if(e||S||n||!u)return e;x();const t=o.wsKey||path.basename(o.root),s={...(0,pod_env_1.podBaseEnv)(process.env,r.root,o.root),NX_DAEMON:"false",NODE_ENV:"development",NX_WORKSPACE_ROOT:o.root,...o.wsKey?{LENSMCP_WS_KEY:o.wsKey}:{},...o.sockNs?{LENSMCP_SOCK_NS:o.sockNs}:{},LENSMCP_EVENT_FILE:o.eventFile,LENSMCP_GATEWAY_PID:String(process.pid)},c=h((0,process_name_1.namedExecPath)((0,process_name_1.processName)(t,protocol_1.BUILD_HOST_ROLE)),[...u.execArgv,u.path,...(0,protocol_1.buildHostArgs)(o.root)],{cwd:o.root,detached:!0,env:s,stdio:["ignore","pipe","pipe","ipc"]});e=c,f(`[gateway] build host starting (one compilation for the services of ${E} \u2014 LENSMCP_SHARED_COMPILER)`);const l=i(p=>m=>{if(!r.stopped())for(const g of m.toString().split(`
|
|
3
3
|
`))g.trim()&&p.write(`[build] ${g}
|
|
4
|
-
`)},"relayOut");return
|
|
4
|
+
`)},"relayOut");return c.stdout?.on("data",l(process.stdout)),c.stderr?.on("data",l(process.stderr)),c.on("message",p=>{if(p?.type==="member"){const m=d.get(p.member);m&&b(m.proc,p.event)}}),c.on("exit",(p,m)=>{if(e===c&&(e=void 0),S||r.stopped())return;const g=Date.now();if(a=[...a.filter(w=>g-w<CRASH_WINDOW_MS),g],a.length>=exports.HOST_MAX_RESTARTS){n=`the build host exited ${a.length} times in a minute (last: ${p??m})`,f(`[gateway] ${n} \u2014 every service compiles on its own from now on`);for(const w of d.values())b(w.proc,{type:"refused",reason:n});d.clear();return}if(d.size!==0&&(f(`[gateway] build host exited (${p??m}) \u2014 restarting it for ${d.size} service tree(s)`),N()))for(const[w,{join:P}]of d)R({type:"join",member:w,join:P})}),c},"start"),k=i(t=>{const s=v.get(t);!s||d.get(s)?.proc!==t||(d.delete(s),R({type:"leave",member:s}))},"detach");return{offered:i(()=>!!u&&!n,"offered"),attach:i((t,s)=>{v.set(s,t),s.on("message",c=>{if((0,protocol_1.isRunnerJoinMessage)(c)){if(n||!N()){b(s,{type:"refused",reason:n??"the build host could not be started"});return}d.set(t,{proc:s,join:c.join}),R({type:"join",member:t,join:c.join})}}),s.once("exit",()=>k(s))},"attach"),detach:k,hostPid:i(()=>e?.pid,"hostPid"),stop(){S=!0,d.clear();const t=e;return e=void 0,!t||t.exitCode!==null||t.signalCode!==null?Promise.resolve():new Promise(s=>{const c=i(()=>{clearTimeout(l),s()},"done");t.once("exit",c);try{process.kill(-t.pid,"SIGTERM")}catch{try{t.kill("SIGTERM")}catch{}}const l=setTimeout(()=>{try{process.kill(-t.pid,"SIGKILL")}catch{try{t.kill("SIGKILL")}catch{}}s()},6e3);l.unref?.()})}}}y(createWorkspaceRelay,"createWorkspaceRelay"),i(createWorkspaceRelay,"createWorkspaceRelay");
|
|
@@ -55,6 +55,16 @@ export interface ClusterProjectDecl {
|
|
|
55
55
|
default?: boolean;
|
|
56
56
|
/** Override the auto `internal.<host>` name, or `false` to disable it. */
|
|
57
57
|
internalHost?: string | false;
|
|
58
|
+
/**
|
|
59
|
+
* The identities this service may NAME as its `x-api-key-id` on an `internal.<host>` call, besides its own
|
|
60
|
+
* project name. The gateway stamps `x-api-key-id` from the caller's validated key — its project — which is
|
|
61
|
+
* right for one identity per service, and collapsed a service that legitimately acts as several: FoodGuard's
|
|
62
|
+
* engine calls knowledge as `engine`, `console` and `platform` on one secret, knowledge grants each different
|
|
63
|
+
* stores, and every call arrived as `engine` (issues/closed/foodguard-50). A name listed here is kept when this
|
|
64
|
+
* service's own key presents it; anything else it claims is replaced by its project name, as before. A name
|
|
65
|
+
* that is ANOTHER service's project in the same workspace is never honoured — that would be impersonation.
|
|
66
|
+
*/
|
|
67
|
+
callerIds?: string[];
|
|
58
68
|
/** Edge auth policy (default + per-path overrides) — generated from controller decorators
|
|
59
69
|
* by @tetros-server/gateway-sync; the gateway verifies + injects identity + enforces it.
|
|
60
70
|
* Absent ⇒ the service is unguarded at the edge (current behavior — opt-in enforcement). */
|
|
@@ -99,6 +109,8 @@ export interface SockPool {
|
|
|
99
109
|
scannedAt: number;
|
|
100
110
|
}
|
|
101
111
|
export interface Route {
|
|
112
|
+
/** The workspace the route belongs to (its key) — whose edge middleware runs on it (workspace-middleware.ts). */
|
|
113
|
+
wsKey?: string;
|
|
102
114
|
host?: string;
|
|
103
115
|
prefix?: string;
|
|
104
116
|
/** For a host-less `default` catch-all: the apex it belongs to — so a shared multi-workspace gateway
|
|
@@ -137,6 +149,10 @@ export interface LensFrontend {
|
|
|
137
149
|
sourceRoots?: string[];
|
|
138
150
|
/** Set once the Vite child has been spawned (idempotency guard). */
|
|
139
151
|
started?: boolean;
|
|
152
|
+
/** A worktree instance's variables (instance.ts `InstanceRouting.env`), handed to this app's Vite. */
|
|
153
|
+
env?: Readonly<Record<string, string>>;
|
|
154
|
+
/** A FROZEN instance's app (instance.ts): its Vite watches nothing, and no source change hands it off. */
|
|
155
|
+
frozen?: boolean;
|
|
140
156
|
}
|
|
141
157
|
export interface SourceSetChange {
|
|
142
158
|
changedAt: number;
|
|
@@ -214,6 +230,17 @@ export interface ServiceCtl {
|
|
|
214
230
|
/** The owning workspace's key — passed as `LENSMCP_WS_KEY` to the spawn so the devserver writes its pods
|
|
215
231
|
* to `$TMPDIR/<wsKey>/<svc>-devserver` (the namespaced pool the daemon scans for this fragment). */
|
|
216
232
|
wsKey: string;
|
|
233
|
+
/** Set when the pods' socket folder is NOT `wsKey` (a worktree instance's short namespace — scope.ts
|
|
234
|
+
* `sockNs`): passed as `LENSMCP_SOCK_NS`, and the pool is `$TMPDIR/<sockNs>/<svc>-devserver`. */
|
|
235
|
+
sockNs?: string;
|
|
236
|
+
/** A worktree instance's variables (instance.ts `InstanceRouting.env`): its hosts and `instance.env`, injected
|
|
237
|
+
* into this service's tree under the gateway's own variables. */
|
|
238
|
+
instanceEnv?: Readonly<Record<string, string>>;
|
|
239
|
+
/** Set while this service waits for a cold-start slot ({@link MAX_COLD_STARTS}): later requests wait on it
|
|
240
|
+
* rather than queueing a second start. */
|
|
241
|
+
coldWait?: Promise<void>;
|
|
242
|
+
/** A FROZEN instance's service (instance.ts): compiled once, no watcher, no type checker — `LENSMCP_FREEZE=1`. */
|
|
243
|
+
frozen?: boolean;
|
|
217
244
|
pool: SockPool;
|
|
218
245
|
proc?: ChildProcess;
|
|
219
246
|
state: 'down' | 'starting' | 'up';
|
|
@@ -314,6 +341,15 @@ export declare const POD_SLOW_FIRST_BYTE_MS: number;
|
|
|
314
341
|
/** Per-probe timeout for `GET /__lensmcp/alive`. A live raw server answers a loopback
|
|
315
342
|
* unix-socket request in microseconds, so anything slower is indistinguishable from dead. */
|
|
316
343
|
export declare const POD_ALIVE_PROBE_TIMEOUT_MS: number;
|
|
344
|
+
/**
|
|
345
|
+
* How long a pod that ACCEPTS but stays silent on its liveness route is re-probed before the first-byte guard calls it
|
|
346
|
+
* wedged (issues/closed/foodguard-54). One missed 1 s probe used to be the verdict — and a LIVE pod in a long synchronous
|
|
347
|
+
* stretch (a large parse, a GC pause on a 650 MB heap) misses one exactly like a dead event loop does: the kernel accepts
|
|
348
|
+
* the connection either way. FoodGuard's engine was evicted mid-request at 30 s that way, its socket unlinked, the
|
|
349
|
+
* service recycled under the handler. A pod that answers within the window was busy; one silent for all of it is wedged.
|
|
350
|
+
* `0` restores the single-probe verdict.
|
|
351
|
+
*/
|
|
352
|
+
export declare const POD_WEDGE_CONFIRM_MS: number;
|
|
317
353
|
/** Internal liveness route served by the devserver child itself (`main.devserver.ts`). */
|
|
318
354
|
export declare const POD_ALIVE_PATH = "/__lensmcp/alive";
|
|
319
355
|
/**
|
|
@@ -446,6 +482,15 @@ export declare const POD_ROLL_READY_MS: number;
|
|
|
446
482
|
* `0` removes the cap. Default 2.
|
|
447
483
|
*/
|
|
448
484
|
export declare const POD_MAX_CONCURRENT_ROLLS: number;
|
|
485
|
+
/**
|
|
486
|
+
* How many services may COLD START at once, daemon-wide — every workspace's and every worktree instance's
|
|
487
|
+
* together. A cold start is a whole `serve-hmr` tree and a cold webpack compile; a page that touches a dozen lazy
|
|
488
|
+
* services, or two dozen agent worktrees waking at once, used to start them all in the same second and swap the
|
|
489
|
+
* machine (the 2026-10-06 stall had eleven compiles at once). The rest queue, first come first served, and start
|
|
490
|
+
* as slots free; a waiting request just waits longer. Override `LENSMCP_MAX_COLD_STARTS`; `0` removes the cap.
|
|
491
|
+
* Default: half the cores, at least 2.
|
|
492
|
+
*/
|
|
493
|
+
export declare const MAX_COLD_STARTS: number;
|
|
449
494
|
/** Spawn grace: a child is never probed (and so never recycled) until it has had this long to BIND its port
|
|
450
495
|
* — a cold vite/nx boot in a big monorepo takes many seconds, and killing a still-booting dev server would
|
|
451
496
|
* be a self-inflicted outage. Override `LENSMCP_LENS_PROBE_GRACE_MS`. Default 45s. */
|
|
@@ -659,6 +704,8 @@ export interface GatewayRuntime {
|
|
|
659
704
|
* multi-workspace gateway can host several workspaces without same-named services colliding. Flows to
|
|
660
705
|
* each service devserver as `LENSMCP_WS_KEY`. */
|
|
661
706
|
readonly wsKey: string;
|
|
707
|
+
/** This workspace's pod socket folder when it is not `wsKey` (scope.ts `sockNs`). */
|
|
708
|
+
readonly sockNs?: string;
|
|
662
709
|
readonly eventFile: string;
|
|
663
710
|
/** The LIVE merged route table (mutated in place from `registry` on every register/unregister, so the
|
|
664
711
|
* request pipeline — which reads `rt.routes` fresh per request — picks the change up with no restart). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var O=Object.defineProperty;var e=(_,E)=>O(_,"name",{value:E,configurable:!0});var M=Object.defineProperty,R=e((_,E)=>M(_,"name",{value:E,configurable:!0}),"R");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_RESET_RATE=exports.GATEWAY_H2_RESET_BURST=exports.BODY_STALL_MS=exports.GATEWAY_CONN_CHECK_MS=exports.GATEWAY_MAX_CONNECTIONS=exports.GATEWAY_STOP_GRACE_MS=exports.GATEWAY_HEADERS_TIMEOUT_MS=exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=exports.LENS_REQUEST_RECOVERY_WAIT_MS=exports.LENS_REPLACEMENT_DRAIN_MS=exports.LENS_REPLACEMENT_READY_MS=exports.LENS_RESTART_MODE=exports.LENS_RECYCLE_KILL_GRACE_MS=exports.LENS_PARKED_PORT_REPROBE_MS=exports.MAX_PORT_CONFLICT_RETRIES=exports.CHILD_START_FAIL_MS=exports.PORT_FREE_POLL_MS=exports.PORT_FREE_WAIT_MS=exports.LENS_PROBE_BOOT_GRACE_MS=exports.LENS_PROBE_COOLDOWN_MS=exports.LENS_PROBE_TIMEOUT_MS=exports.LENS_PROBE_FAILS=exports.LENS_PROBE_GRACE_MS=exports.MAX_COLD_STARTS=exports.POD_MAX_CONCURRENT_ROLLS=exports.POD_ROLL_READY_MS=exports.POD_RECYCLE_MODE=exports.POD_WATCHER_SILENT=exports.POD_WATCHER_UNVERIFIED=exports.LENS_WATCH_SCOPE_ENABLED=exports.LENS_WATCHER_UNVERIFIED=exports.LENS_WATCHER_PROBE_ENABLED=exports.LENS_PROBE_ENABLED=exports.POD_STALE_RECYCLE_COOLDOWN_MS=exports.POD_STALE_SCAN_MS=exports.POD_RECYCLE_SETTLE_MS=exports.POD_STALE_GRACE_MS=exports.POD_MAX_AGE_MS=exports.POD_RECYCLE_ENABLED=exports.WEDGE_RECYCLE_GRACE_MS=exports.POD_ALIVE_PATH=exports.POD_WEDGE_CONFIRM_MS=exports.POD_ALIVE_PROBE_TIMEOUT_MS=exports.POD_SLOW_FIRST_BYTE_MS=exports.POD_RESPONSE_TIMEOUT_MS=exports.UPSTREAM_HEAL_STEP_MS=exports.UPSTREAM_HEAL_WINDOW_MS=exports.SERVICE_RESTART_WINDOW_MS=exports.SERVICE_MAX_RESTARTS=exports.SERVICE_ERROR_THROTTLE_MS=void 0;const tslib_1=require("tslib"),os=tslib_1.__importStar(require("node:os"));function envInt(_,E,L=!1){const S=Number(process.env[_]);return!Number.isFinite(S)||S<0?E:S===0?L?0:E:S}e(envInt,"envInt"),R(envInt,"envInt"),exports.SERVICE_ERROR_THROTTLE_MS=5e3,exports.SERVICE_MAX_RESTARTS=5,exports.SERVICE_RESTART_WINDOW_MS=6e4,exports.UPSTREAM_HEAL_WINDOW_MS=8e3,exports.UPSTREAM_HEAL_STEP_MS=300,exports.POD_RESPONSE_TIMEOUT_MS=(()=>{const _=Number(process.env.LENSMCP_POD_RESPONSE_TIMEOUT_MS);return Number.isFinite(_)&&_>0?_:3e4})(),exports.POD_SLOW_FIRST_BYTE_MS=envInt("LENSMCP_POD_SLOW_FIRST_BYTE_MS",10*6e4,!0),exports.POD_ALIVE_PROBE_TIMEOUT_MS=envInt("LENSMCP_POD_ALIVE_PROBE_TIMEOUT_MS",1e3),exports.POD_WEDGE_CONFIRM_MS=envInt("LENSMCP_POD_WEDGE_CONFIRM_MS",1e4,!0),exports.POD_ALIVE_PATH="/__lensmcp/alive",exports.WEDGE_RECYCLE_GRACE_MS=(()=>{const _=Number(process.env.LENSMCP_WEDGE_RECYCLE_GRACE_MS);return Number.isFinite(_)&&_>0?_:15e3})(),exports.POD_RECYCLE_ENABLED=process.env.LENSMCP_POD_RECYCLE!=="0",exports.POD_MAX_AGE_MS=envInt("LENSMCP_POD_MAX_AGE_MS",240*6e4,!0),exports.POD_STALE_GRACE_MS=envInt("LENSMCP_POD_STALE_GRACE_MS",1e4),exports.POD_RECYCLE_SETTLE_MS=envInt("LENSMCP_POD_RECYCLE_SETTLE_MS",8e3),exports.POD_STALE_SCAN_MS=envInt("LENSMCP_POD_STALE_SCAN_MS",3e4),exports.POD_STALE_RECYCLE_COOLDOWN_MS=envInt("LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS",6e4),exports.LENS_PROBE_ENABLED=process.env.LENSMCP_LENS_PROBE!=="0",exports.LENS_WATCHER_PROBE_ENABLED=process.env.LENSMCP_LENS_WATCHER_PROBE!=="0",exports.LENS_WATCHER_UNVERIFIED=process.env.LENSMCP_LENS_WATCHER_UNVERIFIED==="keep"?"keep":"recycle",exports.LENS_WATCH_SCOPE_ENABLED=process.env.LENSMCP_LENS_WATCH_SCOPE!=="0",exports.POD_WATCHER_UNVERIFIED=process.env.LENSMCP_POD_WATCHER_UNVERIFIED==="keep"?"keep":"recycle",exports.POD_WATCHER_SILENT=process.env.LENSMCP_POD_WATCHER_SILENT==="recycle"?"recycle":"keep",exports.POD_RECYCLE_MODE=process.env.LENSMCP_POD_RECYCLE_MODE==="in-place"?"in-place":"rolling",exports.POD_ROLL_READY_MS=envInt("LENSMCP_POD_ROLL_READY_MS",18e4),exports.POD_MAX_CONCURRENT_ROLLS=envInt("LENSMCP_POD_MAX_CONCURRENT_ROLLS",2,!0),exports.MAX_COLD_STARTS=envInt("LENSMCP_MAX_COLD_STARTS",Math.max(2,Math.floor(os.availableParallelism()/2)),!0),exports.LENS_PROBE_GRACE_MS=envInt("LENSMCP_LENS_PROBE_GRACE_MS",45e3),exports.LENS_PROBE_FAILS=envInt("LENSMCP_LENS_PROBE_FAILS",2),exports.LENS_PROBE_TIMEOUT_MS=envInt("LENSMCP_LENS_PROBE_TIMEOUT_MS",1e3),exports.LENS_PROBE_COOLDOWN_MS=envInt("LENSMCP_LENS_PROBE_COOLDOWN_MS",3e4),exports.LENS_PROBE_BOOT_GRACE_MS=envInt("LENSMCP_LENS_PROBE_BOOT_GRACE_MS",24e4),exports.PORT_FREE_WAIT_MS=envInt("LENSMCP_PORT_FREE_WAIT_MS",1e4),exports.PORT_FREE_POLL_MS=envInt("LENSMCP_PORT_FREE_POLL_MS",250),exports.CHILD_START_FAIL_MS=envInt("LENSMCP_CHILD_START_FAIL_MS",1e4),exports.MAX_PORT_CONFLICT_RETRIES=envInt("LENSMCP_MAX_PORT_CONFLICT_RETRIES",5),exports.LENS_PARKED_PORT_REPROBE_MS=envInt("LENSMCP_PARKED_PORT_REPROBE_MS",3e5),exports.LENS_RECYCLE_KILL_GRACE_MS=envInt("LENSMCP_RECYCLE_KILL_GRACE_MS",5e3),exports.LENS_RESTART_MODE=process.env.LENSMCP_LENS_RESTART_MODE==="in-place"?"in-place":"rolling",exports.LENS_REPLACEMENT_READY_MS=envInt("LENSMCP_LENS_REPLACEMENT_READY_MS",3e4),exports.LENS_REPLACEMENT_DRAIN_MS=envInt("LENSMCP_LENS_REPLACEMENT_DRAIN_MS",2e3,!0),exports.LENS_REQUEST_RECOVERY_WAIT_MS=envInt("LENSMCP_LENS_REQUEST_RECOVERY_WAIT_MS",15e3),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=envInt("LENSMCP_KEEPALIVE_TIMEOUT_MS",76e3),exports.GATEWAY_HEADERS_TIMEOUT_MS=envInt("LENSMCP_HEADERS_TIMEOUT_MS",6e4),exports.GATEWAY_STOP_GRACE_MS=envInt("LENSMCP_STOP_GRACE_MS",3e3),exports.GATEWAY_MAX_CONNECTIONS=envInt("LENSMCP_MAX_CONNECTIONS",0,!0),exports.GATEWAY_CONN_CHECK_MS=envInt("LENSMCP_CONN_CHECK_MS",0,!0),exports.BODY_STALL_MS=envInt("LENSMCP_BODY_STALL_MS",6e4,!0),exports.GATEWAY_H2_RESET_BURST=envInt("LENSMCP_H2_RESET_BURST",1e5),exports.GATEWAY_H2_RESET_RATE=envInt("LENSMCP_H2_RESET_RATE",1e4);
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The WebSocket upgrade pipeline. WS upgrades enforce the SAME trust model as
|
|
3
|
-
* HTTP — otherwise an `internal.<host>` socket would be reachable with no
|
|
4
|
-
* x-api-key, and a public socket would reach pods without the x-internal-token
|
|
5
|
-
* they expect. Reuses the shared auth + proxy layers so it can't drift from the
|
|
6
|
-
* HTTP handler.
|
|
7
|
-
*/
|
|
8
1
|
import type * as http from 'node:http';
|
|
9
2
|
import type * as stream from 'node:stream';
|
|
10
3
|
import type { GatewayRuntime } from './types';
|
|
11
4
|
import type { EdgeAuth } from './auth';
|
|
12
5
|
import type { ProxyLayer } from './proxy';
|
|
13
6
|
import type { HooksLayer } from './hooks';
|
|
7
|
+
import type { Observability } from './observability';
|
|
14
8
|
export interface UpgradeDeps {
|
|
15
9
|
rt: GatewayRuntime;
|
|
16
10
|
auth: EdgeAuth;
|
|
17
11
|
proxy: ProxyLayer;
|
|
18
12
|
hooks: HooksLayer;
|
|
13
|
+
/** Where a refused lens HMR socket is reported (see {@link watchLensHmr}). */
|
|
14
|
+
obs?: Pick<Observability, 'emit'>;
|
|
19
15
|
}
|
|
20
|
-
|
|
16
|
+
/** One report per frontend per this window: a reload storm must not become a log storm. */
|
|
17
|
+
export declare const LENS_HMR_REFUSAL_REPORT_MS = 10000;
|
|
18
|
+
export type UpgradeListener = ((req: http.IncomingMessage, socket: stream.Duplex, head: Buffer) => void) & {
|
|
19
|
+
/**
|
|
20
|
+
* Destroy every live tunnel — both its sockets — for the gateway's `stop()`. An upgraded socket leaves `http`'s
|
|
21
|
+
* connection tracking, so `closeAllConnections` never reaches it, and a tunnel whose upstream ignores a half-close
|
|
22
|
+
* stays open for good (issues/closed/test-infra-09). Returns how many sockets were destroyed.
|
|
23
|
+
*/
|
|
24
|
+
closeTunnels(): number;
|
|
25
|
+
};
|
|
21
26
|
export declare function createUpgrade(deps: UpgradeDeps): UpgradeListener;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var K=Object.defineProperty;var w=(c,s)=>K(c,"name",{value:s,configurable:!0});var M=Object.defineProperty,u=w((c,s)=>M(c,"name",{value:s,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.LENS_HMR_REFUSAL_REPORT_MS=void 0,exports.createUpgrade=createUpgrade;const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path")),manifest_1=require("../manifest"),discovery_1=require("./discovery"),edge_1=require("./edge"),hmr_reload_1=require("./hmr-reload"),service_keys_1=require("./service-keys");exports.LENS_HMR_REFUSAL_REPORT_MS=1e4;function accountUpgrade(c,s){const i=c.svc;if(!i||s.destroyed)return;i.inflight+=1,i.lastUsed=Date.now();let d=!1;s.once("close",()=>{d||(d=!0,i.inflight-=1,i.lastUsed=Date.now())})}w(accountUpgrade,"accountUpgrade"),u(accountUpgrade,"accountUpgrade");function createUpgrade(c){const{rt:s,auth:i,proxy:d,hooks:p,obs:b}=c,y=new Set,_=u(e=>{e.destroyed||(y.add(e),e.once("close",()=>y.delete(e)))},"track");d.proxy.on?.("open",_);const v=new WeakMap,k=new Map,m=u((e,r)=>{const n=Date.now();n-(k.get(e.frontendKey)??0)<exports.LENS_HMR_REFUSAL_REPORT_MS||(k.set(e.frontendKey,n),console.warn(`[gateway] lens HMR socket refused: ${e.frontendKey} at ${e.target} answered ${r} for a tab on ${e.host} \u2014 that tab loads but gets no hot updates; the lens runtime reloads it once if it had a socket before.`),b?.emit("warning",`lens HMR socket refused: ${e.frontendKey} (${r})`,`cluster-lens-hmr-refused:${e.frontendKey}`,{kind:"lens-hmr-refused",frontendKey:e.frontendKey,host:e.host,target:e.target,answer:r},void 0,e.busFile))},"reportRefusal");d.proxy.on?.("proxyReqWs",(e,r)=>{const n=v.get(r);if(!n)return;const a=e;a.once("upgrade",()=>{n.answered=!0}),a.once("response",g=>{n.answered=!0,m(n,`HTTP ${g.statusCode??"?"}`)})});const S=u(async(e,r,n)=>{(0,edge_1.stripOwnedHeaders)(e);const a=(0,edge_1.canonicalize)(e.url??"/");if(a===void 0){r.destroy();return}e.url=a.path+a.query;const g=(e.headers.host||"").split(":")[0],t=(0,manifest_1.matchRoute)(s.routes,g,a.policy+a.query);if(!t){r.destroy();return}(0,edge_1.resolveClientIp)(s,e);const l=p.upgradeContext(e,r,void 0);if(l.route=t,t.internal){const o=i.internal(e,t,void 0);if(!o.ok){r.destroy();return}l.caller=o.caller}else{const o=i.public(e,t,void 0,a.policy);if(!o.ok){r.destroy();return}if(l.claims=o.claims,p.has("afterAuth")&&(await p.run("afterAuth",l),l.responded()))return}const x=t.pool?s.serviceKeys[(0,service_keys_1.serviceKeyId)(t.svc?.wsKey,t.project)]:void 0;if(x&&(e.headers["x-internal-token"]=x),t.prependPrefix&&!e.url?.startsWith(t.prependPrefix)&&(e.url=t.prependPrefix+(e.url??"/")),(0,edge_1.stampForwardingHeaders)(s,e),p.has("onUpgrade")&&(await p.run("onUpgrade",l),l.responded()))return;const h=t.pool?(()=>{const o=(0,discovery_1.pickSock)(t.pool,s.scanTtlMs);return o?{socketPath:o}:void 0})():t.target;if(!h){r.destroy();return}if(t.lens&&String(e.headers["sec-websocket-protocol"]??"").split(",").map(o=>o.trim()).includes("vite-hmr")){const o=t.lens.workspaceKey?`${t.lens.workspaceKey}:${t.lens.project}`:t.lens.project;(0,hmr_reload_1.registerHmrClient)(s,o,r);const f=!t.lens.workspaceRoot||path.resolve(t.lens.workspaceRoot)===path.resolve(s.root??"");v.set(e,{frontendKey:o,host:g,target:String(h),answered:!1,...f?{}:{busFile:path.join(t.lens.workspaceRoot,".lensmcp","events.jsonl")}})}accountUpgrade(t,r),d.proxy.ws(e,r,n,{target:h,changeOrigin:!t.pool,agent:d.agentForTarget(h)},o=>{const f=v.get(e);f&&!f.answered&&m(f,o?.code??o?.message??"an error"),r.destroy()})},"run"),R=u(((e,r,n)=>{r.on("error",()=>r.destroy()),_(r),S(e,r,n).catch(()=>r.destroy())}),"listener");return R.closeTunnels=()=>{const e=[...y];for(const r of e)r.destroy();return y.clear(),e.length},R}w(createUpgrade,"createUpgrade"),u(createUpgrade,"createUpgrade");
|