@lensmcp/cluster 1.21.4 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/executors/gateway/jwks-verify.js +1 -1
  2. package/executors/gateway/jwt-claims.d.ts +34 -0
  3. package/executors/gateway/jwt-claims.js +1 -0
  4. package/executors/gateway/main.prod-gateway.js +2 -2
  5. package/executors/gateway/prod-config.d.ts +27 -0
  6. package/executors/gateway/prod-config.js +1 -1
  7. package/executors/gateway/rate-limit.d.ts +9 -0
  8. package/executors/gateway/rate-limit.js +2 -2
  9. package/executors/gateway/runtime/auth.js +1 -1
  10. package/executors/gateway/runtime/chooser.d.ts +30 -9
  11. package/executors/gateway/runtime/chooser.js +9 -9
  12. package/executors/gateway/runtime/control.d.ts +17 -2
  13. package/executors/gateway/runtime/control.js +2 -2
  14. package/executors/gateway/runtime/edge.d.ts +29 -3
  15. package/executors/gateway/runtime/edge.js +1 -1
  16. package/executors/gateway/runtime/handler.js +1 -1
  17. package/executors/gateway/runtime/lens-children.js +1 -1
  18. package/executors/gateway/runtime/lifecycle.d.ts +21 -0
  19. package/executors/gateway/runtime/lifecycle.js +3 -3
  20. package/executors/gateway/runtime/proxy.d.ts +25 -0
  21. package/executors/gateway/runtime/proxy.js +1 -1
  22. package/executors/gateway/runtime/server.d.ts +90 -0
  23. package/executors/gateway/runtime/server.js +2 -2
  24. package/executors/gateway/runtime/types.d.ts +20 -0
  25. package/executors/gateway/runtime/types.js +1 -1
  26. package/executors/gateway/runtime/upgrade.js +1 -1
  27. package/executors/serve/serve.impl.js +1 -1
  28. package/executors/trust/hostname.d.ts +20 -0
  29. package/executors/trust/hostname.js +1 -0
  30. package/executors/trust/trust.impl.js +5 -3
  31. package/main.devserver.d.ts +50 -0
  32. package/main.devserver.js +7 -7
  33. package/package.json +6 -6
@@ -1 +1 @@
1
- "use strict";var ye=Object.defineProperty;var P=(s,f)=>ye(s,"name",{value:f,configurable:!0});var Se=Object.defineProperty,d=P((s,f)=>Se(s,"name",{value:f,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.probeWatcherHealth=probeWatcherHealth,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.claimPort=claimPort,exports.createLensChildren=createLensChildren;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_child_process_1=require("node:child_process"),lens_frontend_1=require("@lensmcp/nx-plugin/lens-frontend"),hmr_reload_1=require("./hmr-reload"),pod_env_1=require("./pod-env"),scope_1=require("./scope"),workspace_registry_1=require("./workspace-registry"),lifecycle_1=require("./lifecycle"),types_1=require("./types"),observability_1=require("./observability"),HEAL_BASE_MS=1500,HEAL_MAX_MS=3e4,HEAL_HEALTHY_MS=2e4;function tryBind(s,f){return new Promise(u=>{const _=net.createServer();_.unref(),_.once("error",p=>{const E=p.code??"";u(E==="EADDRINUSE"||E==="EACCES"?"in-use":"unavailable")}),_.listen(f===void 0?{port:s}:{port:s,host:f},()=>_.close(()=>u("free")))})}P(tryBind,"tryBind"),d(tryBind,"tryBind");async function probePortFree(s){for(const f of[void 0,"0.0.0.0","127.0.0.1","::1"])if(await tryBind(s,f)==="in-use")return!1;return!0}P(probePortFree,"probePortFree"),d(probePortFree,"probePortFree");function probeConnect(s,f,u){return new Promise(_=>{let p=!1;const E=net.createConnection({host:s,port:f});E.unref();const M=setTimeout(()=>y(!1),u);M.unref?.();function y(k){p||(p=!0,clearTimeout(M),E.destroy(),_(k))}P(y,"g"),d(y,"finish"),E.once("connect",()=>y(!0)),E.once("error",()=>y(!1))})}P(probeConnect,"probeConnect"),d(probeConnect,"probeConnect");async function probePortListening(s,f=1e3){return await probeConnect("127.0.0.1",s,f)?!0:probeConnect("::1",s,f)}P(probePortListening,"probePortListening"),d(probePortListening,"probePortListening");async function probeWatcherHealth(s,f=1500){try{const u=new AbortController,_=setTimeout(()=>u.abort(),f);_.unref?.();try{const p=await fetch(`http://127.0.0.1:${s}/__lensmcp/watcher-health`,{signal:u.signal});if(!p.ok)return null;const E=await p.json();if(typeof E?.lastFsSetEventAt!="number")return null;const M=Array.isArray(E.recentEvents)?E.recentEvents.filter(y=>y&&typeof y.path=="string"&&typeof y.at=="number"&&typeof y.sequence=="number"&&["add","addDir","unlink","unlinkDir"].includes(y.kind)):[];return{schemaVersion:E.schemaVersion===2?2:1,ready:E.ready!==!1,sequence:typeof E.sequence=="number"?E.sequence:0,lastFsSetEventAt:E.lastFsSetEventAt,fsSetEvents:typeof E.fsSetEvents=="number"?E.fsSetEvents:0,recentEvents:M}}finally{clearTimeout(_)}}catch{return null}}P(probeWatcherHealth,"probeWatcherHealth"),d(probeWatcherHealth,"probeWatcherHealth");const sleepUnref=d(s=>new Promise(f=>{setTimeout(f,s).unref?.()}),"sleepUnref");async function waitForPortFree(s,f,u={}){const _=u.isPortFree??probePortFree,p=Math.max(1,u.intervalMs??types_1.PORT_FREE_POLL_MS),E=Date.now()+Math.max(0,f);for(;;){if(await _(s))return!0;const M=E-Date.now();if(M<=0)return!1;await sleepUnref(Math.min(p,M))}}P(waitForPortFree,"waitForPortFree"),d(waitForPortFree,"waitForPortFree");function viteArgPort(s){const f=d(u=>Number.isInteger(u)&&u>0&&u<65536?u:void 0,"valid");for(let u=0;u<s.length;u+=1){const _=s[u];if(_==="--port")return f(Number(s[u+1]));if(_.startsWith("--port="))return f(Number(_.slice(7)))}}P(viteArgPort,"viteArgPort"),d(viteArgPort,"viteArgPort");async function claimPort(s,f,u,_){let p=s;for(;f.has(p)||_?.has(p)||!await u(p);)p+=1;return f.add(p),p}P(claimPort,"claimPort"),d(claimPort,"claimPort");function createLensChildren(s,f,u,_){const{emit:p}=f,E=_?.isPortFree??probePortFree,M=_?.probeWatcherHealth??probeWatcherHealth,y=[],k=new Map,m=new Map,j=new Map,B=new Map,K=new Set;let O;const A=new Map,$=new Set,W=new Set,N=new Set,v=new Map,D=new WeakMap,x=new Set,z=d((e,r,n)=>{if(x.has(e))return;x.add(e);const t=setInterval(()=>{(async()=>{if(s.stopped()||$.has(e)){clearInterval(t),x.delete(e);return}await E(r)&&(clearInterval(t),x.delete(e),console.log(`[gateway] lens child ${e}: :${r} freed \u2014 respawning.`),n())})().catch(()=>{})},types_1.LENS_PARKED_PORT_REPROBE_MS);t.unref?.()},"parkUntilPortFree"),F=d(async(e,r,n)=>{const t=await claimPort(e,r,E,n?.reserved);return t!==e&&(n?.heldBy?console.log(`[gateway] handoff: :${e} is held by ${n.heldBy} \u2014 replacement on :${t}.`):console.warn(`[gateway] port ${e} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${t} instead.`)),t},"claimFreePort"),ae=d((e,r,n)=>{const t=n?.LENSMCP_FRONTEND_KEY,o=Number(n?.LENSMCP_FRONTEND_GENERATION);if(!t||!Number.isFinite(o))return;const a=[...v.get(t)?.generations??[]].find(i=>i.generation===o);a&&(e.endsWith(":vite")?a.vite=r:e.endsWith(":browser-capture")&&(a.capture=r))},"attachRespawnedGenerationChild"),T=d((e,r,n,t,o=!1,a,i,l)=>{const w=Date.now(),c=(0,node_child_process_1.spawn)(r,n,{cwd:i??s.root,env:{...(0,pod_env_1.podBaseEnv)(process.env,s.root,i??s.root),...t??{},LENSMCP_PARENT_PID:String(process.pid)},stdio:"inherit"});y.push(c),A.set(e,c),ae(e,c,t);const S=e==="vite"||e.endsWith(":vite"),C=typeof t?.LENSMCP_EVENT_FILE=="string"?t.LENSMCP_EVENT_FILE:void 0,R=C&&C!==s.eventFile?C:void 0,g=S?viteArgPort(n):l;S&&m.set(c,{key:t?.LENSMCP_FRONTEND_KEY??`${i??s.root}:${t?.LENSMCP_PROJECT??"app"}`,project:t?.LENSMCP_PROJECT??"app",workspaceRoot:i??s.root,spawnedAt:w,port:g,probeFails:0,listening:!1,generation:Number(t?.LENSMCP_FRONTEND_GENERATION??0),role:t?.LENSMCP_FRONTEND_ROLE==="replacement"?"replacement":"active",...R?{bus:R}:{}});const h=e==="lens-mcp"?"lens-mcp":e==="lens-dashboard"||e.startsWith("lens-dashboard:")?"lens-dashboard":void 0;return h&&g!==void 0&&(p("info",`lens child starting: ${h} (:${g})`,`cluster-lens-child:${h}`,{kind:"lens-child-starting",project:h,label:e,pid:c.pid,port:g},void 0,R),(async()=>{const b=Date.now()+types_1.LENS_REPLACEMENT_READY_MS;for(;!s.stopped()&&Date.now()<b;){if(c.exitCode!==null||c.signalCode!==null||$.has(e))return;if(await probePortListening(g,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS))){p("info",`lens child up: ${h} (:${g})`,`cluster-lens-child:${h}`,{kind:"lens-child-up",project:h,label:e,pid:c.pid,port:g},void 0,R);return}await sleepUnref(150)}})().catch(()=>{})),c.on("exit",(b,V)=>{const te=y.indexOf(c);te>=0&&y.splice(te,1),A.get(e)===c&&A.delete(e);const oe=m.get(c);m.delete(c);const we=$.delete(e);if(s.stopped()||we||(h&&p("warning",`lens child exited: ${h}`,`cluster-lens-child:${h}`,{kind:"lens-child-exit",project:h,label:e,pid:c.pid,code:b,signal:V},void 0,R),oe&&ce(c,oe)))return;const ne=Date.now()-w,L=k.get(e)??{attempts:0,portConflicts:0};(async()=>{const _e=g!==void 0&&!await E(g),re=(0,lifecycle_1.isPortConflictExit)({code:b,signal:V,ranMs:ne,portHeld:_e},{fastFailMs:types_1.CHILD_START_FAIL_MS});if(re&&L.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)L.portConflicts+=1,k.set(e,L),console.warn(`[gateway] lens child ${e} lost the :${g} bind race (EADDRINUSE, code=${b}); waiting for the port, then retrying (port-retry ${L.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else if(re){L.portConflicts=0,k.set(e,L),console.warn(`[gateway] lens child ${e}: :${g} persistently held after ${types_1.MAX_PORT_CONFLICT_RETRIES} bind races \u2014 another server owns it (an external/shared daemon?). Parking; re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s, will respawn if it frees.`),p("warning",`lens child parked: ${e} (:${g} held externally)`,`cluster-lens-child:${e}`,{kind:"lens-child-parked",label:e,port:g}),z(e,g,()=>T(e,r,n,t,o,a,i,l));return}else{L.portConflicts=0,ne>=HEAL_HEALTHY_MS&&(L.attempts=0);const se=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**L.attempts);L.attempts+=1,k.set(e,L),console.warn(`[gateway] lens child ${e} exited (code=${b} sig=${V}); auto-healing in ${Math.round(se/1e3)}s (attempt ${L.attempts}).`),await sleepUnref(se)}s.stopped()||$.has(e)||(g!==void 0&&!await waitForPortFree(g,types_1.PORT_FREE_WAIT_MS,{isPortFree:E,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${e}: :${g} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(s.stopped()||$.has(e))&&T(e,r,n,t,o,a,i,l))})().catch(()=>{})}),c},"spawnManagedChild"),X=d(e=>{const r=`${e.wsKey}:${e.project}`,n=v.get(r);if(n)return n;const t={key:r,...e,sourceDirs:e.frontend.sourceRoots?.length?e.frontend.sourceRoots:[e.projectRoot],generation:0,state:"starting",generations:new Set,disposed:!1};v.set(r,t);for(const o of e.routes)o.lens&&(o.lens.workspaceKey??=e.wsKey,D.set(o.lens,t));return e.frontend.workspaceKey??=e.wsKey,D.set(e.frontend,t),t},"registerFrontend"),ie=d(e=>{if(!types_1.LENS_WATCH_SCOPE_ENABLED)return{};const r=path.resolve(e.projectRoot),n=e.sourceDirs.map(t=>path.resolve(t)).filter(t=>t!==r);return n.length>0?{LENSMCP_WATCH_DIRS:n.join(path.delimiter)}:{}},"lensWatchScopeEnv"),J=d((e,r,n)=>{const t=++e.generation,o=`${e.key}:g${t}:`,a=`${o}vite`,i=`${o}browser-capture`;$.delete(a),$.delete(i);const l=d((S,C,R,g,h,b)=>T(S,C,R,g,h,b,e.workspaceRoot),"spawnInRoot"),w=(0,lens_frontend_1.spawnLensFrontend)({projectRoot:e.projectRoot,workspaceRoot:e.workspaceRoot,project:e.project,eventFile:e.eventFile,port:r,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:e.project,LENSMCP_FRONTEND_KEY:e.key,LENSMCP_FRONTEND_GENERATION:String(t),LENSMCP_FRONTEND_ROLE:n,...ie(e),NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false"},labelPrefix:o,log:d(S=>console.log(S),"log")},l),c={ownerKey:e.key,generation:t,port:r,labelPrefix:o,vite:w?.vite??A.get(a),capture:A.get(i)};return e.generations.add(c),c},"spawnFrontendGeneration"),I=d((e,r)=>{if(!e)return;v.get(e.ownerKey)?.generations.delete(e);const n=[[`${e.labelPrefix}browser-capture`,e.capture],[`${e.labelPrefix}vite`,e.vite]];for(const[t,o]of n)if($.add(t),!(!o||o.exitCode!==null||o.signalCode!==null)){try{o.kill("SIGTERM")}catch{}setTimeout(()=>{if(o.exitCode===null&&o.signalCode===null)try{o.kill("SIGKILL")}catch{}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}r&&N.delete(e.port)},"terminateGeneration"),le=d(async e=>{const r=Date.now()+types_1.LENS_REPLACEMENT_READY_MS;for(;!s.stopped()&&Date.now()<r;){const n=e.vite;if(!n||n.exitCode!==null||n.signalCode!==null)return!1;if(await probePortListening(e.port,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS))){const t=await M(e.port);if(!t||t.ready)return!0}await sleepUnref(150)}return!1},"waitForGenerationReady"),G=d(async e=>{const r=e?.vite;return!!(e&&r&&r.exitCode===null&&r.signalCode===null&&await probePortListening(e.port,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS)))},"generationIsServing"),H=d(e=>!e.disposed&&!s.stopped()&&v.get(e.key)===e,"frontendIsCurrent"),Y=d((e,r)=>{if(!H(e))return Promise.resolve(!1);if(e.replacement)return e.replacement;const n=e.active;e.state=n?"replacing":"starting",e.reason=r,e.startedAt=Date.now();const t=(async()=>{let o,a;const i=Date.now();try{const l=new Set;for(const h of v.values())h!==e&&l.add(h.desiredPort);const w=n&&n.port===e.desiredPort?`g${n.generation}`:void 0,c=await F(e.desiredPort,N,{reserved:l,heldBy:w});if(a=c,!H(e))return N.delete(c),!1;o=J(e,c,"replacement"),e.candidate=o;const S=await G(n);console.log(`[gateway] lens handoff start: ${e.key} g${n?.generation??0}\u2192g${o.generation} :${n?.port??"down"}\u2192:${c} reason=${r}`),p("info",`lens handoff starting: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-start",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,oldPort:n?.port,newPort:c,oldServing:S},void 0,e.eventFile===s.eventFile?void 0:e.eventFile);const C=await le(o);if(!H(e))return I(o,!0),e.candidate=void 0,!1;if(!C){I(o,!0),e.candidate=void 0;const h=await G(n);return e.state=h?"ready":"failed",h&&(e.reason=void 0,e.startedAt=void 0),console.error(`[gateway] lens handoff failed: ${e.key} g${o.generation} did not become ready within ${types_1.LENS_REPLACEMENT_READY_MS}ms; ${e.state==="ready"?"old generation kept serving":"no active generation"}.`),p("warning",`lens handoff failed: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-failed",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,port:c,durationMs:Date.now()-i,oldKept:h},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),!1}for(const h of e.routes)h.target=`http://localhost:${o.port}`,h.lens&&(h.lens.port=o.port);e.frontend.port=o.port,e.active=o,e.candidate=void 0,e.state="ready",e.reason=void 0,e.startedAt=void 0;const R=o.vite?m.get(o.vite):void 0;R&&(R.role="active",R.listening=!0);const g=(0,hmr_reload_1.broadcastFullReload)(s,e.key);if(console.log(`[gateway] lens handoff ready: ${e.key} g${o.generation} :${o.port} in ${Date.now()-i}ms; draining g${n?.generation??0}${g>0?`; reloading ${g} tab(s)`:""}.`),p("info",`lens handoff complete: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-complete",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,oldPort:n?.port,newPort:o.port,durationMs:Date.now()-i,reloadedTabs:g},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),n){const h=`${n.labelPrefix}browser-capture`;$.add(h);try{n.capture?.kill("SIGTERM")}catch{}setTimeout(()=>I(n,!0),types_1.LENS_REPLACEMENT_DRAIN_MS).unref?.()}return!0}catch(l){I(o,!0),!o&&a!==void 0&&N.delete(a),e.candidate=void 0;const w=await G(n);e.state=w?"ready":"failed",w&&(e.reason=void 0,e.startedAt=void 0);const c=l.message;return console.error(`[gateway] lens handoff failed: ${e.key}: ${c}`),p("warning",`lens handoff failed: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-failed",wsKey:e.wsKey,project:e.project,reason:r,generation:o?.generation??e.generation,port:o?.port??a,durationMs:Date.now()-i,oldKept:w,error:c},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),!1}finally{e.replacement=void 0}})();return e.replacement=t,t},"rollFrontend"),Q=d((e,r)=>{e.active=J(e,r,"active"),e.state="starting",e.startedAt=Date.now()},"spawnInitialFrontend"),ce=d((e,r)=>{const n=v.get(r.key);return!n||types_1.LENS_RESTART_MODE==="in-place"?!1:(n.candidate?.vite===e||n.active?.vite!==e||(n.state="starting",n.reason="child-exit",n.startedAt=Date.now(),console.warn(`[gateway] lens frontend exited: ${n.key} g${r.generation} :${r.port}; starting an alternate-port replacement.`),Y(n,"child-exit")),!0)},"handleManagedViteExit");s.lensSupervisor=types_1.LENS_RESTART_MODE==="rolling"?{status(e){const r=D.get(e);return r?{state:r.state,generation:r.candidate?.generation??r.active?.generation??r.generation,reason:r.reason,startedAt:r.startedAt}:void 0},recover(e,r){const n=D.get(e);return n?Y(n,r):Promise.resolve(!1)}}:void 0;let q=!1,Z=0;const de=d(()=>{q||Date.now()-Z<types_1.POD_STALE_SCAN_MS||(q=!0,Z=Date.now(),(async()=>{for(const e of[...v.values()]){if(s.stopped())return;const r=Date.now(),n=(0,scope_1.readSourceSetOptions)(e.workspaceRoot),t=await(0,lifecycle_1.sourceSetSnapshot)(e.workspaceRoot,e.sourceDirs,n.exclude,{ignoreTests:n.ignoreTests});if(v.get(e.key)!==e)continue;const o=e.sourceSnapshot,a=e.sourceScanStartedAt;if(e.sourceSnapshot=t,e.sourceScanStartedAt=r,!o)continue;const i=Date.now(),l=(0,lifecycle_1.diffSourceSets)(o,t,i,(0,lifecycle_1.sourceChangeWindowMs)(i,a,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS));l&&(e.sourceChange=l,p("info",`source file set changed for ${e.key}`,`cluster-lens-app:${e.project}`,{kind:"source-set-changed",scoped:!0,wsKey:e.wsKey,project:e.project,added:l.added,removed:l.removed,truncated:l.truncated},void 0,e.eventFile===s.eventFile?void 0:e.eventFile))}})().catch(()=>{}).finally(()=>{q=!1}))},"scanFrontendSourceSets");let U=!1;const pe=d(async(e,r)=>{if(!U){U=!0;try{for(const[n,t]of[...m]){if(s.stopped())return;if(t.port===void 0||n.exitCode!==null||n.signalCode!==null)continue;if(await probePortListening(t.port,types_1.LENS_PROBE_TIMEOUT_MS)){if(t.probeFails=0,!t.listening){t.listening=!0;const l=v.get(t.key);l?.active?.vite===n&&(l.state="ready",l.reason=void 0),p("info",`lens app up: ${t.project} (vite :${t.port} listening)`,`cluster-lens-app:${t.project}`,{kind:"service-up",project:t.project,port:t.port},void 0,t.bus)}continue}const o=t.listening?e.graceMs:Math.max(e.graceMs,e.neverListenedGraceMs);if(Date.now()-t.spawnedAt<o){t.probeFails=0;continue}if(!r)continue;t.probeFails+=1;const a=(0,lifecycle_1.lensChildWedged)({spawnedAt:t.spawnedAt,probeFails:t.probeFails,hasListened:t.listening},j.get(t.key)??0,Date.now(),e);if(!a)continue;const i=t.listening?"its listener VANISHED (was up, now refusing)":`it NEVER bound within the ${Math.round(o/1e3)}s boot allowance`;j.set(t.key,Date.now()),t.probeFails=0,t.listening=!1,console.error(`[gateway] wedge recycle: lens app ${t.project} is ALIVE but NOT LISTENING on :${t.port} after ${Math.max(2,e.failureThreshold)} probes \u2014 ${i} \u2014 SIGKILL; a fresh vite is coming.`),p("warning",`wedge recycle: ${t.project} (${a})`,`cluster-lens-app:${t.project}`,{kind:"pod-recycle",project:t.project,reason:a,port:t.port},void 0,t.bus);try{n.kill("SIGKILL")}catch{}}}finally{U=!1}}},"probeLensChildren"),ue=d(()=>{if(O||!types_1.POD_RECYCLE_ENABLED&&!types_1.LENS_PROBE_ENABLED)return;const e={graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};O=setInterval(()=>{if(s.stopped()||m.size===0||(ee(),!types_1.POD_RECYCLE_ENABLED))return;de();const r=Date.now();for(const[n,t]of m){const o=v.get(t.key),a=o?.sourceChange?.changedAt??0,i=j.get(t.key)??0;(0,lifecycle_1.lensFrontendStale)(t.spawnedAt,i,a,r,e)&&((B.get(t.key)??0)>=a||K.has(t.key)||(K.add(t.key),(async()=>{try{const l=o?.sourceChange,w=l&&!l.truncated?[...l.added,...l.removed].map(R=>path.resolve(t.workspaceRoot,R)):[],c=types_1.LENS_WATCHER_PROBE_ENABLED&&t.port!==void 0?(0,lifecycle_1.lensWatcherVerdict)(await M(t.port),a,l?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,w):"unknown";if(c==="heard"){B.set(t.key,a),console.log(`[gateway] stale source set: lens app ${t.project} watcher CONFIRMED the change \u2014 no recycle, HMR intact.`),p("info",`stale check: ${t.project} watcher confirmed \u2014 no recycle`,`cluster-lens-app:${t.project}`,{kind:"pod-stale-verified",project:t.project},void 0,t.bus);return}if(c==="unknown"&&types_1.LENS_WATCHER_UNVERIFIED==="keep"){console.log(`[gateway] stale source set: lens app ${t.project} unverifiable (no watcher testimony) \u2014 keeping it (LENSMCP_LENS_WATCHER_UNVERIFIED=keep).`);return}if(n.exitCode!==null||n.signalCode!==null)return;j.set(t.key,Date.now());const S=v.get(t.key);if(types_1.LENS_RESTART_MODE==="rolling"&&S){console.log(`[gateway] stale refresh: lens app ${t.key} (stale-source-set) \u2014 watcher ${c==="missed"?"missed the exact path change":"unverifiable"}; preparing a standby without dropping traffic.`),p("warning",`stale refresh: ${t.project} (stale-source-set)`,`cluster-lens-app:${t.project}`,{kind:"pod-refresh",wsKey:S.wsKey,project:t.project,reason:"stale-source-set",watcher:c,added:l?.added,removed:l?.removed},void 0,t.bus),await Y(S,"stale-source-set");return}t.listening=!1,console.log(`[gateway] zombie recycle: lens app ${t.project} (stale-source-set) \u2014 watcher ${c==="missed"?"SILENT across the change":"unverifiable"}; restarting it`),p("warning",`zombie recycle: ${t.project} (stale-source-set)`,`cluster-lens-app:${t.project}`,{kind:"pod-recycle",project:t.project,reason:"stale-source-set",watcher:c},void 0,t.bus);const C=(0,hmr_reload_1.broadcastFullReload)(s,t.key);C>0&&console.log(`[gateway] \u2192 told ${C} open tab(s) to reload (stale optimize-dep generation)`);try{n.kill("SIGTERM")}catch{}setTimeout(()=>{if(n.exitCode===null&&n.signalCode===null){console.log(`[gateway] zombie recycle: lens app ${t.project} ignored SIGTERM for ${Math.round(types_1.LENS_RECYCLE_KILL_GRACE_MS/1e3)}s \u2014 escalating to SIGKILL.`);try{n.kill("SIGKILL")}catch{}}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}finally{K.delete(t.key)}})()))}},Math.min(types_1.POD_STALE_SCAN_MS,15e3)),O.unref?.()},"startLensSweeper"),fe=d(async()=>{const e=[...new Map(s.routes.filter(t=>t.lens).map(t=>[t.project,t.lens])).values()],r=u.dashboard!==!1,n=u.mcp!==!1&&e.length>0;if(r){const t=(0,lens_frontend_1.findDashboardBundle)(s.root);if(!t)console.warn("[gateway] lens dashboard bundle not found \u2014 skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");else{const o=(0,scope_1.readLensScope)(s.root),a=await F(u.dashboardPort??o.dashboardPort,W),i=u.lensHost??"lensmcp.local",l=o.basePath;s.registry.appendRoutes(s.wsKey,[{host:i,prefix:l,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${a}`}]),s.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:o.key,root:s.root,base:l,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] starting lens dashboard \u2192 http://localhost:${a} (routed at https://${i}${l}/)`),p("info",`lens dashboard up: ${i}${l} \u2192 :${a}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:i,port:a,base:l}),T("lens-dashboard",process.execPath,[t],{LENSMCP_EVENT_FILE:s.eventFile,LENSMCP_PROCESS_ROLE:"dashboard",LENSMCP_DASHBOARD_PORT:String(a),LENSMCP_DASHBOARD_BASE:l},!1,void 0,void 0,a)}}if(n){const t=(0,lens_frontend_1.findMcpBundle)(s.root);if(!t)console.warn("[gateway] MCP server bundle not found \u2014 skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");else{const o=(0,scope_1.readLensScope)(s.root),a=Number(process.env.LENSMCP_PORT??o.mcpHttpPort),i=d(()=>{console.log(`[gateway] starting lens MCP server on 127.0.0.1:${a} (${t})`),T("lens-mcp",process.execPath,[t],{LENSMCP_EVENT_FILE:s.eventFile,LENSMCP_PROCESS_ROLE:"mcp",LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:String(a),LENSMCP_WORKSPACE_KEY:o.key,LENSMCP_WORKSPACE_ROOT:s.root},!1,void 0,void 0,a)},"spawnMcp");await E(a)?i():(console.warn(`[gateway] lens MCP port :${a} is already served \u2014 an external lensmcp server (a shared daemon?) owns this workspace's MCP; not spawning a second. Re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s; will take over if it frees.`),p("info",`lens MCP served externally on :${a} \u2014 gateway parked its own`,"cluster-lens-child:lens-mcp",{kind:"lens-child-up",project:"lens-mcp",label:"lens-mcp",port:a,external:!0}),z("lens-mcp",a,i))}}for(const t of e){if(t.started)continue;t.started=!0;const o=t.port,a=(s.registry.get(s.wsKey)?.routes??s.routes).filter(w=>w.lens?.project===t.project),i=await F(t.port,N);for(const w of a)w.lens&&(w.lens.port=i),w.target=`http://localhost:${i}`;t.port=i;const l=X({wsKey:s.wsKey,project:t.project,workspaceRoot:s.root,projectRoot:t.projectRoot,eventFile:s.eventFile,routes:a,frontend:t,desiredPort:o});try{Q(l,i),p("info",`lens app started: ${t.project} (vite :${t.port})`,`cluster-lens-app:${t.project}`,{kind:"lens-app-up",project:t.project,port:t.port})}catch(w){N.delete(i),l.state="failed",console.error(`[gateway] lens app ${t.project}: ${w.message}`)}}ue()},"bootSingletonsAndApps"),Ee=d(async e=>{const r=(0,lens_frontend_1.findDashboardBundle)(s.root);if(!r)return;const n=(0,scope_1.readLensScope)(e.root),t=u.lensHost??"lensmcp.local",o=n.basePath,a=await F(n.dashboardPort,W),i=`lens-dashboard:${e.wsKey}`,l=path.join(e.root,".lensmcp","events.jsonl");return $.delete(i),s.registry.appendRoutes(e.wsKey,[{host:t,prefix:o,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${a}`}]),(0,workspace_registry_1.registerWorkspace)({key:e.wsKey,root:e.root,base:o,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] hosting dashboard for workspace '${e.wsKey}' \u2192 http://localhost:${a} (routed at https://${t}${o}/)`),p("info",`lens dashboard up: ${t}${o} \u2192 :${a}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:t,port:a,base:o}),T(i,process.execPath,[r],{LENSMCP_EVENT_FILE:l,LENSMCP_PROCESS_ROLE:"dashboard",LENSMCP_DASHBOARD_PORT:String(a),LENSMCP_DASHBOARD_BASE:o,LENSMCP_WS_KEY:e.wsKey},!1,void 0,e.root,a),()=>{$.add(i),W.delete(a),(0,workspace_registry_1.unregisterWorkspace)(e.wsKey);try{A.get(i)?.kill("SIGTERM")}catch{}A.delete(i)}},"hostWorkspaceDashboard"),he=d(async e=>{const r=new Map;for(const o of e.routes){if(!o.lens)continue;const a=r.get(o.lens.project)??[];a.push(o),r.set(o.lens.project,a)}if(r.size===0)return()=>{};const n=path.join(e.root,".lensmcp","events.jsonl"),t=[];for(const[o,a]of r){const i=a[0].lens;if(i.started)continue;const l=await F(i.port,N),w=i.port;for(const S of a)S.lens.started=!0,S.lens.port=l,S.target=`http://localhost:${l}`;const c=X({wsKey:e.wsKey,project:o,workspaceRoot:e.root,projectRoot:i.projectRoot,eventFile:n,routes:a,frontend:i,desiredPort:w});t.push(c),console.log(`[gateway] hosting lens app '${o}' of '${e.wsKey}' \u2192 vite :${l}`);try{Q(c,l),p("info",`lens app started: ${e.wsKey}/${o} (vite :${l})`,`cluster-lens-app:${e.wsKey}:${o}`,{kind:"lens-app-up",project:o,port:l},void 0,n)}catch(S){N.delete(l),c.state="failed",console.error(`[gateway] lens app ${e.wsKey}/${o}: ${S.message}`)}}return()=>{for(const o of t){o.disposed=!0;for(const a of[...o.generations])I(a,!0);for(const a of o.routes)a.lens&&D.delete(a.lens);D.delete(o.frontend),j.delete(o.key),B.delete(o.key),K.delete(o.key),v.delete(o.key)}}},"hostWorkspaceApps"),ge=d(async(e=2500)=>{O&&(clearInterval(O),O=void 0);const r=[...y];for(const o of r)try{o.kill("SIGTERM")}catch{}const n=d(o=>o.exitCode===null&&o.signalCode===null,"alive"),t=Date.now()+e;for(;Date.now()<t&&r.some(n);)await new Promise(o=>setTimeout(o,100));for(const o of r)if(n(o))try{o.kill("SIGKILL")}catch{}},"reapAll"),ee=d(()=>pe({graceMs:types_1.LENS_PROBE_GRACE_MS,failureThreshold:types_1.LENS_PROBE_FAILS,cooldownMs:types_1.LENS_PROBE_COOLDOWN_MS,neverListenedGraceMs:types_1.LENS_PROBE_BOOT_GRACE_MS},types_1.LENS_PROBE_ENABLED),"probeLensFrontends");return{spawnManagedChild:T,bootSingletonsAndApps:fe,hostWorkspaceDashboard:Ee,hostWorkspaceApps:he,probeLensFrontends:ee,reapAll:ge}}P(createLensChildren,"createLensChildren"),d(createLensChildren,"createLensChildren");
1
+ "use strict";var Re=Object.defineProperty;var P=(s,f)=>Re(s,"name",{value:f,configurable:!0});var Le=Object.defineProperty,d=P((s,f)=>Le(s,"name",{value:f,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.probeWatcherHealth=probeWatcherHealth,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.claimPort=claimPort,exports.createLensChildren=createLensChildren;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_child_process_1=require("node:child_process"),lens_frontend_1=require("@lensmcp/nx-plugin/lens-frontend"),hmr_reload_1=require("./hmr-reload"),pod_env_1=require("./pod-env"),scope_1=require("./scope"),workspace_registry_1=require("./workspace-registry"),lifecycle_1=require("./lifecycle"),types_1=require("./types"),observability_1=require("./observability"),HEAL_BASE_MS=1500,HEAL_MAX_MS=3e4,HEAL_HEALTHY_MS=2e4;function tryBind(s,f){return new Promise(u=>{const _=net.createServer();_.unref(),_.once("error",p=>{const E=p.code??"";u(E==="EADDRINUSE"||E==="EACCES"?"in-use":"unavailable")}),_.listen(f===void 0?{port:s}:{port:s,host:f},()=>_.close(()=>u("free")))})}P(tryBind,"tryBind"),d(tryBind,"tryBind");async function probePortFree(s){for(const f of[void 0,"0.0.0.0","127.0.0.1","::1"])if(await tryBind(s,f)==="in-use")return!1;return!0}P(probePortFree,"probePortFree"),d(probePortFree,"probePortFree");function probeConnect(s,f,u){return new Promise(_=>{let p=!1;const E=net.createConnection({host:s,port:f});E.unref();const C=setTimeout(()=>y(!1),u);C.unref?.();function y(k){p||(p=!0,clearTimeout(C),E.destroy(),_(k))}P(y,"y"),d(y,"finish"),E.once("connect",()=>y(!0)),E.once("error",()=>y(!1))})}P(probeConnect,"probeConnect"),d(probeConnect,"probeConnect");async function probePortListening(s,f=1e3){return await probeConnect("127.0.0.1",s,f)?!0:probeConnect("::1",s,f)}P(probePortListening,"probePortListening"),d(probePortListening,"probePortListening");async function probeWatcherHealth(s,f=1500){try{const u=new AbortController,_=setTimeout(()=>u.abort(),f);_.unref?.();try{const p=await fetch(`http://127.0.0.1:${s}/__lensmcp/watcher-health`,{signal:u.signal});if(!p.ok)return null;const E=await p.json();if(typeof E?.lastFsSetEventAt!="number")return null;const C=Array.isArray(E.recentEvents)?E.recentEvents.filter(y=>y&&typeof y.path=="string"&&typeof y.at=="number"&&typeof y.sequence=="number"&&["add","addDir","unlink","unlinkDir"].includes(y.kind)):[];return{schemaVersion:E.schemaVersion===2?2:1,ready:E.ready!==!1,sequence:typeof E.sequence=="number"?E.sequence:0,lastFsSetEventAt:E.lastFsSetEventAt,fsSetEvents:typeof E.fsSetEvents=="number"?E.fsSetEvents:0,recentEvents:C}}finally{clearTimeout(_)}}catch{return null}}P(probeWatcherHealth,"probeWatcherHealth"),d(probeWatcherHealth,"probeWatcherHealth");const sleepUnref=d(s=>new Promise(f=>{setTimeout(f,s).unref?.()}),"sleepUnref");async function waitForPortFree(s,f,u={}){const _=u.isPortFree??probePortFree,p=Math.max(1,u.intervalMs??types_1.PORT_FREE_POLL_MS),E=Date.now()+Math.max(0,f);for(;;){if(await _(s))return!0;const C=E-Date.now();if(C<=0)return!1;await sleepUnref(Math.min(p,C))}}P(waitForPortFree,"waitForPortFree"),d(waitForPortFree,"waitForPortFree");function viteArgPort(s){const f=d(u=>Number.isInteger(u)&&u>0&&u<65536?u:void 0,"valid");for(let u=0;u<s.length;u+=1){const _=s[u];if(_==="--port")return f(Number(s[u+1]));if(_.startsWith("--port="))return f(Number(_.slice(7)))}}P(viteArgPort,"viteArgPort"),d(viteArgPort,"viteArgPort");async function claimPort(s,f,u,_){let p=s;for(;f.has(p)||_?.has(p)||!await u(p);)p+=1;return f.add(p),p}P(claimPort,"claimPort"),d(claimPort,"claimPort");function createLensChildren(s,f,u,_){const{emit:p}=f,E=_?.isPortFree??probePortFree,C=_?.probeWatcherHealth??probeWatcherHealth,y=[],k=new Map,A=new Map,j=new Map,W=new Map,K=new Set;let O;const N=new Map,$=new Set,X=new Set,H=new Set,b=new Set,v=new Map,D=new WeakMap,x=new Set,J=d((e,r,n)=>{if(x.has(e))return;x.add(e);const t=setInterval(()=>{(async()=>{if(s.stopped()||$.has(e)){clearInterval(t),x.delete(e);return}await E(r)&&(clearInterval(t),x.delete(e),console.log(`[gateway] lens child ${e}: :${r} freed \u2014 respawning.`),n())})().catch(()=>{})},types_1.LENS_PARKED_PORT_REPROBE_MS);t.unref?.()},"parkUntilPortFree"),F=d(async(e,r,n)=>{const t=await claimPort(e,r,E,n?.reserved);return t!==e&&(n?.heldBy?console.log(`[gateway] handoff: :${e} is held by ${n.heldBy} \u2014 replacement on :${t}.`):console.warn(`[gateway] port ${e} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${t} instead.`)),t},"claimFreePort"),le=d((e,r,n)=>{const t=n?.LENSMCP_FRONTEND_KEY,o=Number(n?.LENSMCP_FRONTEND_GENERATION);if(!t||!Number.isFinite(o))return;const a=[...v.get(t)?.generations??[]].find(i=>i.generation===o);a&&(e.endsWith(":vite")?a.vite=r:e.endsWith(":browser-capture")&&(a.capture=r))},"attachRespawnedGenerationChild"),B=d(e=>{if(!(e.exitCode!==null||e.signalCode!==null)){X.add(e);try{e.kill("SIGTERM")}catch{}setTimeout(()=>{if(e.exitCode===null&&e.signalCode===null)try{e.kill("SIGKILL")}catch{}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}},"retireChild"),ce=d((e,r)=>{const n=N.get(e);!n||n===r||n.exitCode!==null||n.signalCode!==null||(console.warn(`[gateway] lens child ${e}: a replacement took the label \u2014 retiring the previous child (pid ${n.pid??"?"}).`),B(n))},"retireLabelHolder"),T=d((e,r,n,t,o=!1,a,i,l)=>{const g=Date.now(),c=(0,node_child_process_1.spawn)(r,n,{cwd:i??s.root,env:{...(0,pod_env_1.podBaseEnv)(process.env,s.root,i??s.root),...t??{},LENSMCP_PARENT_PID:String(process.pid)},stdio:"inherit"});y.push(c),ce(e,c),N.set(e,c),le(e,c,t);const S=e==="vite"||e.endsWith(":vite"),M=typeof t?.LENSMCP_EVENT_FILE=="string"?t.LENSMCP_EVENT_FILE:void 0,L=M&&M!==s.eventFile?M:void 0,w=S?viteArgPort(n):l;S&&A.set(c,{key:t?.LENSMCP_FRONTEND_KEY??`${i??s.root}:${t?.LENSMCP_PROJECT??"app"}`,project:t?.LENSMCP_PROJECT??"app",workspaceRoot:i??s.root,spawnedAt:g,port:w,probeFails:0,listening:!1,generation:Number(t?.LENSMCP_FRONTEND_GENERATION??0),role:t?.LENSMCP_FRONTEND_ROLE==="replacement"?"replacement":"active",...L?{bus:L}:{}});const h=e==="lens-mcp"?"lens-mcp":e==="lens-dashboard"||e.startsWith("lens-dashboard:")?"lens-dashboard":void 0;return h&&w!==void 0&&(p("info",`lens child starting: ${h} (:${w})`,`cluster-lens-child:${h}`,{kind:"lens-child-starting",project:h,label:e,pid:c.pid,port:w},void 0,L),(async()=>{const m=Date.now()+types_1.LENS_REPLACEMENT_READY_MS;for(;!s.stopped()&&Date.now()<m;){if(c.exitCode!==null||c.signalCode!==null||$.has(e))return;if(await probePortListening(w,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS))){p("info",`lens child up: ${h} (:${w})`,`cluster-lens-child:${h}`,{kind:"lens-child-up",project:h,label:e,pid:c.pid,port:w},void 0,L);return}await sleepUnref(150)}})().catch(()=>{})),c.on("exit",(m,z)=>{const ne=y.indexOf(c);ne>=0&&y.splice(ne,1),N.get(e)===c&&N.delete(e);const re=A.get(c);if(A.delete(c),X.delete(c))return;const ye=$.delete(e);if(s.stopped()||ye||(h&&p("warning",`lens child exited: ${h}`,`cluster-lens-child:${h}`,{kind:"lens-child-exit",project:h,label:e,pid:c.pid,code:m,signal:z},void 0,L),re&&ue(c,re)))return;const se=Date.now()-g,R=k.get(e)??{attempts:0,portConflicts:0};(async()=>{const ve=w!==void 0&&!await E(w),ae=(0,lifecycle_1.isPortConflictExit)({code:m,signal:z,ranMs:se,portHeld:ve},{fastFailMs:types_1.CHILD_START_FAIL_MS});if(ae&&R.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)R.portConflicts+=1,k.set(e,R),console.warn(`[gateway] lens child ${e} lost the :${w} bind race (EADDRINUSE, code=${m}); waiting for the port, then retrying (port-retry ${R.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else if(ae){R.portConflicts=0,k.set(e,R),console.warn(`[gateway] lens child ${e}: :${w} persistently held after ${types_1.MAX_PORT_CONFLICT_RETRIES} bind races \u2014 another server owns it (an external/shared daemon?). Parking; re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s, will respawn if it frees.`),p("warning",`lens child parked: ${e} (:${w} held externally)`,`cluster-lens-child:${e}`,{kind:"lens-child-parked",label:e,port:w}),J(e,w,()=>T(e,r,n,t,o,a,i,l));return}else{R.portConflicts=0,se>=HEAL_HEALTHY_MS&&(R.attempts=0);const ie=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**R.attempts);R.attempts+=1,k.set(e,R),console.warn(`[gateway] lens child ${e} exited (code=${m} sig=${z}); auto-healing in ${Math.round(ie/1e3)}s (attempt ${R.attempts}).`),await sleepUnref(ie)}s.stopped()||$.has(e)||(w!==void 0&&!await waitForPortFree(w,types_1.PORT_FREE_WAIT_MS,{isPortFree:E,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${e}: :${w} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(s.stopped()||$.has(e))&&T(e,r,n,t,o,a,i,l))})().catch(()=>{})}),c},"spawnManagedChild"),Q=d(e=>{const r=`${e.wsKey}:${e.project}`,n=v.get(r);if(n)return n;const t={key:r,...e,sourceDirs:e.frontend.sourceRoots?.length?e.frontend.sourceRoots:[e.projectRoot],generation:0,state:"starting",generations:new Set,disposed:!1};v.set(r,t);for(const o of e.routes)o.lens&&(o.lens.workspaceKey??=e.wsKey,D.set(o.lens,t));return e.frontend.workspaceKey??=e.wsKey,D.set(e.frontend,t),t},"registerFrontend"),de=d(e=>{if(!types_1.LENS_WATCH_SCOPE_ENABLED)return{};const r=path.resolve(e.projectRoot),n=e.sourceDirs.map(t=>path.resolve(t)).filter(t=>t!==r);return n.length>0?{LENSMCP_WATCH_DIRS:n.join(path.delimiter)}:{}},"lensWatchScopeEnv"),Z=d((e,r,n)=>{const t=++e.generation,o=`${e.key}:g${t}:`,a=`${o}vite`,i=`${o}browser-capture`;$.delete(a),$.delete(i);const l=d((S,M,L,w,h,m)=>T(S,M,L,w,h,m,e.workspaceRoot),"spawnInRoot"),g=(0,lens_frontend_1.spawnLensFrontend)({projectRoot:e.projectRoot,workspaceRoot:e.workspaceRoot,project:e.project,eventFile:e.eventFile,port:r,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:e.project,LENSMCP_FRONTEND_KEY:e.key,LENSMCP_FRONTEND_GENERATION:String(t),LENSMCP_FRONTEND_ROLE:n,...de(e),NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false"},labelPrefix:o,log:d(S=>console.log(S),"log")},l),c={ownerKey:e.key,generation:t,port:r,labelPrefix:o,vite:g?.vite??N.get(a),capture:N.get(i)};return e.generations.add(c),c},"spawnFrontendGeneration"),I=d((e,r)=>{if(!e)return;v.get(e.ownerKey)?.generations.delete(e);const n=[[`${e.labelPrefix}browser-capture`,e.capture],[`${e.labelPrefix}vite`,e.vite]];for(const[t,o]of n)$.add(t),o&&B(o);r&&b.delete(e.port)},"terminateGeneration"),pe=d(async e=>{const r=Date.now()+types_1.LENS_REPLACEMENT_READY_MS;for(;!s.stopped()&&Date.now()<r;){const n=e.vite;if(!n||n.exitCode!==null||n.signalCode!==null)return!1;if(await probePortListening(e.port,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS))){const t=await C(e.port);if(!t||t.ready)return!0}await sleepUnref(150)}return!1},"waitForGenerationReady"),G=d(async e=>{const r=e?.vite;return!!(e&&r&&r.exitCode===null&&r.signalCode===null&&await probePortListening(e.port,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS)))},"generationIsServing"),Y=d(e=>!e.disposed&&!s.stopped()&&v.get(e.key)===e,"frontendIsCurrent"),q=d((e,r)=>{if(!Y(e))return Promise.resolve(!1);if(e.replacement)return e.replacement;const n=e.active;e.state=n?"replacing":"starting",e.reason=r,e.startedAt=Date.now();const t=(async()=>{let o,a;const i=Date.now();try{const l=new Set;for(const h of v.values())h!==e&&l.add(h.desiredPort);const g=n&&n.port===e.desiredPort?`g${n.generation}`:void 0,c=await F(e.desiredPort,b,{reserved:l,heldBy:g});if(a=c,!Y(e))return b.delete(c),!1;o=Z(e,c,"replacement"),e.candidate=o;const S=await G(n);console.log(`[gateway] lens handoff start: ${e.key} g${n?.generation??0}\u2192g${o.generation} :${n?.port??"down"}\u2192:${c} reason=${r}`),p("info",`lens handoff starting: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-start",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,oldPort:n?.port,newPort:c,oldServing:S},void 0,e.eventFile===s.eventFile?void 0:e.eventFile);const M=await pe(o);if(!Y(e))return I(o,!0),e.candidate=void 0,!1;if(!M){I(o,!0),e.candidate=void 0;const h=await G(n);return e.state=h?"ready":"failed",h&&(e.reason=void 0,e.startedAt=void 0),console.error(`[gateway] lens handoff failed: ${e.key} g${o.generation} did not become ready within ${types_1.LENS_REPLACEMENT_READY_MS}ms; ${e.state==="ready"?"old generation kept serving":"no active generation"}.`),p("warning",`lens handoff failed: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-failed",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,port:c,durationMs:Date.now()-i,oldKept:h},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),!1}for(const h of e.routes)h.target=`http://localhost:${o.port}`,h.lens&&(h.lens.port=o.port);e.frontend.port=o.port,e.active=o,e.candidate=void 0,e.state="ready",e.reason=void 0,e.startedAt=void 0;const L=o.vite?A.get(o.vite):void 0;L&&(L.role="active",L.listening=!0);const w=(0,hmr_reload_1.broadcastFullReload)(s,e.key);if(console.log(`[gateway] lens handoff ready: ${e.key} g${o.generation} :${o.port} in ${Date.now()-i}ms; draining g${n?.generation??0}${w>0?`; reloading ${w} tab(s)`:""}.`),p("info",`lens handoff complete: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-complete",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,oldPort:n?.port,newPort:o.port,durationMs:Date.now()-i,reloadedTabs:w},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),n){const h=`${n.labelPrefix}browser-capture`;$.add(h),n.capture&&B(n.capture),setTimeout(()=>I(n,!0),types_1.LENS_REPLACEMENT_DRAIN_MS).unref?.()}return!0}catch(l){I(o,!0),!o&&a!==void 0&&b.delete(a),e.candidate=void 0;const g=await G(n);e.state=g?"ready":"failed",g&&(e.reason=void 0,e.startedAt=void 0);const c=l.message;return console.error(`[gateway] lens handoff failed: ${e.key}: ${c}`),p("warning",`lens handoff failed: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-failed",wsKey:e.wsKey,project:e.project,reason:r,generation:o?.generation??e.generation,port:o?.port??a,durationMs:Date.now()-i,oldKept:g,error:c},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),!1}finally{e.replacement=void 0}})();return e.replacement=t,t},"rollFrontend"),ee=d((e,r)=>{e.active=Z(e,r,"active"),e.state="starting",e.startedAt=Date.now()},"spawnInitialFrontend"),ue=d((e,r)=>{const n=v.get(r.key);return!n||types_1.LENS_RESTART_MODE==="in-place"?!1:(n.candidate?.vite===e||n.active?.vite!==e||(n.state="starting",n.reason="child-exit",n.startedAt=Date.now(),console.warn(`[gateway] lens frontend exited: ${n.key} g${r.generation} :${r.port}; starting an alternate-port replacement.`),q(n,"child-exit")),!0)},"handleManagedViteExit");s.lensSupervisor=types_1.LENS_RESTART_MODE==="rolling"?{status(e){const r=D.get(e);return r?{state:r.state,generation:r.candidate?.generation??r.active?.generation??r.generation,reason:r.reason,startedAt:r.startedAt}:void 0},recover(e,r){const n=D.get(e);return n?q(n,r):Promise.resolve(!1)}}:void 0;let V=!1,te=0;const fe=d(()=>{V||Date.now()-te<types_1.POD_STALE_SCAN_MS||(V=!0,te=Date.now(),(async()=>{for(const e of[...v.values()]){if(s.stopped())return;const r=Date.now(),n=(0,scope_1.readSourceSetOptions)(e.workspaceRoot),t=await(0,lifecycle_1.sourceSetSnapshot)(e.workspaceRoot,e.sourceDirs,n.exclude,{ignoreTests:n.ignoreTests});if(v.get(e.key)!==e)continue;const o=e.sourceSnapshot,a=e.sourceScanStartedAt;if(e.sourceSnapshot=t,e.sourceScanStartedAt=r,!o)continue;const i=Date.now(),l=(0,lifecycle_1.diffSourceSets)(o,t,i,(0,lifecycle_1.sourceChangeWindowMs)(i,a,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS));l&&(e.sourceChange=l,p("info",`source file set changed for ${e.key}`,`cluster-lens-app:${e.project}`,{kind:"source-set-changed",scoped:!0,wsKey:e.wsKey,project:e.project,added:l.added,removed:l.removed,truncated:l.truncated},void 0,e.eventFile===s.eventFile?void 0:e.eventFile))}})().catch(()=>{}).finally(()=>{V=!1}))},"scanFrontendSourceSets");let U=!1;const Ee=d(async(e,r)=>{if(!U){U=!0;try{for(const[n,t]of[...A]){if(s.stopped())return;if(t.port===void 0||n.exitCode!==null||n.signalCode!==null)continue;if(await probePortListening(t.port,types_1.LENS_PROBE_TIMEOUT_MS)){if(t.probeFails=0,!t.listening){t.listening=!0;const l=v.get(t.key);l?.active?.vite===n&&(l.state="ready",l.reason=void 0),p("info",`lens app up: ${t.project} (vite :${t.port} listening)`,`cluster-lens-app:${t.project}`,{kind:"service-up",project:t.project,port:t.port},void 0,t.bus)}continue}const o=t.listening?e.graceMs:Math.max(e.graceMs,e.neverListenedGraceMs);if(Date.now()-t.spawnedAt<o){t.probeFails=0;continue}if(!r)continue;t.probeFails+=1;const a=(0,lifecycle_1.lensChildWedged)({spawnedAt:t.spawnedAt,probeFails:t.probeFails,hasListened:t.listening},j.get(t.key)??0,Date.now(),e);if(!a)continue;const i=t.listening?"its listener VANISHED (was up, now refusing)":`it NEVER bound within the ${Math.round(o/1e3)}s boot allowance`;j.set(t.key,Date.now()),t.probeFails=0,t.listening=!1,console.error(`[gateway] wedge recycle: lens app ${t.project} is ALIVE but NOT LISTENING on :${t.port} after ${Math.max(2,e.failureThreshold)} probes \u2014 ${i} \u2014 SIGKILL; a fresh vite is coming.`),p("warning",`wedge recycle: ${t.project} (${a})`,`cluster-lens-app:${t.project}`,{kind:"pod-recycle",project:t.project,reason:a,port:t.port},void 0,t.bus);try{n.kill("SIGKILL")}catch{}}}finally{U=!1}}},"probeLensChildren"),he=d(()=>{if(O||!types_1.POD_RECYCLE_ENABLED&&!types_1.LENS_PROBE_ENABLED)return;const e={graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};O=setInterval(()=>{if(s.stopped()||A.size===0||(oe(),!types_1.POD_RECYCLE_ENABLED))return;fe();const r=Date.now();for(const[n,t]of A){const o=v.get(t.key),a=o?.sourceChange?.changedAt??0,i=j.get(t.key)??0;(0,lifecycle_1.lensFrontendStale)(t.spawnedAt,i,a,r,e)&&((W.get(t.key)??0)>=a||K.has(t.key)||(K.add(t.key),(async()=>{try{const l=o?.sourceChange,g=l&&!l.truncated?[...l.added,...l.removed].map(L=>path.resolve(t.workspaceRoot,L)):[],c=types_1.LENS_WATCHER_PROBE_ENABLED&&t.port!==void 0?(0,lifecycle_1.lensWatcherVerdict)(await C(t.port),a,l?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,g):"unknown";if(c==="heard"){W.set(t.key,a),console.log(`[gateway] stale source set: lens app ${t.project} watcher CONFIRMED the change \u2014 no recycle, HMR intact.`),p("info",`stale check: ${t.project} watcher confirmed \u2014 no recycle`,`cluster-lens-app:${t.project}`,{kind:"pod-stale-verified",project:t.project},void 0,t.bus);return}if(c==="unknown"&&types_1.LENS_WATCHER_UNVERIFIED==="keep"){console.log(`[gateway] stale source set: lens app ${t.project} unverifiable (no watcher testimony) \u2014 keeping it (LENSMCP_LENS_WATCHER_UNVERIFIED=keep).`);return}if(n.exitCode!==null||n.signalCode!==null)return;j.set(t.key,Date.now());const S=v.get(t.key);if(types_1.LENS_RESTART_MODE==="rolling"&&S){console.log(`[gateway] stale refresh: lens app ${t.key} (stale-source-set) \u2014 watcher ${c==="missed"?"missed the exact path change":"unverifiable"}; preparing a standby without dropping traffic.`),p("warning",`stale refresh: ${t.project} (stale-source-set)`,`cluster-lens-app:${t.project}`,{kind:"pod-refresh",wsKey:S.wsKey,project:t.project,reason:"stale-source-set",watcher:c,added:l?.added,removed:l?.removed},void 0,t.bus),await q(S,"stale-source-set");return}t.listening=!1,console.log(`[gateway] zombie recycle: lens app ${t.project} (stale-source-set) \u2014 watcher ${c==="missed"?"SILENT across the change":"unverifiable"}; restarting it`),p("warning",`zombie recycle: ${t.project} (stale-source-set)`,`cluster-lens-app:${t.project}`,{kind:"pod-recycle",project:t.project,reason:"stale-source-set",watcher:c},void 0,t.bus);const M=(0,hmr_reload_1.broadcastFullReload)(s,t.key);M>0&&console.log(`[gateway] \u2192 told ${M} open tab(s) to reload (stale optimize-dep generation)`);try{n.kill("SIGTERM")}catch{}setTimeout(()=>{if(n.exitCode===null&&n.signalCode===null){console.log(`[gateway] zombie recycle: lens app ${t.project} ignored SIGTERM for ${Math.round(types_1.LENS_RECYCLE_KILL_GRACE_MS/1e3)}s \u2014 escalating to SIGKILL.`);try{n.kill("SIGKILL")}catch{}}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}finally{K.delete(t.key)}})()))}},Math.min(types_1.POD_STALE_SCAN_MS,15e3)),O.unref?.()},"startLensSweeper"),ge=d(async()=>{const e=[...new Map(s.routes.filter(t=>t.lens).map(t=>[t.project,t.lens])).values()],r=u.dashboard!==!1,n=u.mcp!==!1&&e.length>0;if(r){const t=(0,lens_frontend_1.findDashboardBundle)(s.root);if(!t)console.warn("[gateway] lens dashboard bundle not found \u2014 skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");else{const o=(0,scope_1.readLensScope)(s.root),a=await F(u.dashboardPort??o.dashboardPort,H),i=u.lensHost??"lensmcp.local",l=o.basePath;s.registry.appendRoutes(s.wsKey,[{host:i,prefix:l,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${a}`}]),s.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:o.key,root:s.root,base:l,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] starting lens dashboard \u2192 http://localhost:${a} (routed at https://${i}${l}/)`),p("info",`lens dashboard up: ${i}${l} \u2192 :${a}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:i,port:a,base:l}),T("lens-dashboard",process.execPath,[t],{LENSMCP_EVENT_FILE:s.eventFile,LENSMCP_PROCESS_ROLE:"dashboard",LENSMCP_DASHBOARD_PORT:String(a),LENSMCP_DASHBOARD_BASE:l},!1,void 0,void 0,a)}}if(n){const t=(0,lens_frontend_1.findMcpBundle)(s.root);if(!t)console.warn("[gateway] MCP server bundle not found \u2014 skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");else{const o=(0,scope_1.readLensScope)(s.root),a=Number(process.env.LENSMCP_PORT??o.mcpHttpPort),i=d(()=>{console.log(`[gateway] starting lens MCP server on 127.0.0.1:${a} (${t})`),T("lens-mcp",process.execPath,[t],{LENSMCP_EVENT_FILE:s.eventFile,LENSMCP_PROCESS_ROLE:"mcp",LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:String(a),LENSMCP_WORKSPACE_KEY:o.key,LENSMCP_WORKSPACE_ROOT:s.root},!1,void 0,void 0,a)},"spawnMcp");await E(a)?i():(console.warn(`[gateway] lens MCP port :${a} is already served \u2014 an external lensmcp server (a shared daemon?) owns this workspace's MCP; not spawning a second. Re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s; will take over if it frees.`),p("info",`lens MCP served externally on :${a} \u2014 gateway parked its own`,"cluster-lens-child:lens-mcp",{kind:"lens-child-up",project:"lens-mcp",label:"lens-mcp",port:a,external:!0}),J("lens-mcp",a,i))}}for(const t of e){if(t.started)continue;t.started=!0;const o=t.port,a=(s.registry.get(s.wsKey)?.routes??s.routes).filter(g=>g.lens?.project===t.project),i=await F(t.port,b);for(const g of a)g.lens&&(g.lens.port=i),g.target=`http://localhost:${i}`;t.port=i;const l=Q({wsKey:s.wsKey,project:t.project,workspaceRoot:s.root,projectRoot:t.projectRoot,eventFile:s.eventFile,routes:a,frontend:t,desiredPort:o});try{ee(l,i),p("info",`lens app started: ${t.project} (vite :${t.port})`,`cluster-lens-app:${t.project}`,{kind:"lens-app-up",project:t.project,port:t.port})}catch(g){b.delete(i),l.state="failed",console.error(`[gateway] lens app ${t.project}: ${g.message}`)}}he()},"bootSingletonsAndApps"),we=d(async e=>{if(u.dashboard===!1)return;const r=(0,lens_frontend_1.findDashboardBundle)(s.root);if(!r)return;const n=(0,scope_1.readLensScope)(e.root),t=u.lensHost??"lensmcp.local",o=n.basePath,a=await F(n.dashboardPort,H),i=`lens-dashboard:${e.wsKey}`,l=path.join(e.root,".lensmcp","events.jsonl");return $.delete(i),s.registry.appendRoutes(e.wsKey,[{host:t,prefix:o,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${a}`}]),(0,workspace_registry_1.registerWorkspace)({key:e.wsKey,root:e.root,base:o,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] hosting dashboard for workspace '${e.wsKey}' \u2192 http://localhost:${a} (routed at https://${t}${o}/)`),p("info",`lens dashboard up: ${t}${o} \u2192 :${a}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:t,port:a,base:o}),T(i,process.execPath,[r],{LENSMCP_EVENT_FILE:l,LENSMCP_PROCESS_ROLE:"dashboard",LENSMCP_DASHBOARD_PORT:String(a),LENSMCP_DASHBOARD_BASE:o,LENSMCP_WS_KEY:e.wsKey},!1,void 0,e.root,a),()=>{$.add(i),H.delete(a),(0,workspace_registry_1.unregisterWorkspace)(e.wsKey);const g=N.get(i);g&&B(g),N.delete(i)}},"hostWorkspaceDashboard"),_e=d(async e=>{const r=new Map;for(const o of e.routes){if(!o.lens)continue;const a=r.get(o.lens.project)??[];a.push(o),r.set(o.lens.project,a)}if(r.size===0)return()=>{};const n=path.join(e.root,".lensmcp","events.jsonl"),t=[];for(const[o,a]of r){const i=a[0].lens;if(i.started)continue;const l=await F(i.port,b),g=i.port;for(const S of a)S.lens.started=!0,S.lens.port=l,S.target=`http://localhost:${l}`;const c=Q({wsKey:e.wsKey,project:o,workspaceRoot:e.root,projectRoot:i.projectRoot,eventFile:n,routes:a,frontend:i,desiredPort:g});t.push(c),console.log(`[gateway] hosting lens app '${o}' of '${e.wsKey}' \u2192 vite :${l}`);try{ee(c,l),p("info",`lens app started: ${e.wsKey}/${o} (vite :${l})`,`cluster-lens-app:${e.wsKey}:${o}`,{kind:"lens-app-up",project:o,port:l},void 0,n)}catch(S){b.delete(l),c.state="failed",console.error(`[gateway] lens app ${e.wsKey}/${o}: ${S.message}`)}}return()=>{for(const o of t){o.disposed=!0;for(const a of[...o.generations])I(a,!0);for(const a of o.routes)a.lens&&D.delete(a.lens);D.delete(o.frontend),j.delete(o.key),W.delete(o.key),K.delete(o.key),v.delete(o.key)}}},"hostWorkspaceApps"),Se=d(async(e=2500)=>{O&&(clearInterval(O),O=void 0);const r=[...y];for(const o of r)try{o.kill("SIGTERM")}catch{}const n=d(o=>o.exitCode===null&&o.signalCode===null,"alive"),t=Date.now()+e;for(;Date.now()<t&&r.some(n);)await new Promise(o=>setTimeout(o,100));for(const o of r)if(n(o))try{o.kill("SIGKILL")}catch{}},"reapAll"),oe=d(()=>Ee({graceMs:types_1.LENS_PROBE_GRACE_MS,failureThreshold:types_1.LENS_PROBE_FAILS,cooldownMs:types_1.LENS_PROBE_COOLDOWN_MS,neverListenedGraceMs:types_1.LENS_PROBE_BOOT_GRACE_MS},types_1.LENS_PROBE_ENABLED),"probeLensFrontends");return{spawnManagedChild:T,bootSingletonsAndApps:ge,hostWorkspaceDashboard:we,hostWorkspaceApps:_e,probeLensFrontends:oe,reapAll:Se}}P(createLensChildren,"createLensChildren"),d(createLensChildren,"createLensChildren");
@@ -235,3 +235,24 @@ export interface ServiceLayerDeps {
235
235
  reapForeign?: boolean;
236
236
  }
237
237
  export declare function createServiceLayer(rt: GatewayRuntime, obs: Observability, deps?: ServiceLayerDeps): ServiceLayer;
238
+ /**
239
+ * Which pid — if any — may be signalled to evict a stale adopted pod.
240
+ *
241
+ * The `.owner` sidecar used to be the whole answer, and it is an ordinary file at a predictable
242
+ * `$TMPDIR/<workspace-key>/<service>-devserver/` path. Another local user could pre-create that
243
+ * hierarchy, bind a fake socket, and write ANY pid into the sidecar — including the gateway's own —
244
+ * and the gateway would signal it with the victim's authority (issues/codex-security/18).
245
+ *
246
+ * The pod's SELF-REPORTED pid is the better witness: it came from the process actually listening on
247
+ * that socket, over that socket, in an exchange whose key fingerprint the caller has just verified.
248
+ * The sidecar can only corroborate it.
249
+ *
250
+ * Returns `undefined` — signal nothing — when:
251
+ * - the pod reported no pid (an older bundle): there is no trustworthy target;
252
+ * - the sidecar DISAGREES with the pod: that is evidence of tampering, not of a second pod;
253
+ * - the target is this gateway itself.
254
+ *
255
+ * Signalling nothing is safe. Unlinking the socket, which the caller does regardless, is what
256
+ * actually stops the pod being re-adopted.
257
+ */
258
+ export declare function chooseStalePodKillTarget(reportedPid: number | undefined, sidecarPid: number | undefined, selfPid: number): number | undefined;
@@ -1,3 +1,3 @@
1
- "use strict";var ae=Object.defineProperty;var v=(t,n)=>ae(t,"name",{value:n,configurable:!0});var ie=Object.defineProperty,i=v((t,n)=>ie(t,"name",{value:n,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isTestSourcePath=isTestSourcePath,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),types_1=require("./types");function eastWestEnv(t,n){const a={},l=t.serviceKeys[(0,service_keys_1.serviceKeyId)(n.wsKey,n.project)];if(l&&(a.LENSMCP_SERVICE_KEY=l),t.https){const u=require("../../../basic-ssl").caCertPath();fs.existsSync(u)&&(a.NODE_EXTRA_CA_CERTS=u)}return a}v(eastWestEnv,"eastWestEnv"),i(eastWestEnv,"eastWestEnv");const sleep=i(t=>new Promise(n=>setTimeout(n,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let n=2166136261;for(let a=0;a<t.length;a+=1)n^=t.charCodeAt(a),n=Math.imul(n,16777619);return n>>>0}v(fnv1a,"fnv1a"),i(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:n}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(n.split("\0").filter(Boolean))}catch{return new Set}}v(gitIgnoredPaths,"gitIgnoredPaths"),i(gitIgnoredPaths,"gitIgnoredPaths");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const n=t.split("/"),a=n[n.length-1]??"";if(TEST_FILE_RE.test(a))return!0;for(let l=0;l<n.length-1;l+=1)if(TEST_DIR_SEGMENTS.has(n[l]))return!0;return!1}v(isTestSourcePath,"isTestSourcePath"),i(isTestSourcePath,"isTestSourcePath");async function sourceSetSnapshot(t,n,a,l){const u=l?.ignoreTests??!0;let S=0,y=0,E=!1;const w=new Set,T=await gitIgnoredPaths(t),h=i(M=>path.relative(t,M).split(path.sep).join("/"),"rel"),k=i(async(M,K=!0)=>{if(S>=SCAN_FILE_CAP){E=!0;return}let R;try{R=await fs.promises.readdir(M,{withFileTypes:!0})}catch{return}for(const O of R){if(S>=SCAN_FILE_CAP){E=!0;return}const C=O.name,F=path.join(M,C);if(O.isDirectory()){if(!K||C.startsWith(".")||SRC_EXCLUDE.has(C)||a?.has(C)||C.endsWith("-devserver")||T.has(`${h(F)}/`))continue;await k(F)}else if(O.isFile()){const b=C.lastIndexOf(".");if(b<=0||!SRC_EXT.has(C.slice(b)))continue;const P=h(F);if(T.has(P)||u&&isTestSourcePath(P)||w.has(P))continue;w.add(P),S+=1,y^=fnv1a(P)}}},"walk");if(n&&n.length>0){await k(t,!1);for(const M of n)await k(M)}else await k(t);return{signature:`${S}:${(y>>>0).toString(36)}`,paths:w,truncated:E}}v(sourceSetSnapshot,"sourceSetSnapshot"),i(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,n,a,l){return(await sourceSetSnapshot(t,n,a,l)).signature}v(sourceSetSignature,"sourceSetSignature"),i(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,n,a,l){if(t.paths.size===n.paths.size){let E=!0;for(const w of t.paths)if(!n.paths.has(w)){E=!1;break}if(E)return null}const u=[],S=[];let y=t.truncated||n.truncated;for(const E of n.paths)t.paths.has(E)||(u.length+S.length<SOURCE_DIFF_PATH_CAP?u.push(E):y=!0);for(const E of t.paths)n.paths.has(E)||(u.length+S.length<SOURCE_DIFF_PATH_CAP?S.push(E):y=!0);return{changedAt:a,added:u,removed:S,truncated:y,...l!==void 0?{windowMs:l}:{}}}v(diffSourceSets,"diffSourceSets"),i(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,n,a,l){const u=n&&n>0?t-n:a;return Math.max(u,a)+l}v(sourceChangeWindowMs,"sourceChangeWindowMs"),i(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,n,a,l,u){const S=i(w=>{const T=path.relative(t,w).split(path.sep).filter(Boolean);return T.length===0||T[0].startsWith("..")?"":T[0]},"bucketOf")(n);if(!S)return[];const y=new Set(a.filter(Boolean)),E=new Set([S]);for(const w of l)w.startsWith(".")||SRC_EXCLUDE.has(w)||u?.has(w)||w.endsWith("-devserver")||y.has(w)||E.add(w);return[...E].sort().map(w=>path.join(t,w))}v(sourceScopeDirs,"sourceScopeDirs"),i(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,n,a,l){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||n-t.lastUsed<l.settleMs||t.lastStaleRecycleAt!=null&&n-t.lastStaleRecycleAt<l.cooldownMs?null:a>0&&a-t.spawnAt>l.graceMs?"stale-source-set":l.maxAgeMs>0&&n-t.spawnAt>l.maxAgeMs?"max-age":null}v(podRecycleReason,"podRecycleReason"),i(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,n,a,l,u){return!(l-n<u.cooldownMs||a<=0||a-t<=u.graceMs||l-a<u.settleMs)}v(lensFrontendStale,"lensFrontendStale"),i(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,n,a,l=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&l.length>0){const u=n-a,S=new Set(t.recentEvents.filter(y=>y.at>=u).map(y=>path.resolve(y.path)));return l.every(y=>S.has(path.resolve(y)))?"heard":"missed"}return t.lastFsSetEventAt>=n-a?"heard":"missed"}v(lensWatcherVerdict,"lensWatcherVerdict"),i(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,n,a){if(!t)return"unknown";if(t.degraded)return"degraded";const l=n-a;return t.lastWatchRunAt>=l||t.lastBuildAt>=l?"heard":"silent"}v(podWatcherVerdict,"podWatcherVerdict"),i(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,n,a,l){const u=t.hasListened?l.graceMs:Math.max(l.graceMs,l.neverListenedGraceMs);return a-t.spawnedAt<u||a-n<l.cooldownMs||t.probeFails<Math.max(2,l.failureThreshold)?null:"wedged-no-listener"}v(lensChildWedged,"lensChildWedged"),i(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,n){return t.signal||t.code===null||t.code===0||t.ranMs>n.fastFailMs?!1:t.portHeld}v(isPortConflictExit,"isPortConflictExit"),i(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,n,a={}){const l=a.spawnProcess??node_child_process_1.spawn,u=a.probeAlive??pod_probe_1.probePodAlive,S=a.killTree??((e,o="SIGTERM")=>{try{process.kill(-e.pid,o)}catch{try{e.kill(o)}catch{}}}),y=a.rollReadyMs??types_1.POD_ROLL_READY_MS,E=a.rollPollMs??750,{emit:w}=n,T=i(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),h=i((e,o,r,s,c)=>w(o,r,s,{wsKey:e.wsKey,...c},void 0,T(e)),"emitSvc"),k=i(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),M=i((e,o)=>{let r;try{r=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const s=e.proc?.pid;let c=0;for(const d of r.split(`
2
- `)){const p=d.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!p)continue;const _=Number(p[1]),$=Number(p[2]),m=p[3];if(m.includes(`run ${o}`)&&/\bnx(\.js)?\b/.test(m)&&!(m.includes("/node_modules/nx/")&&!m.includes(e.root))&&!(_===process.pid||_===s)&&!(s!==void 0&&$===s)){try{process.kill(-$,"SIGKILL")}catch{try{process.kill(_,"SIGKILL")}catch{}}c+=1}}return c>0&&(console.log(`[gateway] ${o}: reaped ${c} stale runner tree(s) \u2014 a foreign generation held the nx task lock`),h(e,"warning",`stale-runner reap: ${e.project} (${c} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:c})),c},"reapForeignRunners"),K=i((e,o,r)=>{const s=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,c=Math.max(1,e.decl.minPods??1),d=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),[p,_]=fs.existsSync(d)?[process.execPath,[d,"run",s,`--childCount=${c}`]]:["yarn",["nx","run",s,`--childCount=${c}`]],$=l(p,_,{cwd:e.root,detached:!0,env:{...(0,pod_env_1.podBaseEnv)(process.env,t.root,e.root),NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false",...e.wsKey?{LENSMCP_WS_KEY:e.wsKey}:{},LENSMCP_EVENT_FILE:e.wsKey===t.wsKey?t.eventFile:path.join(e.root,".lensmcp","events.jsonl"),LENSMCP_GATEWAY_PID:String(process.pid),LENSMCP_POD_GENERATION:String(o),...eastWestEnv(t,e)},stdio:["ignore","pipe","pipe"]});r==="active"&&(e.lastErrors=[],e.buildFailed=!1);const m=i(g=>{const j=g.toString();for(const D of j.split(`
3
- `)){if(/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(D)){const f=D.replace(/\x1b\[[0-9;]*m/g,"").trim();e.lastErrors.push(f),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const x=Date.now();(f!==e.lastErrorEmitSig||x-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=f,e.lastErrorEmitAt=x,h(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:f}))}/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(D)&&(e.buildFailed=!1),!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(D)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${s}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),h(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(L(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),M(e,s),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&R(e)},1500).unref?.())},250).unref?.())}},"sniff"),A=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0;return $.stdout?.on("data",g=>{if(t.stopped())return;m(g);const j=`[${e.project}] ${g}`;process.stdout.write(j),A?.(j)}),$.stderr?.on("data",g=>{if(t.stopped())return;m(g);const j=`[${e.project}] ${g}`;process.stderr.write(j),A?.(j)}),$.on("exit",g=>{if(e.proc!==$||(e.proc=void 0,e.state="down",t.stopped())||(console.log(`[gateway] ${s} exited (${g})`),h(e,g===0?"info":"warning",`service down: ${e.project} (exit ${g})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:g}),g===0||e.buildFailed))return;const j=Date.now();if(e.restarts=(e.restarts??[]).filter(f=>j-f<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(j);const D=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),h(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),R(e))},D)}),$},"launchServiceProcess"),R=i(e=>{if(t.stopped())return;const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,r=Math.max(1,e.decl.minPods??1);a.reapForeign!==!1&&M(e,o),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${o} (pods=${r})\u2026`),h(e,"info",`service starting: ${e.project} (${r} pod${r>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:r}),e.state="starting",e.spawnAt=Date.now(),e.generation=(e.generation??0)+1,e.proc=K(e,e.generation,"active")},"spawnService"),O=i(async e=>{const o=e.pool.socks[0];if(!o)return;const r=await(0,pod_probe_1.probePodAlive)(o,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);r&&(e.activeWork=r.activeWork,r.watcher&&(e.watcher=r.watcher))},"refreshActiveWork"),C=i(async e=>{const o=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!o)return[];const r=(0,node_crypto_1.createHash)("sha256").update(o).digest("hex").slice(0,8),s=[];for(const c of e.pool.socks){const d=await(0,pod_probe_1.probePodAlive)(c,void 0,o);d&&(!d.keyFingerprint||d.keyFingerprint!==r)&&s.push(c)}return s},"staleAdoptedSocks"),F=i((e,o)=>{for(const r of o){try{const s=Number(fs.readFileSync(`${r}.owner`,"utf8").trim());if(Number.isFinite(s)&&s>1)try{process.kill(s,"SIGTERM")}catch{}}catch{}try{fs.unlinkSync(r)}catch{}try{fs.unlinkSync(`${r}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(r=>!o.includes(r)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),b=i((e,o)=>{if(e.state==="up")return!1;e.state="up";const r=e.pool.socks.length;return h(e,"info",`service up: ${e.project} (${r} pods, ${o})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:r,adopted:o==="adopted"}),!0},"markUp"),P=i(async e=>{if(k(e.pool)>0){const s=await C(e);if(s.length>0&&(console.error(`[gateway] ${e.project}: ${s.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),h(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:s.length}),e.proc&&L(e,"stale adoption (foreign east-west key)"),F(e,s)),e.pool.socks.length>0)return b(e,"adopted"),!0}if(t.stopped())return!1;e.state!=="starting"&&!e.proc&&R(e);const o=Date.now()+t.startupTimeoutMs,r=Date.now()+12e3;for(;!t.stopped()&&Date.now()<o;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>r)return!1;if(k(e.pool)>0){e.state="up",e.buildFailed=!1;const s=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${s?`, cold start ${s}ms`:""})`),h(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${s??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:s}),!0}if(e.state==="down")return!1}return!1},"ensureUp"),J=i(e=>{const o=e.decl.maxPods??e.decl.minPods??1,r=e.pool.socks.length,s=e.decl.adminUrl??(e.decl.port?`http://localhost:${e.decl.port}`:void 0);if(!s||r===0||r>=o||e.inflight<=r||Date.now()-e.lastScaleAt<5e3)return;e.lastScaleAt=Date.now();const c=(s.startsWith("https")?require("node:https"):http).request(`${s}/webpack/scale`,{method:"POST",rejectUnauthorized:!1},d=>d.resume());c.on("error",()=>{}),c.end(JSON.stringify({pods:r+1})),console.log(`[gateway] autoscale ${e.project}: ${r} \u2192 ${r+1} pods (inflight ${e.inflight})`),h(e,"info",`autoscale: ${e.project} ${r} \u2192 ${r+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:r,to:r+1,inflight:e.inflight})},"scaleUp"),L=i((e,o,r)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),r?.announce!==!1&&h(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const s=e.proc;try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}S(s,"SIGTERM"),e.proc=void 0,e.state="down",setTimeout(()=>{G(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),G=i(e=>{try{for(const o of fs.readdirSync(e.pool.dir)){if(!o.endsWith(".sock"))continue;const r=path.join(e.pool.dir,o);try{const s=Number(fs.readFileSync(`${r}.owner`,"utf8").trim());if(Number.isFinite(s)&&s>0)try{process.kill(s,0);continue}catch{}}catch{}try{fs.unlinkSync(r)}catch{}try{fs.unlinkSync(`${r}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks"),Y=i((e,o)=>{if(e.rolling)return e.rolling;const r=e.proc;if(!r||t.stopped())return Promise.resolve(!1);const s=e.generation??0,c=s+1,d=Date.now();e.lastStaleRecycleAt=d,console.log(`[gateway] pod handoff start: ${e.project} g${s}\u2192g${c} reason=${o} \u2014 standby spawning, old generation keeps serving.`),h(e,"info",`pod handoff starting: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:o,generation:c,fromGeneration:s});const p=K(e,c,"standby"),_=(async()=>{const $=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],m=d+y;let A;for(;!t.stopped()&&Date.now()<m&&!(p.exitCode!==null||p.signalCode!==null);){k(e.pool);for(const g of e.pool.socks)if((await u(g,void 0,$))?.podGeneration===c){A=g;break}if(A)break;await sleep(E)}if(!A||t.stopped()){const g=t.stopped()?"gateway stopping":p.exitCode!==null||p.signalCode!==null?`standby exited (${p.exitCode??p.signalCode})`:`standby not ready within ${Math.round(y/1e3)}s`;try{p.stdout?.removeAllListeners("data"),p.stderr?.removeAllListeners("data")}catch{}return S(p,"SIGTERM"),console.error(`[gateway] pod handoff failed: ${e.project} g${c}: ${g} \u2014 old generation g${s} kept serving.`),h(e,"warning",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:o,generation:c,error:g,durationMs:Date.now()-d}),setTimeout(()=>G(e),1200).unref?.(),!1}if(e.proc!==r)return S(p,"SIGTERM"),!1;e.proc=p,e.generation=c,e.spawnAt=d,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{r.stdout?.removeAllListeners("data"),r.stderr?.removeAllListeners("data"),r.removeAllListeners("exit")}catch{}return S(r,"SIGTERM"),setTimeout(()=>{G(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${c} answered on ${path.basename(A)} in ${Date.now()-d}ms; retiring g${s}.`),h(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:o,generation:c,fromGeneration:s,durationMs:Date.now()-d}),!0})().finally(()=>{e.rolling=void 0});return e.rolling=_,_},"rollPod"),Q=i(e=>{const o=Date.now();if(e.restarts=(e.restarts??[]).filter(r=>o-r<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(e,"error",`service crash-looping: ${e.project} \u2014 wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project}),L(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.restarts.push(o),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),h(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:e.restarts.length}),L(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&R(e)},1500).unref?.()},"recycleWedged"),Z={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let q;const X=new Map;let U=!1,B=0,V=0;const ee=i(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(s=>s.project));for(const s of t.routes)s.lens&&o.add(s.lens.project);const r=new Set;for(const s of o){const c=e[s];if(!c)continue;const d=path.relative(t.root,c).split(path.sep).filter(Boolean)[0];d&&!d.startsWith("..")&&r.add(d)}return[...r]},"managedBuckets"),te=i((e,o,r,s)=>{const c=t.projectSourceRoots?.[e];if(c?.length)return c;const d=t.projectRoots?.[e];if(!d)return;const p=sourceScopeDirs(t.root,d,r,o,s);return p.length>0?p:void 0},"scopeFor"),re=i(()=>{if(U||Date.now()-B<types_1.POD_STALE_SCAN_MS)return;U=!0,B=Date.now();const e=B;(async()=>{const o=(0,scope_1.readSourceSetOptions)(t.root),r=o.exclude,s={ignoreTests:o.ignoreTests},c=await sourceSetSnapshot(t.root,void 0,r,s),d=Date.now(),p=sourceChangeWindowMs(d,V,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),_=q?diffSourceSets(q,c,d,p):null;if(_&&(t.sourceSetChangedAt=d,t.sourceSetChange=_,w("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:_.added,removed:_.removed,truncated:_.truncated})),q=c,!t.projectRoots){V=e;return}let $;try{$=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(f=>f.isDirectory()).map(f=>f.name)}catch{return}const m=ee(),A=new Set(t.services.map(f=>f.project));for(const f of t.routes)f.lens&&A.add(f.lens.project);const g=new Map;for(const f of A){const x=te(f,$,m,r);if(!x)continue;const I=x.join("|"),N=g.get(I)??{dirs:x,projects:[]};N.projects.push(f),g.set(I,N)}const j=Date.now(),D=sourceChangeWindowMs(j,V,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);V=e;for(const[f,{dirs:x,projects:I}]of g){const N=await sourceSetSnapshot(t.root,x,r,s),H=X.get(f);if(X.set(f,N),!H)continue;const W=diffSourceSets(H,N,j,D);if(W){t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const z of I)t.sourceSetChangedFor.set(z,j),t.sourceSetChangesFor.set(z,W);w("info",`source file set changed in ${I.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:I,added:W.added,removed:W.removed,truncated:W.truncated})}}})().catch(()=>{}).finally(()=>{U=!1})},"maybeScanSourceSet"),oe=i((e,o)=>{const r=(t.sourceSetChangesFor?.get(e.project)??t.sourceSetChange)?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,s=podWatcherVerdict(e.watcher,o,r),c=i(d=>(e.staleKeptAt!==o&&(e.staleKeptAt=o,console.log(`[gateway] stale source set: pod ${e.project} ${d} \u2014 no recycle.`)),!1),"keep");return s==="heard"?(e.staleVerifiedAt=o,h(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:s}),c(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`)):s==="degraded"?(console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0):s==="silent"?types_1.POD_WATCHER_SILENT==="keep"?c("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):!0:types_1.POD_WATCHER_UNVERIFIED==="keep"?c("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):!0},"podStaleRecycleAllowed"),se=i((e,o)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc){Y(e,o);return}console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),h(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const r=!!e.decl.eager;L(e,`${o} \u2014 recycling stale pod`,{announce:!1}),r&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&R(e)},1500).unref?.()},"recyclePod"),ne=i(()=>{const e=setInterval(()=>{ce().catch(o=>console.error("[gateway] sweep error:",o))},t.sweepMs);return e.unref(),e},"startSweeper"),ce=i(async()=>{{if(t.stopped())return;types_1.POD_RECYCLE_ENABLED&&re();for(const e of t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const o=e.decl.idleKillSec;if(o&&e.inflight===0&&Date.now()-e.lastUsed>o*1e3){L(e,`idle ${o}s`),e.wedgedSince=void 0;continue}const r=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,s=k(e.pool)>0;if(s||r){if(e.wedgedSince=void 0,s&&b(e,"observed"),s&&!r&&types_1.POD_RECYCLE_ENABLED){const c=t.sourceSetChangedFor?.get(e.project)??t.sourceSetChangedAt??0;await O(e);const d=(e.staleVerifiedAt??0)>=c&&c>0,p=podRecycleReason(e,Date.now(),d?0:c,Z);if(p==="stale-source-set"&&!oe(e,c))continue;p&&se(e,p)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,Q(e))}}},"sweepOnce");return{scanNow:k,spawnService:R,ensureUp:P,scaleUp:J,killSpawned:L,markUp:b,startSweeper:ne,rollPod:Y}}v(createServiceLayer,"createServiceLayer"),i(createServiceLayer,"createServiceLayer");
1
+ "use strict";var he=Object.defineProperty;var v=(t,n)=>he(t,"name",{value:n,configurable:!0});var fe=Object.defineProperty,i=v((t,n)=>fe(t,"name",{value:n,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isTestSourcePath=isTestSourcePath,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer,exports.chooseStalePodKillTarget=chooseStalePodKillTarget;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),types_1=require("./types");function eastWestEnv(t,n){const a={},d=t.serviceKeys[(0,service_keys_1.serviceKeyId)(n.wsKey,n.project)];if(d&&(a.LENSMCP_SERVICE_KEY=d),t.https){const f=require("../../../basic-ssl").caCertPath();fs.existsSync(f)&&(a.NODE_EXTRA_CA_CERTS=f)}return a}v(eastWestEnv,"eastWestEnv"),i(eastWestEnv,"eastWestEnv");const sleep=i(t=>new Promise(n=>setTimeout(n,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let n=2166136261;for(let a=0;a<t.length;a+=1)n^=t.charCodeAt(a),n=Math.imul(n,16777619);return n>>>0}v(fnv1a,"fnv1a"),i(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:n}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(n.split("\0").filter(Boolean))}catch{return new Set}}v(gitIgnoredPaths,"gitIgnoredPaths"),i(gitIgnoredPaths,"gitIgnoredPaths");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const n=t.split("/"),a=n[n.length-1]??"";if(TEST_FILE_RE.test(a))return!0;for(let d=0;d<n.length-1;d+=1)if(TEST_DIR_SEGMENTS.has(n[d]))return!0;return!1}v(isTestSourcePath,"isTestSourcePath"),i(isTestSourcePath,"isTestSourcePath");async function sourceSetSnapshot(t,n,a,d){const f=d?.ignoreTests??!0;let S=0,y=0,_=!1;const w=new Set,L=await gitIgnoredPaths(t),h=i(T=>path.relative(t,T).split(path.sep).join("/"),"rel"),k=i(async(T,B=!0)=>{if(S>=SCAN_FILE_CAP){_=!0;return}let C;try{C=await fs.promises.readdir(T,{withFileTypes:!0})}catch{return}for(const K of C){if(S>=SCAN_FILE_CAP){_=!0;return}const R=K.name,V=path.join(T,R);if(K.isDirectory()){if(!B||R.startsWith(".")||SRC_EXCLUDE.has(R)||a?.has(R)||R.endsWith("-devserver")||L.has(`${h(V)}/`))continue;await k(V)}else if(K.isFile()){const Y=R.lastIndexOf(".");if(Y<=0||!SRC_EXT.has(R.slice(Y)))continue;const D=h(V);if(L.has(D)||f&&isTestSourcePath(D)||w.has(D))continue;w.add(D),S+=1,y^=fnv1a(D)}}},"walk");if(n&&n.length>0){await k(t,!1);for(const T of n)await k(T)}else await k(t);return{signature:`${S}:${(y>>>0).toString(36)}`,paths:w,truncated:_}}v(sourceSetSnapshot,"sourceSetSnapshot"),i(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,n,a,d){return(await sourceSetSnapshot(t,n,a,d)).signature}v(sourceSetSignature,"sourceSetSignature"),i(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,n,a,d){if(t.paths.size===n.paths.size){let _=!0;for(const w of t.paths)if(!n.paths.has(w)){_=!1;break}if(_)return null}const f=[],S=[];let y=t.truncated||n.truncated;for(const _ of n.paths)t.paths.has(_)||(f.length+S.length<SOURCE_DIFF_PATH_CAP?f.push(_):y=!0);for(const _ of t.paths)n.paths.has(_)||(f.length+S.length<SOURCE_DIFF_PATH_CAP?S.push(_):y=!0);return{changedAt:a,added:f,removed:S,truncated:y,...d!==void 0?{windowMs:d}:{}}}v(diffSourceSets,"diffSourceSets"),i(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,n,a,d){const f=n&&n>0?t-n:a;return Math.max(f,a)+d}v(sourceChangeWindowMs,"sourceChangeWindowMs"),i(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,n,a,d,f){const S=i(w=>{const L=path.relative(t,w).split(path.sep).filter(Boolean);return L.length===0||L[0].startsWith("..")?"":L[0]},"bucketOf")(n);if(!S)return[];const y=new Set(a.filter(Boolean)),_=new Set([S]);for(const w of d)w.startsWith(".")||SRC_EXCLUDE.has(w)||f?.has(w)||w.endsWith("-devserver")||y.has(w)||_.add(w);return[..._].sort().map(w=>path.join(t,w))}v(sourceScopeDirs,"sourceScopeDirs"),i(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,n,a,d){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||n-t.lastUsed<d.settleMs||t.lastStaleRecycleAt!=null&&n-t.lastStaleRecycleAt<d.cooldownMs?null:a>0&&a-t.spawnAt>d.graceMs?"stale-source-set":d.maxAgeMs>0&&n-t.spawnAt>d.maxAgeMs?"max-age":null}v(podRecycleReason,"podRecycleReason"),i(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,n,a,d,f){return!(d-n<f.cooldownMs||a<=0||a-t<=f.graceMs||d-a<f.settleMs)}v(lensFrontendStale,"lensFrontendStale"),i(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,n,a,d=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&d.length>0){const f=n-a,S=new Set(t.recentEvents.filter(y=>y.at>=f).map(y=>path.resolve(y.path)));return d.every(y=>S.has(path.resolve(y)))?"heard":"missed"}return t.lastFsSetEventAt>=n-a?"heard":"missed"}v(lensWatcherVerdict,"lensWatcherVerdict"),i(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,n,a){if(!t)return"unknown";if(t.degraded)return"degraded";const d=n-a;return t.lastWatchRunAt>=d||t.lastBuildAt>=d?"heard":"silent"}v(podWatcherVerdict,"podWatcherVerdict"),i(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,n,a,d){const f=t.hasListened?d.graceMs:Math.max(d.graceMs,d.neverListenedGraceMs);return a-t.spawnedAt<f||a-n<d.cooldownMs||t.probeFails<Math.max(2,d.failureThreshold)?null:"wedged-no-listener"}v(lensChildWedged,"lensChildWedged"),i(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,n){return t.signal||t.code===null||t.code===0||t.ranMs>n.fastFailMs?!1:t.portHeld}v(isPortConflictExit,"isPortConflictExit"),i(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,n,a={}){const d=a.spawnProcess??node_child_process_1.spawn,f=a.probeAlive??pod_probe_1.probePodAlive,S=a.killTree??((e,o="SIGTERM")=>{try{process.kill(-e.pid,o)}catch{try{e.kill(o)}catch{}}}),y=a.rollReadyMs??types_1.POD_ROLL_READY_MS,_=a.rollPollMs??750,{emit:w}=n,L=i(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),h=i((e,o,s,r,c)=>w(o,s,r,{wsKey:e.wsKey,...c},void 0,L(e)),"emitSvc"),k=i(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),T=i((e,o)=>{let s;try{s=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const r=e.proc?.pid;let c=0;for(const l of s.split(`
2
+ `)){const p=l.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!p)continue;const j=Number(p[1]),m=Number(p[2]),$=p[3];if($.includes(`run ${o}`)&&/\bnx(\.js)?\b/.test($)&&!($.includes("/node_modules/nx/")&&!$.includes(e.root))&&!(j===process.pid||j===r)&&!(r!==void 0&&m===r)){try{process.kill(-m,"SIGKILL")}catch{try{process.kill(j,"SIGKILL")}catch{}}c+=1}}return c>0&&(console.log(`[gateway] ${o}: reaped ${c} stale runner tree(s) \u2014 a foreign generation held the nx task lock`),h(e,"warning",`stale-runner reap: ${e.project} (${c} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:c})),c},"reapForeignRunners"),B=i((e,o,s)=>{const r=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,c=Math.max(1,e.decl.minPods??1),l=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),[p,j]=fs.existsSync(l)?[process.execPath,[l,"run",r,`--childCount=${c}`]]:["yarn",["nx","run",r,`--childCount=${c}`]],m=d(p,j,{cwd:e.root,detached:!0,env:{...(0,pod_env_1.podBaseEnv)(process.env,t.root,e.root),NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false",...e.wsKey?{LENSMCP_WS_KEY:e.wsKey}:{},LENSMCP_EVENT_FILE:e.wsKey===t.wsKey?t.eventFile:path.join(e.root,".lensmcp","events.jsonl"),LENSMCP_GATEWAY_PID:String(process.pid),LENSMCP_POD_GENERATION:String(o),...eastWestEnv(t,e)},stdio:["ignore","pipe","pipe"]});s==="active"&&(e.lastErrors=[],e.buildFailed=!1);const $=i(u=>{const E=u.toString();for(const x of E.split(`
3
+ `)){if(/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(x)){const P=x.replace(/\x1b\[[0-9;]*m/g,"").trim();e.lastErrors.push(P),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const g=Date.now();(P!==e.lastErrorEmitSig||g-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=P,e.lastErrorEmitAt=g,h(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:P}))}/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(x)&&(e.buildFailed=!1),!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(x)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${r}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),h(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(O(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),T(e,r),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&C(e)},1500).unref?.())},250).unref?.())}},"sniff"),A=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0;return m.stdout?.on("data",u=>{if(t.stopped())return;$(u);const E=`[${e.project}] ${u}`;process.stdout.write(E),A?.(E)}),m.stderr?.on("data",u=>{if(t.stopped())return;$(u);const E=`[${e.project}] ${u}`;process.stderr.write(E),A?.(E)}),m.on("exit",u=>{if(e.proc!==m||(e.proc=void 0,e.state="down",t.stopped())||(console.log(`[gateway] ${r} exited (${u})`),h(e,u===0?"info":"warning",`service down: ${e.project} (exit ${u})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:u}),u===0||e.buildFailed))return;const E=Date.now();if(e.restarts=(e.restarts??[]).filter(P=>E-P<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(E);const x=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),h(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),C(e))},x)}),m},"launchServiceProcess"),C=i(e=>{if(t.stopped())return;const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,s=Math.max(1,e.decl.minPods??1);a.reapForeign!==!1&&T(e,o),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${o} (pods=${s})\u2026`),h(e,"info",`service starting: ${e.project} (${s} pod${s>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:s}),e.state="starting",e.spawnAt=Date.now(),e.generation=(e.generation??0)+1,e.proc=B(e,e.generation,"active")},"spawnService"),K=i(async e=>{const o=e.pool.socks[0];if(!o)return;const s=await(0,pod_probe_1.probePodAlive)(o,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);s&&(e.activeWork=s.activeWork,s.watcher&&(e.watcher=s.watcher))},"refreshActiveWork"),R=i(e=>{try{const o=Number(fs.readFileSync(`${e}.owner`,"utf8").trim());return Number.isInteger(o)&&o>1?o:void 0}catch{return}},"readOwnerSidecar"),V=i(async e=>{const o=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!o)return[];const s=(0,node_crypto_1.createHash)("sha256").update(o).digest("hex").slice(0,8),r=[];for(const c of e.pool.socks){const l=await(0,pod_probe_1.probePodAlive)(c,void 0,o);l&&(!l.keyFingerprint||l.keyFingerprint!==s)&&r.push({sock:c,pid:l.pid})}return r},"staleAdoptedSocks"),Y=i((e,o)=>{const s=o.map(r=>r.sock);for(const{sock:r,pid:c}of o){const l=chooseStalePodKillTarget(c,R(r),process.pid);if(l!==void 0)try{process.kill(l,"SIGTERM")}catch{}try{fs.unlinkSync(r)}catch{}try{fs.unlinkSync(`${r}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(r=>!s.includes(r)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),D=i((e,o)=>{if(e.state==="up")return!1;e.state="up";const s=e.pool.socks.length;return h(e,"info",`service up: ${e.project} (${s} pods, ${o})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:s,adopted:o==="adopted"}),!0},"markUp"),re=i(async e=>{if(k(e.pool)>0){const r=await V(e);if(r.length>0&&(console.error(`[gateway] ${e.project}: ${r.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),h(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:r.length}),e.proc&&O(e,"stale adoption (foreign east-west key)"),Y(e,r)),e.pool.socks.length>0)return D(e,"adopted"),!0}if(t.stopped())return!1;e.state!=="starting"&&!e.proc&&C(e);const o=Date.now()+t.startupTimeoutMs,s=Date.now()+12e3;for(;!t.stopped()&&Date.now()<o;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>s)return!1;if(k(e.pool)>0){e.state="up",e.buildFailed=!1;const r=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${r?`, cold start ${r}ms`:""})`),h(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${r??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:r}),!0}if(e.state==="down")return!1}return!1},"ensureUp"),se=i(e=>{const o=e.decl.maxPods??e.decl.minPods??1,s=e.pool.socks.length,r=e.decl.adminUrl??(e.decl.port?`http://localhost:${e.decl.port}`:void 0);if(!r||s===0||s>=o||e.inflight<=s||Date.now()-e.lastScaleAt<5e3)return;e.lastScaleAt=Date.now();const c=(r.startsWith("https")?require("node:https"):http).request(`${r}/webpack/scale`,{method:"POST",rejectUnauthorized:!1},l=>l.resume());c.on("error",()=>{}),c.end(JSON.stringify({pods:s+1})),console.log(`[gateway] autoscale ${e.project}: ${s} \u2192 ${s+1} pods (inflight ${e.inflight})`),h(e,"info",`autoscale: ${e.project} ${s} \u2192 ${s+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:s,to:s+1,inflight:e.inflight})},"scaleUp"),O=i((e,o,s)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),s?.announce!==!1&&h(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const r=e.proc;try{r.stdout?.removeAllListeners("data"),r.stderr?.removeAllListeners("data"),r.removeAllListeners("exit")}catch{}S(r,"SIGTERM"),e.proc=void 0,e.state="down",setTimeout(()=>{H(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),H=i(e=>{try{for(const o of fs.readdirSync(e.pool.dir)){if(!o.endsWith(".sock"))continue;const s=path.join(e.pool.dir,o);try{const r=Number(fs.readFileSync(`${s}.owner`,"utf8").trim());if(Number.isFinite(r)&&r>0)try{process.kill(r,0);continue}catch{}}catch{}try{fs.unlinkSync(s)}catch{}try{fs.unlinkSync(`${s}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks"),ee=i((e,o)=>{if(e.rolling)return e.rolling;const s=e.proc;if(!s||t.stopped())return Promise.resolve(!1);const r=e.generation??0,c=r+1,l=Date.now();e.lastStaleRecycleAt=l,console.log(`[gateway] pod handoff start: ${e.project} g${r}\u2192g${c} reason=${o} \u2014 standby spawning, old generation keeps serving.`),h(e,"info",`pod handoff starting: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:o,generation:c,fromGeneration:r});const p=B(e,c,"standby"),j=(async()=>{const m=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],$=l+y;let A;for(;!t.stopped()&&Date.now()<$&&!(p.exitCode!==null||p.signalCode!==null);){k(e.pool);for(const u of e.pool.socks)if((await f(u,void 0,m))?.podGeneration===c){A=u;break}if(A)break;await sleep(_)}if(!A||t.stopped()){const u=t.stopped()?"gateway stopping":p.exitCode!==null||p.signalCode!==null?`standby exited (${p.exitCode??p.signalCode})`:`standby not ready within ${Math.round(y/1e3)}s`;try{p.stdout?.removeAllListeners("data"),p.stderr?.removeAllListeners("data")}catch{}return S(p,"SIGTERM"),console.error(`[gateway] pod handoff failed: ${e.project} g${c}: ${u} \u2014 old generation g${r} kept serving.`),h(e,"warning",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:o,generation:c,error:u,durationMs:Date.now()-l}),setTimeout(()=>H(e),1200).unref?.(),!1}if(e.proc!==s)return S(p,"SIGTERM"),!1;e.proc=p,e.generation=c,e.spawnAt=l,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}return S(s,"SIGTERM"),setTimeout(()=>{H(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${c} answered on ${path.basename(A)} in ${Date.now()-l}ms; retiring g${r}.`),h(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:o,generation:c,fromGeneration:r,durationMs:Date.now()-l}),!0})().finally(()=>{e.rolling=void 0});return e.rolling=j,j},"rollPod"),ne=i(e=>{const o=Date.now();if(e.restarts=(e.restarts??[]).filter(s=>o-s<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(e,"error",`service crash-looping: ${e.project} \u2014 wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project}),O(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.restarts.push(o),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),h(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:e.restarts.length}),O(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&C(e)},1500).unref?.()},"recycleWedged"),ce={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let z;const G=new Map,te=new Map;let J=!1,Q=0,X=0;const ae=i(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(r=>r.project));for(const r of t.routes)r.lens&&o.add(r.lens.project);const s=new Set;for(const r of o){const c=e[r];if(!c)continue;const l=path.relative(t.root,c).split(path.sep).filter(Boolean)[0];l&&!l.startsWith("..")&&s.add(l)}return[...s]},"managedBuckets"),ie=i((e,o,s,r)=>{const c=t.projectSourceRoots?.[e];if(c?.length)return c;const l=t.projectRoots?.[e];if(!l)return;const p=sourceScopeDirs(t.root,l,s,o,r);return p.length>0?p:void 0},"scopeFor"),de=i(()=>{if(J||Date.now()-Q<types_1.POD_STALE_SCAN_MS)return;J=!0,Q=Date.now();const e=Q;(async()=>{const o=(0,scope_1.readSourceSetOptions)(t.root),s=o.exclude,r={ignoreTests:o.ignoreTests},c=await sourceSetSnapshot(t.root,void 0,s,r),l=Date.now(),p=sourceChangeWindowMs(l,X,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),j=z?diffSourceSets(z,c,l,p):null;if(j&&(t.sourceSetChangedAt=l,t.sourceSetChange=j,w("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:j.added,removed:j.removed,truncated:j.truncated})),z=c,!t.projectRoots){X=e;return}let m;try{m=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(g=>g.isDirectory()).map(g=>g.name)}catch{return}const $=ae(),A=new Set(t.services.map(g=>g.project));for(const g of t.routes)g.lens&&A.add(g.lens.project);const u=new Map;for(const g of A){const N=ie(g,m,$,s);if(!N)continue;const M=N.join("|"),b=u.get(M)??{dirs:N,projects:[]};b.projects.push(g),u.set(M,b)}const E=Date.now(),x=sourceChangeWindowMs(E,X,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);X=e;const P=i((g,N,M)=>{t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const b of g)t.sourceSetChangedFor.set(b,N),t.sourceSetChangesFor.set(b,M)},"stampScoped");for(const[g,{dirs:N,projects:M}]of u){const b=await sourceSetSnapshot(t.root,N,s,r),oe=G.get(g);G.set(g,b);const Z=new Map;for(const F of M){const I=te.get(F);if(I===void 0||I===g)continue;const W=Z.get(I)??[];W.push(F),Z.set(I,W)}for(const F of M)te.set(F,g);for(const[F,I]of Z){const W=G.get(F),U=W?diffSourceSets(W,b,E,x):j;U&&(P(I,W?E:l,U),w("info",`source file set changed \u2014 ${I.join(", ")}'s watch scope was re-keyed`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,rescoped:!0,projects:I,added:U.added,removed:U.removed,truncated:U.truncated}))}if(!oe)continue;const q=diffSourceSets(oe,b,E,x);q&&(P(M,E,q),w("info",`source file set changed in ${M.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:M,added:q.added,removed:q.removed,truncated:q.truncated}))}for(const g of[...G.keys()])u.has(g)||G.delete(g)})().catch(()=>{}).finally(()=>{J=!1})},"maybeScanSourceSet"),le=i((e,o)=>{const s=(t.sourceSetChangesFor?.get(e.project)??t.sourceSetChange)?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,r=podWatcherVerdict(e.watcher,o,s),c=i(l=>(e.staleKeptAt!==o&&(e.staleKeptAt=o,console.log(`[gateway] stale source set: pod ${e.project} ${l} \u2014 no recycle.`)),!1),"keep");return r==="heard"?(e.staleVerifiedAt=o,h(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:r}),c(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`)):r==="degraded"?(console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0):r==="silent"?types_1.POD_WATCHER_SILENT==="keep"?c("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):!0:types_1.POD_WATCHER_UNVERIFIED==="keep"?c("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):!0},"podStaleRecycleAllowed"),pe=i((e,o)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc){ee(e,o);return}console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),h(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const s=!!e.decl.eager;O(e,`${o} \u2014 recycling stale pod`,{announce:!1}),s&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&C(e)},1500).unref?.()},"recyclePod"),ue=i(()=>{const e=setInterval(()=>{ge().catch(o=>console.error("[gateway] sweep error:",o))},t.sweepMs);return e.unref(),e},"startSweeper"),ge=i(async()=>{{if(t.stopped())return;types_1.POD_RECYCLE_ENABLED&&de();for(const e of t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const o=e.decl.idleKillSec;if(o&&e.inflight===0&&Date.now()-e.lastUsed>o*1e3){O(e,`idle ${o}s`),e.wedgedSince=void 0;continue}const s=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,r=k(e.pool)>0;if(r||s){if(e.wedgedSince=void 0,r&&D(e,"observed"),r&&!s&&types_1.POD_RECYCLE_ENABLED){const c=t.sourceSetChangedFor?.get(e.project)??t.sourceSetChangedAt??0;await K(e);const l=(e.staleVerifiedAt??0)>=c&&c>0,p=podRecycleReason(e,Date.now(),l?0:c,ce);if(p==="stale-source-set"&&!le(e,c))continue;p&&pe(e,p)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,ne(e))}}},"sweepOnce");return{scanNow:k,spawnService:C,ensureUp:re,scaleUp:se,killSpawned:O,markUp:D,startSweeper:ue,rollPod:ee}}v(createServiceLayer,"createServiceLayer"),i(createServiceLayer,"createServiceLayer");function chooseStalePodKillTarget(t,n,a){if(!(t===void 0||!Number.isInteger(t)||t<=1)&&!(n!==void 0&&n!==t)&&t!==a)return t}v(chooseStalePodKillTarget,"chooseStalePodKillTarget"),i(chooseStalePodKillTarget,"chooseStalePodKillTarget");
@@ -3,6 +3,31 @@ import type * as stream from 'node:stream';
3
3
  import type { GatewayRuntime, Route } from './types';
4
4
  import type { Observability } from './observability';
5
5
  import type { ServiceLayer } from './lifecycle';
6
+ /**
7
+ * May the heal loop RE-ISSUE this request upstream after a connection-class failure?
8
+ *
9
+ * The loop below retries on the *absence of a downstream response* — `isConnError`, nothing written to
10
+ * the edge yet — and used to treat that as proof that no upstream bytes were written. It is not. A TCP
11
+ * upstream that accepts a `POST`, COMMITS it, and then resets before writing response headers is
12
+ * indistinguishable from one that never saw it: `ECONNRESET`, `res.headersSent === false`. The gateway
13
+ * then re-sent the same method, the same URL and the same credentials — the caller's cookies, its
14
+ * `authorization`, the gateway's own stamped `x-internal-token` and `x-forwarded-*` identity — every
15
+ * 300ms for the whole 8s heal window, i.e. up to ~26 replays of a side-effecting call the origin had
16
+ * already applied (issues/codex-security/28). Node flushes a `ClientRequest`'s header block the moment
17
+ * its socket connects, so those replays reach the upstream's request handler even though the retry can
18
+ * no longer produce a body — which is the second half of the same defect:
19
+ *
20
+ * A body cannot be replayed at all. `req` is a one-shot `IncomingMessage`; once the first attempt has
21
+ * piped it (node-http-proxy does this unconditionally, `pipeUpstream` for every non-bodyless method) the
22
+ * stream has ended, so a retry re-sends the ORIGINAL framing headers with an empty or partial body and
23
+ * the upstream waits for bytes that will never arrive. So "safe to retry" is not just an idempotent
24
+ * method, it is an idempotent method that carries NO BODY — the only request this gateway can reproduce
25
+ * byte-for-byte.
26
+ *
27
+ * The prod gateway reached the same place in round 2 by cutting its retry count to 1
28
+ * (issues/prod-gateway-2/07); the dev path never inherited it — the *two gateways, two codebases* rule.
29
+ */
30
+ export declare function isReplayableRequest(req: Pick<http.IncomingMessage, 'method' | 'headers'>): boolean;
6
31
  export interface ProxyServer {
7
32
  web(req: http.IncomingMessage, res: http.ServerResponse, opts: Record<string, unknown>, cb: (err: Error) => void): void;
8
33
  ws(req: http.IncomingMessage, socket: stream.Duplex, head: Buffer, opts: Record<string, unknown>, cb: (err?: Error) => void): void;
@@ -1 +1 @@
1
- "use strict";var L=Object.defineProperty;var T=(n,u)=>L(n,"name",{value:u,configurable:!0});var I=Object.defineProperty,s=T((n,u)=>I(n,"name",{value:u,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),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(n){const u=n.code??"";return u==="ECONNREFUSED"||u==="ECONNRESET"||u==="ETIMEDOUT"||u==="EPIPE"||u==="EHOSTUNREACH"||u==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(n.message)}T(isConnError,"isConnError"),s(isConnError,"isConnError");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),httpProxy=require("http-proxy");function declaredFirstByteMs(n){const u=Number(n);return Number.isFinite(u)&&u>0?u:types_1.POD_RESPONSE_TIMEOUT_MS}T(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");function armFirstByteGuard(n){let u,E=0,M=!1,j=!1;const v=s(b=>{u=setTimeout(O,b),u.unref?.()},"arm"),O=s(()=>{if(!(M||!n.pending())){if(E+=n.firstByteMs,n.ceilingMs<=0||E>=n.ceilingMs){n.onWedged(`no response within ${E}ms (wedged socket) \u2014 evicted + respawning`);return}(0,pod_probe_1.probePodAlive)(n.socketPath,void 0,n.serviceKey).then(b=>{if(!(M||!n.pending())){if(!b){n.onWedged(`no response within ${E}ms and no answer on ${types_1.POD_ALIVE_PATH} (wedged socket) \u2014 evicted + respawning`);return}j||(j=!0,n.onSlow(E,b)),v(Math.max(n.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))}})}},"onDeadline");return v(n.firstByteMs),{clear:s(()=>{M=!0,u&&clearTimeout(u)},"clear")}}T(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(n,u,E){const{recordEdge:M,finishTrace:j,traceStep:v,emit:O}=u,b=s(async e=>{if(!e.lens||!n.lensSupervisor)return"unsupported";let t;const r=new Promise(l=>{t=setTimeout(()=>l("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),t.unref?.()}),y=n.lensSupervisor.recover(e.lens,"proxy-connection").then(l=>l?"ready":"failed").catch(()=>"failed"),g=await Promise.race([y,r]);t&&clearTimeout(t);const f=n.lensSupervisor.status(e.lens);return g==="failed"&&(!f||f.state==="ready")?"unsupported":g},"waitForManagedRecovery"),x=s((e,t,r,y,g,f)=>{if(r.headersSent||r.writableEnded||r.destroyed){r.destroyed||r.destroy();return}const l=e.lens?n.lensSupervisor?.status(e.lens):void 0,_=f==="timeout"||l?.state==="starting"||l?.state==="replacing",a=(0,gateway_errors_1.ensureRequestId)(t),o=e.lens?{project:e.project,reason:l?.reason??"connection-failure",generation:l?.generation??0,..._?{retryAfterMs:2e3}:{}}:void 0,i=_?503:502,p=_?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${a} project=${e.project} target=${String(y)} state=${l?.state??"unmanaged"} generation=${l?.generation??0}: ${g.message}`),O("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:a,project:e.project,target:String(y),state:l?.state??"unmanaged",generation:l?.generation,error:g.message}),(0,edge_1.sendError)(r,t,i,p,_?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,o,_?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),$=(0,body_stall_1.createBodyStallGuard)(n.bodyStallMs??types_1.BODY_STALL_MS),U={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},k=new http.Agent(U),A=new(require("node:https")).Agent(U),R=s(e=>typeof e=="string"&&e.startsWith("https:")?A:k,"agentForTarget"),P=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:k}),D=s((e,t,r)=>{const y=s(()=>{r()||e.destroyed||e.destroy()},"onEdgeClose");t.once("close",y),e.once("close",()=>t.removeListener("close",y))},"propagateEdgeCancel");P.on("proxyReq",(e,t,r)=>{D(e,r,()=>r.writableEnded)}),P.on("proxyRes",(e,t,r)=>{const y=t.__lensmcpPod;y&&(e.headers["x-lensmcp-pod"]=y),delete e.headers["x-powered-by"],e.headers.server="LensMCP",r&&D(e,r,()=>r.writableEnded),r&&$.armResponse(r)});const C=s((e,t,r,y)=>{(0,edge_1.stampForwardingHeaders)(n,t),$.arm(t);const g={};for(const[d,c]of Object.entries(t.headers))c===void 0||d.startsWith(":")||HOP_BY_HOP.has(d)||(g[d]=c);let f=http,l;if(typeof e=="object")l={socketPath:e.socketPath,path:t.url,method:t.method,headers:g,agent:k};else{const d=new URL(e),c=d.protocol==="https:";f=c?require("node:https"):http,g.host=d.host,l={protocol:d.protocol,hostname:d.hostname,port:d.port||(c?443:80),path:t.url,method:t.method,headers:g,agent:R(e)}}const _=r;_.__lensmcpErrContained||(_.__lensmcpErrContained=!0,r.on("error",()=>{}));let a;const o=s(()=>{h.destroy()},"onReqError"),i=s(()=>{a&&!a.destroyed&&a.destroy()},"onEdgeClose"),p=s(()=>{t.removeListener("error",o),r.removeListener("close",i)},"detach"),h=f.request(l,d=>{if(a=d,y.onResponse(),r.headersSent||r.writableEnded||r.destroyed){d.destroy();return}const c={};for(const[S,q]of Object.entries(d.headers))q===void 0||HOP_BY_HOP.has(S)||(c[S]=q);delete c["x-powered-by"],c.server="LensMCP";const w=t.__lensmcpPod;w&&(c["x-lensmcp-pod"]=w);try{r.writeHead(d.statusCode??502,c)}catch(S){console.error("[gateway] h2 relay: bad response header, resetting stream:",S.message),d.destroy(),r.destroyed||r.destroy(S);return}d.pipe(r),$.armResponse(r);const m=s(()=>{!r.writableEnded&&!r.destroyed&&r.destroy()},"abortEdge");d.once("error",m),d.once("close",m),r.on("close",i)});h.once("error",d=>y.onError(d)),h.once("close",p),t.on("error",o),t.method==="GET"||t.method==="HEAD"||t.method==="OPTIONS"?h.end():t.pipe(h)},"pipeUpstream"),B=s(async(e,t,r)=>{const y=t.__lensmcpTrace;if(e.pool){const a=e.svc;a&&(a.lastUsed=Date.now(),a.inflight+=1,r.on("close",()=>{a.inflight-=1,a.lastUsed=Date.now()}));let o=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!o&&a&&(v(y,"cold-start",{project:e.project}),await E.ensureUp(a)&&(o=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),a&&o&&E.markUp(a,"observed"),a&&E.scaleUp(a),!o){const c=a?.lastErrors?.length?` Service errors: ${a.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(r,t,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${c} The watcher recovers and the next request retries automatically.`);return}const i=path.basename(o,".sock");t.__lensmcpPod=i,v(y,"pod-select",{pod:i,pods:e.pool.socks.length,strategy:"round-robin"});let p=!1;const h=s(c=>{e.pool.socks=e.pool.socks.filter(w=>w!==o),e.pool.idx=-1;try{fs.unlinkSync(o)}catch{}console.error(`[gateway] pod ${i} of ${e.project}: ${c}`)},"evictPod"),d=armFirstByteGuard({socketPath:o,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!p&&!r.headersSent&&!r.writableEnded,"pending"),onWedged:s(c=>{p=!0,h(c),(0,edge_1.sendError)(r,t,502,"unavailable",`Pod ${i} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:s((c,w)=>{console.log(`[gateway] pod ${i} of ${e.project}: slow first byte, pod alive (${c}ms, ${w.inflight} in-flight, ${w.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});r.on("close",()=>d.clear()),C({socketPath:o},t,r,{onResponse:s(()=>{d.clear(),p=!0},"onResponse"),onError:s(c=>{d.clear(),!p&&(p=!0,h(c.message),(0,edge_1.sendError)(r,t,502,"unavailable",`Pod ${i} of ${e.project} unavailable \u2014 retry.`))},"onError")});return}const g=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let f=0,l=!1;const _=s(()=>{const a=e.target;C(a,t,r,{onResponse:s(()=>{},"onResponse"),onError:s(o=>{if(isConnError(o)&&(f+=1),e.lens&&isConnError(o)&&f>=2&&!l&&!r.headersSent&&!r.writableEnded&&!r.destroyed){l=!0,b(e).then(i=>{if(!(r.headersSent||r.writableEnded||r.destroyed)){if(i==="ready"){_();return}if(i==="unsupported"&&Date.now()<g){setTimeout(_,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}x(e,t,r,e.target,o,i)}});return}if(isConnError(o)&&Date.now()<g&&!r.headersSent&&!r.writableEnded&&!r.destroyed){setTimeout(_,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(r.headersSent||r.writableEnded||r.destroyed){r.destroyed||r.destroy();return}x(e,t,r,a,o)},"onError")})},"tryTcp");_()},"forwardNative");return{proxy:P,forward:s(async(e,t,r)=>{const y=Date.now(),g=t.__lensmcpTrace;if(r.on("close",()=>{M(e,Date.now()-y,r.statusCode??0,t.__lensmcpCaller),j(g,e.project,r.statusCode??0,{...t.__lensmcpPod?{pod:t.__lensmcpPod}:{},...t.__lensmcpCaller?{caller:t.__lensmcpCaller}:{}})}),e.prependPrefix&&!t.url?.startsWith(e.prependPrefix)&&(t.url=e.prependPrefix+(t.url??"/")),(0,edge_1.stampForwardingHeaders)(n,t),$.arm(t),(t.httpVersionMajor??1)>=2){await B(e,t,r);return}if(e.pool){const o=e.svc;o&&(o.lastUsed=Date.now(),o.inflight+=1,r.on("close",()=>{o.inflight-=1,o.lastUsed=Date.now()}));let i=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!i&&o&&(v(g,"cold-start",{project:e.project}),await E.ensureUp(o)&&(i=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),o&&i&&E.markUp(o,"observed"),o&&E.scaleUp(o),!i){const m=o?.lastErrors?.length?` Service errors: ${o.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(r,t,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 p=path.basename(i,".sock");t.__lensmcpPod=p,v(g,"pod-select",{pod:p,pods:e.pool.socks.length,strategy:"round-robin"});let h=!1;const d=s(m=>{e.pool.socks=e.pool.socks.filter(S=>S!==i),e.pool.idx=-1;try{fs.unlinkSync(i)}catch{}console.error(`[gateway] pod ${p} of ${e.project}: ${m}`)},"evictPod"),c=armFirstByteGuard({socketPath:i,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!h&&!r.headersSent&&!r.writableEnded,"pending"),onWedged:s(m=>{h=!0,d(m),(0,edge_1.sendError)(r,t,502,"unavailable",`Pod ${p} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:s((m,S)=>{console.log(`[gateway] pod ${p} of ${e.project}: slow first byte, pod alive (${m}ms, ${S.inflight} in-flight, ${S.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});r.on("close",()=>c.clear());const w=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4);P.web(t,r,{target:{socketPath:i},agent:k,proxyTimeout:w},m=>{c.clear(),!h&&(h=!0,d(m.message),(0,edge_1.sendError)(r,t,502,"unavailable",`Pod ${p} of ${e.project} unavailable \u2014 retry.`))});return}const f=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let l=0,_=!1;const a=s(()=>{const o=e.target,i={target:o,autoRewrite:typeof o=="string",changeOrigin:!0,agent:R(o)};P.web(t,r,i,p=>{if(isConnError(p)&&(l+=1),e.lens&&isConnError(p)&&l>=2&&!_&&!r.headersSent&&!r.writableEnded&&!r.destroyed){_=!0,b(e).then(h=>{if(!(r.headersSent||r.writableEnded||r.destroyed)){if(h==="ready"){a();return}if(h==="unsupported"&&Date.now()<f){setTimeout(a,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}x(e,t,r,e.target,p,h)}});return}if(isConnError(p)&&Date.now()<f&&!r.headersSent&&!r.writableEnded&&!r.destroyed){setTimeout(a,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}r.destroyed||x(e,t,r,o,p)})},"tryTcp");a()},"forward"),agentForTarget:R,destroyAgents:s(()=>{$.stop(),k.destroy(),A.destroy()},"destroyAgents"),closeProxy:s(()=>{try{P.close?.()}catch{}},"closeProxy")}}T(createProxy,"createProxy"),s(createProxy,"createProxy");
1
+ "use strict";var L=Object.defineProperty;var T=(n,i)=>L(n,"name",{value:i,configurable:!0});var H=Object.defineProperty,r=T((n,i)=>H(n,"name",{value:i,configurable:!0}),"r");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(n){const i=n.code??"";return i==="ECONNREFUSED"||i==="ECONNRESET"||i==="ETIMEDOUT"||i==="EPIPE"||i==="EHOSTUNREACH"||i==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(n.message)}T(isConnError,"isConnError"),r(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(n){if(!IDEMPOTENT_METHODS.has((n.method??"GET").toUpperCase())||n.headers["transfer-encoding"]!==void 0)return!1;const i=n.headers["content-length"];return!(i!==void 0&&String(i).trim()!=="0")}T(isReplayableRequest,"isReplayableRequest"),r(isReplayableRequest,"isReplayableRequest");function edgeGone(n,i){return i.headersSent||i.writableEnded||i.destroyed===!0||n.aborted===!0}T(edgeGone,"edgeGone"),r(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(n){const i=Number(n);return Number.isFinite(i)&&i>0?i:types_1.POD_RESPONSE_TIMEOUT_MS}T(declaredFirstByteMs,"declaredFirstByteMs"),r(declaredFirstByteMs,"declaredFirstByteMs");function armFirstByteGuard(n){let i,m=0,$=!1,O=!1;const P=r(b=>{i=setTimeout(x,b),i.unref?.()},"arm"),x=r(()=>{if(!($||!n.pending())){if(m+=n.firstByteMs,n.ceilingMs<=0||m>=n.ceilingMs){n.onWedged(`no response within ${m}ms (wedged socket) \u2014 evicted + respawning`);return}(0,pod_probe_1.probePodAlive)(n.socketPath,void 0,n.serviceKey).then(b=>{if(!($||!n.pending())){if(!b){n.onWedged(`no response within ${m}ms and no answer on ${types_1.POD_ALIVE_PATH} (wedged socket) \u2014 evicted + respawning`);return}O||(O=!0,n.onSlow(m,b)),P(Math.max(n.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))}})}},"onDeadline");return P(n.firstByteMs),{clear:r(()=>{$=!0,i&&clearTimeout(i)},"clear")}}T(armFirstByteGuard,"armFirstByteGuard"),r(armFirstByteGuard,"armFirstByteGuard");function createProxy(n,i,m){const{recordEdge:$,finishTrace:O,traceStep:P,emit:x}=i,b=r(async e=>{if(!e.lens||!n.lensSupervisor)return"unsupported";let t;const o=new Promise(p=>{t=setTimeout(()=>p("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),t.unref?.()}),u=n.lensSupervisor.recover(e.lens,"proxy-connection").then(p=>p?"ready":"failed").catch(()=>"failed"),y=await Promise.race([u,o]);t&&clearTimeout(t);const _=n.lensSupervisor.status(e.lens);return y==="failed"&&(!_||_.state==="ready")?"unsupported":y},"waitForManagedRecovery"),j=r((e,t,o,u,y,_)=>{if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}const p=e.lens?n.lensSupervisor?.status(e.lens):void 0,h=_==="timeout"||p?.state==="starting"||p?.state==="replacing",f=(0,gateway_errors_1.ensureRequestId)(t),c=e.lens?{project:e.project,reason:p?.reason??"connection-failure",generation:p?.generation??0,...h?{retryAfterMs:2e3}:{}}:void 0,s=h?503:502,d=h?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${f} project=${e.project} target=${String(u)} state=${p?.state??"unmanaged"} generation=${p?.generation??0}: ${y.message}`),x("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:f,project:e.project,target:String(u),state:p?.state??"unmanaged",generation:p?.generation,error:y.message}),(0,edge_1.sendError)(o,t,s,d,h?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,c,h?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),k=(0,body_stall_1.createBodyStallGuard)(n.bodyStallMs??types_1.BODY_STALL_MS),D={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},R=new http.Agent(D),U=new(require("node:https")).Agent(D),A=r(e=>typeof e=="string"&&e.startsWith("https:")?U:R,"agentForTarget"),M=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:R}),C=r((e,t,o)=>{const u=r(()=>{o()||e.destroyed||e.destroy()},"onEdgeClose");t.once("close",u),e.once("close",()=>t.removeListener("close",u))},"propagateEdgeCancel");M.on("proxyReq",(e,t,o)=>{for(const u of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(u);e.setHeader("connection","keep-alive"),C(e,o,()=>o.writableEnded)}),M.on("proxyRes",(e,t,o)=>{const u=t.__lensmcpPod;u&&(e.headers["x-lensmcp-pod"]=u),delete e.headers["x-powered-by"],e.headers.server="LensMCP",o&&C(e,o,()=>o.writableEnded),o&&k.armResponse(o)});const N=r((e,t,o,u)=>{(0,edge_1.stampForwardingHeaders)(n,t),k.arm(t);const y={},_=t.method==="GET"||t.method==="HEAD"||t.method==="OPTIONS";for(const[a,l]of Object.entries(t.headers))l===void 0||a.startsWith(":")||HOP_BY_HOP.has(a)||_&&(a==="content-length"||a==="transfer-encoding")||(y[a]=l);let p=http,h;if(typeof e=="object")h={socketPath:e.socketPath,path:t.url,method:t.method,headers:y,agent:R};else{const a=new URL(e),l=a.protocol==="https:";p=l?require("node:https"):http,y.host=a.host,h={protocol:a.protocol,hostname:a.hostname,port:a.port||(l?443:80),path:t.url,method:t.method,headers:y,agent:A(e)}}const f=o;f.__lensmcpErrContained||(f.__lensmcpErrContained=!0,o.on("error",()=>{}));let c;const s=r(()=>{o.writableEnded||E.destroy()},"onReqGone"),d=r(()=>{c&&!c.destroyed&&c.destroy()},"onEdgeClose"),g=r(()=>{t.removeListener("error",s),t.removeListener("aborted",s),t.removeListener("close",s),o.removeListener("close",d)},"detach"),E=p.request(h,a=>{if(c=a,u.onResponse(),o.headersSent||o.writableEnded||o.destroyed){a.destroy();return}const l={};for(const[v,I]of Object.entries(a.headers))I===void 0||HOP_BY_HOP.has(v)||(l[v]=I);delete l["x-powered-by"],l.server="LensMCP";const w=t.__lensmcpPod;w&&(l["x-lensmcp-pod"]=w);try{o.writeHead(a.statusCode??502,l)}catch(v){console.error("[gateway] h2 relay: bad response header, resetting stream:",v.message),a.destroy(),o.destroyed||o.destroy(v);return}a.pipe(o),k.armResponse(o);const S=r(()=>{!o.writableEnded&&!o.destroyed&&o.destroy()},"abortEdge");a.once("error",S),a.once("close",S),o.on("close",d)});E.once("error",a=>u.onError(a)),E.once("close",g),t.on("error",s),t.on("aborted",s),t.on("close",s),_?E.end():t.pipe(E)},"pipeUpstream"),q=r(async(e,t,o)=>{const u=t.__lensmcpTrace;if(e.pool){const c=e.svc;c&&(c.lastUsed=Date.now(),c.inflight+=1,o.on("close",()=>{c.inflight-=1,c.lastUsed=Date.now()}));let s=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!s&&c&&(P(u,"cold-start",{project:e.project}),await m.ensureUp(c)&&(s=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),c&&s&&m.markUp(c,"observed"),c&&m.scaleUp(c),!s){const l=c?.lastErrors?.length?` Service errors: ${c.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,t,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${l} The watcher recovers and the next request retries automatically.`);return}const d=path.basename(s,".sock");t.__lensmcpPod=d,P(u,"pod-select",{pod:d,pods:e.pool.socks.length,strategy:"round-robin"});let g=!1;const E=r(l=>{e.pool.socks=e.pool.socks.filter(w=>w!==s),e.pool.idx=-1;try{fs.unlinkSync(s)}catch{}console.error(`[gateway] pod ${d} of ${e.project}: ${l}`)},"evictPod"),a=armFirstByteGuard({socketPath:s,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:r(()=>!g&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:r(l=>{g=!0,E(l),(0,edge_1.sendError)(o,t,502,"unavailable",`Pod ${d} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:r((l,w)=>{console.log(`[gateway] pod ${d} of ${e.project}: slow first byte, pod alive (${l}ms, ${w.inflight} in-flight, ${w.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>a.clear()),N({socketPath:s},t,o,{onResponse:r(()=>{a.clear(),g=!0},"onResponse"),onError:r(l=>{a.clear(),!g&&(g=!0,E(l.message),(0,edge_1.sendError)(o,t,502,"unavailable",`Pod ${d} of ${e.project} unavailable \u2014 retry.`))},"onError")});return}const y=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let _=0,p=!1;const h=isReplayableRequest(t),f=r(()=>{if(edgeGone(t,o))return;const c=e.target;N(c,t,o,{onResponse:r(()=>{},"onResponse"),onError:r(s=>{if(isConnError(s)&&(_+=1),e.lens&&isConnError(s)&&h&&_>=2&&!p&&!edgeGone(t,o)){p=!0,b(e).then(d=>{if(!edgeGone(t,o)){if(d==="ready"){f();return}if(d==="unsupported"&&Date.now()<y){setTimeout(f,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}j(e,t,o,e.target,s,d)}});return}if(isConnError(s)&&h&&Date.now()<y&&!edgeGone(t,o)){setTimeout(f,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}j(e,t,o,c,s)},"onError")})},"tryTcp");f()},"forwardNative");return{proxy:M,forward:r(async(e,t,o)=>{const u=Date.now(),y=t.__lensmcpTrace;if(o.on("close",()=>{$(e,Date.now()-u,o.statusCode??0,t.__lensmcpCaller),O(y,e.project,o.statusCode??0,{...t.__lensmcpPod?{pod:t.__lensmcpPod}:{},...t.__lensmcpCaller?{caller:t.__lensmcpCaller}:{}})}),e.prependPrefix&&!t.url?.startsWith(e.prependPrefix)&&(t.url=e.prependPrefix+(t.url??"/")),(0,edge_1.stampForwardingHeaders)(n,t),k.arm(t),(t.httpVersionMajor??1)>=2){await q(e,t,o);return}if(e.pool){const s=e.svc;s&&(s.lastUsed=Date.now(),s.inflight+=1,o.on("close",()=>{s.inflight-=1,s.lastUsed=Date.now()}));let d=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!d&&s&&(P(y,"cold-start",{project:e.project}),await m.ensureUp(s)&&(d=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),s&&d&&m.markUp(s,"observed"),s&&m.scaleUp(s),!d){const S=s?.lastErrors?.length?` Service errors: ${s.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,t,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${S} The watcher recovers and the next request retries automatically.`);return}const g=path.basename(d,".sock");t.__lensmcpPod=g,P(y,"pod-select",{pod:g,pods:e.pool.socks.length,strategy:"round-robin"});let E=!1;const a=r(S=>{e.pool.socks=e.pool.socks.filter(v=>v!==d),e.pool.idx=-1;try{fs.unlinkSync(d)}catch{}console.error(`[gateway] pod ${g} of ${e.project}: ${S}`)},"evictPod"),l=armFirstByteGuard({socketPath:d,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:r(()=>!E&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:r(S=>{E=!0,a(S),(0,edge_1.sendError)(o,t,502,"unavailable",`Pod ${g} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:r((S,v)=>{console.log(`[gateway] pod ${g} of ${e.project}: slow first byte, pod alive (${S}ms, ${v.inflight} in-flight, ${v.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>l.clear());const w=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4);M.web(t,o,{target:{socketPath:d},agent:R,proxyTimeout:w},S=>{l.clear(),!E&&(E=!0,a(S.message),(0,edge_1.sendError)(o,t,502,"unavailable",`Pod ${g} of ${e.project} unavailable \u2014 retry.`))});return}const _=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let p=0,h=!1;const f=isReplayableRequest(t),c=r(()=>{if(edgeGone(t,o))return;const s=e.target,d={target:s,autoRewrite:typeof s=="string",changeOrigin:!0,agent:A(s)};M.web(t,o,d,g=>{if(isConnError(g)&&(p+=1),e.lens&&isConnError(g)&&f&&p>=2&&!h&&!edgeGone(t,o)){h=!0,b(e).then(E=>{if(!edgeGone(t,o)){if(E==="ready"){c();return}if(E==="unsupported"&&Date.now()<_){setTimeout(c,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}j(e,t,o,e.target,g,E)}});return}if(isConnError(g)&&f&&Date.now()<_&&!edgeGone(t,o)){setTimeout(c,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}o.destroyed||j(e,t,o,s,g)})},"tryTcp");c()},"forward"),agentForTarget:A,destroyAgents:r(()=>{k.stop(),R.destroy(),U.destroy()},"destroyAgents"),closeProxy:r(()=>{try{M.close?.()}catch{}},"closeProxy")}}T(createProxy,"createProxy"),r(createProxy,"createProxy");
@@ -48,4 +48,94 @@ export declare function classifyClientError(code: string | undefined, message: s
48
48
  * tight loop, and the point is a dev NOTICING one — not drowning in thousands. One line per cause per
49
49
  * window; the suppressed count rides the next admitted line. Exported for the unit test. */
50
50
  export declare function createClientErrorLogGate(windowMs?: number): (cause: string) => boolean;
51
+ /**
52
+ * Retire the children a workspace's PREVIOUS registration spawned, and drop its reaper from the table.
53
+ *
54
+ * The map is a per-workspace slot, and `onRegister` used to `set()` straight over it: a second
55
+ * `gateway start` for an already-hosted workspace left the first registration's dashboard, vites and
56
+ * capture children running with no handle able to name them, holding their ports for the life of the
57
+ * daemon (issues/codex-security/50). Both the register and the unregister path go through here now, so
58
+ * "replace a registration" and "remove a registration" retire exactly the same set.
59
+ *
60
+ * The entry is deleted BEFORE the reaper runs, so a reaper that throws part-way cannot leave a dead
61
+ * closure behind to be invoked AGAIN on the next registration — by then its labels belong to the
62
+ * replacement, and calling it would SIGTERM the live children. A reaper failure is logged, never fatal:
63
+ * a workspace that joins with one stale child is degraded, one that fails to join at all is down.
64
+ */
65
+ export declare function retirePreviousWorkspace(reapers: Map<string, () => void>, wsKey: string, reason: 're-registered' | 'unregistered'): boolean;
66
+ /**
67
+ * `maxSessionMemory: 512` / `maxConcurrentStreams: 512` (below, in the h2 server options) are DELIBERATE
68
+ * and STAY — a full Vite reload streams tens of MB of transformed ESM over ONE h2 session and Node's 10 MB
69
+ * default turns that into a burst of `ERR_HTTP2_PROTOCOL_ERROR` (issues/foodguard/14, commit `cc6aca4`).
70
+ * What was missing is everything AROUND them. `maxSessionMemory` is a PER-SESSION accounting threshold, not
71
+ * a process budget, and nothing bounded the aggregate: no cap on concurrent h2 sessions, no cap on streams
72
+ * across sessions, and `GATEWAY_MAX_CONNECTIONS` defaults to `0` = unlimited, so the listener accepted
73
+ * sockets until the process ran out of file descriptors. A few sessions each requesting a large asset
74
+ * repeatedly and reading slowly therefore had NO ceiling at all — on routes that are public by design (no
75
+ * `cluster.auth` policy, so no credential is needed).
76
+ *
77
+ * These four knobs make the worst case FINITE and stated, without moving a single per-session ceiling:
78
+ *
79
+ * connections ≤ MAX_CONNECTIONS · h2 sessions ≤ MAX_H2_SESSIONS · open streams ≤ MAX_H2_STREAMS
80
+ * · a session with NO open streams is reclaimed after H2_IDLE_SESSION_MS
81
+ *
82
+ * so the h2 accounting ceiling is `MAX_H2_SESSIONS × maxSessionMemory` instead of unbounded × 512 MB. Every
83
+ * knob is env-overridable and `0` means "explicitly unbounded" (exactly the old behaviour), so a developer
84
+ * who trips one turns it off without a rebuild. They are read at START time, not at module load, because
85
+ * `startGateway` is called in-process by the executor and by the tests — a module-load constant (types.ts
86
+ * `envInt`) cannot be steered by either.
87
+ */
88
+ export declare const GATEWAY_MAX_CONNECTIONS_DEFAULT = 512;
89
+ /** Concurrent h2 sessions. One browser keeps ONE session per origin, so this is ~a dozen origins × a few
90
+ * clients; the reclaim below means reaching it evicts an IDLE session rather than refusing a developer. */
91
+ export declare const GATEWAY_MAX_H2_SESSIONS_DEFAULT = 64;
92
+ /** Open h2 streams across ALL sessions. A full Vite reload is a few hundred; 4096 is ~8 saturated sessions
93
+ * and is the only thing that bounds `sessions × maxConcurrentStreams` (= 32,768) proxied requests, each of
94
+ * which also holds upstream state (issues/codex-security/10). */
95
+ export declare const GATEWAY_MAX_H2_STREAMS_DEFAULT = 4096;
96
+ /** How long a session with ZERO open streams may sit before it is GOAWAY'd. Deliberately keyed on "no open
97
+ * streams", never on socket silence: a silent-but-open stream is an SSE/MCP subscription or an idle HMR
98
+ * channel, and cutting those is the regression this bound must not cause. nginx's `http2_idle_timeout`
99
+ * default is 3 min; 5 min is the dev-friendly side of that. */
100
+ export declare const GATEWAY_H2_IDLE_SESSION_MS_DEFAULT = 300000;
101
+ /**
102
+ * `server.maxConnections` for the dev listener. `GATEWAY_MAX_CONNECTIONS` (types.ts) resolves to `0` when
103
+ * `LENSMCP_MAX_CONNECTIONS` is unset, and `server.ts` only assigned it when `> 0` — i.e. the shipped
104
+ * default was "unlimited". The UNSET case now gets a real ceiling; an EXPLICIT `LENSMCP_MAX_CONNECTIONS=0`
105
+ * still means unlimited, so the documented escape hatch survives (types.ts's "0 (default) = unlimited"
106
+ * describes the CONSTANT, which is still what an explicit setting flows through).
107
+ */
108
+ export declare function resolveMaxConnections(raw: string | undefined): number;
109
+ export interface H2AggregateBudget {
110
+ /** Concurrent h2 sessions across every bound port. 0 = unbounded. */
111
+ maxSessions: number;
112
+ /** Concurrent OPEN h2 streams across every session. 0 = unbounded. */
113
+ maxStreams: number;
114
+ /** Reclaim a session that has had no open stream for this long. 0 = off. */
115
+ idleSessionMs: number;
116
+ }
117
+ /** `LENSMCP_MAX_H2_SESSIONS` / `LENSMCP_MAX_H2_STREAMS` / `LENSMCP_H2_IDLE_MS`, read at gateway start. */
118
+ export declare function h2BudgetFromEnv(env?: NodeJS.ProcessEnv): H2AggregateBudget;
119
+ type GovernedSession = Pick<import('node:http2').ServerHttp2Session, 'close' | 'goaway' | 'once' | 'destroyed' | 'closed'>;
120
+ type GovernedStream = Pick<import('node:http2').ServerHttp2Stream, 'close' | 'once'>;
121
+ export interface H2Governor {
122
+ /** Admit a freshly established session. `false` ⇒ REFUSED — it has already been GOAWAY'd and closed. */
123
+ admitSession(session: GovernedSession): boolean;
124
+ /** Admit a stream on an admitted session. `false` ⇒ REFUSED — already reset with REFUSED_STREAM. */
125
+ admitStream(session: GovernedSession, stream: GovernedStream): boolean;
126
+ /** Drop every armed idle timer (gateway `stop()`). */
127
+ dispose(): void;
128
+ }
129
+ /**
130
+ * The PROCESS-WIDE h2 admission governor. One instance is shared by every bound port, because the budget
131
+ * this enforces is the process's, not a listener's — two ports must not each get their own 64 sessions.
132
+ *
133
+ * Reaching the session cap RECLAIMS before it refuses: the oldest session with no open streams is
134
+ * GOAWAY'd to make room, so an abandoned tab or a slept laptop cannot hold a slot against the developer's
135
+ * next connection. Only when every live session is actually working is the newcomer refused — with
136
+ * `ENHANCE_YOUR_CALM` and `lastStreamID: 0`, i.e. "nothing of yours was processed, retry", rather than a
137
+ * silent socket destroy.
138
+ */
139
+ export declare function createH2Governor(budget: H2AggregateBudget, report: (kind: 'session-refused' | 'stream-refused' | 'session-reclaimed', detail: Record<string, unknown>) => void): H2Governor;
51
140
  export declare function startGateway(options: GatewayRuntimeOptions, context: GatewayContext): Promise<GatewayHandle>;
141
+ export {};
@@ -1,3 +1,3 @@
1
- "use strict";var ne=Object.defineProperty;var S=(r,t)=>ne(r,"name",{value:t,configurable:!0});var oe=Object.defineProperty,n=S((r,t)=>oe(r,"name",{value:t,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,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"),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(r,t){return new Promise((h,w)=>{const f=(r.startsWith("https:")?require("node:https"):require("node:http")).request(r,{method:"GET",headers:t?.headers,rejectUnauthorized:!1},_=>{const u=[];_.on("data",l=>u.push(l)),_.on("end",()=>{const l=_.statusCode??0,a=Buffer.concat(u).toString("utf8");h({ok:l>=200&&l<300,status:l,json:n(async()=>JSON.parse(a),"json")})})});f.on("error",w),f.end()})}S(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(r){const t=process.env.LENSMCP_GW_JWKS_URL,h=process.env.LENSMCP_GW_JWT_ISS,w=new Map;let f;const _=n((u,l)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${u}${l}`);const a=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:u,...h?{issuer:h}:{},fetchImpl:insecureLocalFetch});return a.refresh().catch(E=>console.warn("[gateway] edge JWKS warm failed (will retry):",E.message)),a},"make");return{for(u){if(t)return f??=_(t,"");const l=u.split(":")[0].toLowerCase();let a;for(const m of r()){const v=m.host;if(typeof v!="string"||!/^auth\./.test(v)||v.startsWith("internal."))continue;const T=v.slice(5);(l===T||l.endsWith(`.${T}`))&&(!a||T.length>a.length)&&(a=T)}if(!a)return;let E=w.get(a);return E||(E=_(`https://auth.${a}/jwks.json`,` (auto-derived for apex ${a})`),w.set(a,E)),E},stop(){f?.stop();for(const u of w.values())u.stop()}}}S(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(r,t){return r&&exports.BENIGN_SOCKET_ERRNOS.has(r)?{status:null,report:!1,cause:"client-abort"}:r==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:r==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:r==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:r?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:r??t??"unknown"}}S(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(r=1e4){const t=new Map;return h=>{const w=Date.now(),f=t.get(h)??0;return w-f<r?!1:(t.set(h,w),!0)}}S(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(r){r.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`),r.destroy()})}S(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");async function startGateway(r,t){const h=r.https!==!1,w=r.ports?.length?r.ports:[443],f=r.scanTtlMs??1500,_=r.startupTimeoutMs??12e4,u=r.sweepMs??1e4,l=r.trafficFlushMs??5e3;let a=!1;const E=process.env.LENSMCP_EVENT_FILE??path.join(t.root,".lensmcp","events.jsonl"),m=(0,scope_1.readLensScope)(t.root).key,v=t.projectsConfigurations?.projects??{},T=(0,discovery_1.projectSourceRoots)(t.root,v,t.projectDependencies),{routes:L,services:k}=(0,discovery_1.discoverRoutes)(t.root,v,void 0,m,T);if(L.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const q=new route_registry_1.RouteRegistry;q.register({wsKey:m,root:t.root,routes:L,services:k});const C=[];let W=n(()=>{},"refreshCert");const G=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(C,q),W()},"rebuildRoutes");G();const{serviceKeys:Y,keyToProject:z,keyToWorkspace:Q}=(0,service_keys_1.loadServiceKeys)(t.root,k,m),X=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...r.trustProxyHops!==void 0?{trustProxyHops:r.trustProxyHops}:{},...r.clientIpHeader?{clientIpHeader:r.clientIpHeader}:{},...r.clientIpTrustedProxies?{clientIpTrustedProxies:r.clientIpTrustedProxies}:{}}),p={root:t.root,wsKey:m,eventFile:E,routes:C,registry:q,rebuildRoutes:G,services:k,serviceKeys:Y,keyToProject:z,keyToWorkspace:Q,clientIp:X,bodyStallMs:r.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:f,startupTimeoutMs:_,sweepMs:u,trafficFlushMs:l,https:h,projectRoots:Object.fromEntries(Object.entries(t.projectsConfigurations?.projects??{}).map(([e,o])=>[e,path.resolve(t.root,o.root)])),projectSourceRoots:T,stopped:n(()=>a,"stopped")},d=(0,observability_1.createObservability)(E,{trafficFlushMs:l}),Z=createClientErrorLogGate(),b=(0,lifecycle_1.createServiceLayer)(p,d),A=(0,lens_children_1.createLensChildren)(p,d,r),x=new Map;let H;try{H=(0,control_1.startControlServer)({rt:p,killService:n((e,o)=>b.killSpawned(e,o),"killService"),onRegister:n(async e=>{const o=await A.hostWorkspaceDashboard(e),y=await A.hostWorkspaceApps(e);x.set(e.wsKey,()=>{o?.(),y()})},"onRegister"),onUnregister:n(e=>{x.get(e.wsKey)?.(),x.delete(e.wsKey)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const I=(0,proxy_1.createProxy)(p,d,b),K=buildEdgeVerifierPool(()=>p.routes),F=(0,auth_1.createEdgeAuth)(p,d,K);let M;if(r.middleware){const e=require(path.resolve(t.root,r.middleware));M=e.default??e}const ee=n(e=>{if(M)try{M(e.req)}catch(o){d.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:o.message}),e.route&&d.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${o.message}`)}},"builtinMiddleware"),U=r.hooks??{},D=(0,hooks_1.createHooks)({...U,afterAuth:[ee,...U.afterAuth??[]]}),B=(0,handler_1.createHandler)({rt:p,obs:d,auth:F,proxy:I,hooks:D}),re=(0,upgrade_1.createUpgrade)({rt:p,auth:F,proxy:I,hooks:D}),te=d.startEdgeFlusher(),se=b.startSweeper();await A.bootSingletonsAndApps();for(const e of k)e.decl.eager&&b.ensureUp(e);let P,O,$;if(h){const e=require("../../../basic-ssl"),o=(0,service_keys_1.gatewayCacheDir)(t.root);$=n(()=>e.getCertificateSync(o,"gateway",p.routes.map(y=>y.host).filter(y=>!!y)),"mintCert"),P=$(),O=e.caCertPath()}const N=[],j=[];for(const e of w){const o=P?require("node:http2").createSecureServer({key:P,cert:P,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512},(i,s)=>B(i,s)):http.createServer(B);o.on("upgrade",re),o.on("error",i=>console.error(`[gateway] port ${e}: ${i.code}`)),o.on("connection",guardInboundSocket),o.on("secureConnection",guardInboundSocket),o.on("tlsClientError",(i,s)=>s.destroy()),o.on("clientError",(i,s)=>{const g=i,c=classifyClientError(g.code,g.message);if(c.report&&Z(c.cause)){const V=s.remoteAddress??"unknown",J=c.status===null?"dropped":String(c.status);console.warn(`[gateway] inbound H1 clientError from ${V}: ${c.cause} (${g.code??g.message}${g.bytesParsed!==void 0?`, bytesParsed=${g.bytesParsed}`:""}) \u2192 ${J}. This status is the GATEWAY's, not the upstream service's.`),d.emit("warning",`inbound clientError: ${c.cause} \u2192 ${J}`,`gateway-client-error:${c.cause}`,{kind:"gateway-client-error",cause:c.cause,code:g.code??null,status:c.status,peer:V,bytesParsed:g.bytesParsed??null})}c.status!==null&&s.writable&&s.end(`HTTP/1.1 ${c.status} ${CLIENT_ERROR_REASON[c.status]}\r
1
+ "use strict";var ue=Object.defineProperty;var m=(e,r)=>ue(e,"name",{value:r,configurable:!0});var le=Object.defineProperty,n=m((e,r)=>le(e,"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_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"),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,r){return new Promise((u,S)=>{const a=(e.startsWith("https:")?require("node:https"):require("node:http")).request(e,{method:"GET",headers:r?.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",S),a.end()})}m(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(e){const r=process.env.LENSMCP_GW_JWKS_URL,u=process.env.LENSMCP_GW_JWT_ISS,S=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(r)return a??=g(r,"");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=S.get(l);return s||(s=g(`https://auth.${l}/jwks.json`,` (auto-derived for apex ${l})`),S.set(l,s)),s},stop(){a?.stop();for(const d of S.values())d.stop()}}}m(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(e,r){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??r??"unknown"}}m(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 r=new Map;return u=>{const S=Date.now(),a=r.get(u)??0;return S-a<e?!1:(r.set(u,S),!0)}}m(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(e){e.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`),e.destroy()})}m(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(e,r,u){const S=e.get(r);if(!S)return!1;e.delete(r),u==="re-registered"&&console.log(`[gateway] workspace '${r}' re-registered \u2014 retiring the children of its previous registration first.`);try{S()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${r}' (${u}): ${a.message}`)}return!0}m(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,r){if(e===void 0||e.trim()==="")return r;const u=Number(e);return!Number.isFinite(u)||u<0?r:Math.floor(u)}m(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(e){return parseBudget(e,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}m(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)}}m(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(e,r){const{NGHTTP2_ENHANCE_YOUR_CALM:u,NGHTTP2_REFUSED_STREAM:S}=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||(r("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),r("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()){r("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){r("stream-refused",{streams:g,maxStreams:e.maxStreams,sessions:a.size});try{i.close(S)}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()}}}m(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(e,r){const u=e.https!==!1,S=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(r.root,".lensmcp","events.jsonl"),i=(0,scope_1.readLensScope)(r.root).key,h=r.projectsConfigurations?.projects??{},C=(0,discovery_1.projectSourceRoots)(r.root,h,r.projectDependencies),{routes:F,services:v}=(0,discovery_1.discoverRoutes)(r.root,h,void 0,i,C);if(F.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const b=new route_registry_1.RouteRegistry;b.register({wsKey:i,root:r.root,routes:F,services:v});const N=[];let U=n(()=>{},"refreshCert");const Y=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(N,b),U()},"rebuildRoutes");Y();const{serviceKeys:Q,keyToProject:Z,keyToWorkspace:ee}=(0,service_keys_1.loadServiceKeys)(r.root,v,i),te=(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}:{}}),T={root:r.root,wsKey:i,eventFile:s,routes:N,registry:b,rebuildRoutes:Y,services:v,serviceKeys:Q,keyToProject:Z,keyToWorkspace:ee,clientIp:te,bodyStallMs:e.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:g,sweepMs:d,trafficFlushMs:p,https:u,projectRoots:Object.fromEntries(Object.entries(r.projectsConfigurations?.projects??{}).map(([t,o])=>[t,path.resolve(r.root,o.root)])),projectSourceRoots:C,stopped:n(()=>l,"stopped")},f=(0,observability_1.createObservability)(s,{trafficFlushMs:p}),re=createClientErrorLogGate(),x=h2BudgetFromEnv(),se=createClientErrorLogGate(3e4),$=createH2Governor(x,(t,o)=>{se(t)&&console.warn(`[gateway] h2 aggregate budget: ${t} (${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(t==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${t}`,`gateway-h2-budget:${t}`,{kind:"h2-aggregate-budget",event:t,project:"gateway",...o})}),P=(0,lifecycle_1.createServiceLayer)(T,f),O=(0,lens_children_1.createLensChildren)(T,f,e),H=new Map;let B;try{B=(0,control_1.startControlServer)({rt:T,killService:n((t,o)=>P.killSpawned(t,o),"killService"),onRegister:n(async t=>{retirePreviousWorkspace(H,t.wsKey,"re-registered");const o=await O.hostWorkspaceDashboard(t),w=await O.hostWorkspaceApps(t);H.set(t.wsKey,()=>{try{o?.()}finally{w()}})},"onRegister"),onUnregister:n(t=>{retirePreviousWorkspace(H,t.wsKey,"unregistered")},"onUnregister")})}catch(t){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",t.message)}const L=(0,proxy_1.createProxy)(T,f,P),X=buildEdgeVerifierPool(()=>T.routes),K=(0,auth_1.createEdgeAuth)(T,f,X);let G;if(e.middleware){const t=require(path.resolve(r.root,e.middleware));G=t.default??t}const oe=n(t=>{if(G)try{G(t.req)}catch(o){f.traceStep(t.trace,"auth",{mode:"middleware",ok:!1,reason:o.message}),t.route&&f.finishTrace(t.trace,t.route.project,401),t.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:T,obs:f,auth:K,proxy:L,hooks:V}),ne=(0,upgrade_1.createUpgrade)({rt:T,auth:K,proxy:L,hooks:V}),ae=f.startEdgeFlusher(),ie=P.startSweeper();await O.bootSingletonsAndApps();for(const t of v)t.decl.eager&&P.ensureUp(t);let R,W,k;if(u){const t=require("../../../basic-ssl"),o=(0,service_keys_1.gatewayCacheDir)(r.root);k=n(()=>t.getCertificateSync(o,"gateway",T.routes.map(w=>w.host).filter(w=>!!w)),"mintCert"),R=k(),W=t.caCertPath()}const j=[],I=[],q=e.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const t of S){const o=R?require("node:http2").createSecureServer({key:R,cert:R,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},(y,_)=>z(y,_)):http.createServer(z);o.on("upgrade",ne),o.on("error",y=>console.error(`[gateway] port ${t}: ${y.code}`)),o.on("connection",guardInboundSocket),o.on("secureConnection",guardInboundSocket),o.on("tlsClientError",(y,_)=>_.destroy()),o.on("clientError",(y,_)=>{const c=y,E=classifyClientError(c.code,c.message);if(E.report&&re(E.cause)){const M=_.remoteAddress??"unknown",A=E.status===null?"dropped":String(E.status);console.warn(`[gateway] inbound H1 clientError from ${M}: ${E.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: ${E.cause} \u2192 ${A}`,`gateway-client-error:${E.cause}`,{kind:"gateway-client-error",cause:E.cause,code:c.code??null,status:E.status,peer:M,bytesParsed:c.bytesParsed??null})}E.status!==null&&_.writable&&_.end(`HTTP/1.1 ${E.status} ${CLIENT_ERROR_REASON[E.status]}\r
2
2
  \r
3
- `),s.destroy()}),o.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,o.headersTimeout=r.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS;const y=r.maxConnections??types_1.GATEWAY_MAX_CONNECTIONS;y>0&&(o.maxConnections=y);const R=r.connectionsCheckingIntervalMs??types_1.GATEWAY_CONN_CHECK_MS;if(R>0&&(o.connectionsCheckingInterval=R),P!=null){const i=o;i.on("session",s=>{s.on("error",g=>{const c=g.code??g.message;console.warn(`[gateway] h2 session error (${c}) \u2014 dropping this session only`),s.destroyed||s.destroy()})}),i.on("sessionError",s=>{console.warn(`[gateway] h2 sessionError: ${s.code??s.message}`)})}await new Promise(i=>{o.listen(e,()=>{const s=o.address()?.port??e;j.push(s),console.log(`[gateway] Listening on ${P?"https":"http"}://localhost:${s}`),i()})}),N.push(o)}$&&(W=n(()=>{const e=$();for(const o of N)o.setSecureContext?.({key:e,cert:e})},"refreshCert")),console.log("[gateway] Routes:");for(const e of C){const o=e.pool?`pods@${e.pool.dir}`:e.target;console.log(`[gateway] ${e.host??"(default)"} \u2192 ${o}${e.prependPrefix?` (+${e.prependPrefix})`:""}${e.internal?" [internal: no middleware]":""} [${e.project}]`)}return O&&console.log(`[gateway] CA: ${O} \u2014 one-time setup via the trust executor.`),d.emit("info",`gateway up: ${C.length} routes on ${j.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:j,routes:C.map(e=>({host:e.host??"(default)",project:e.project,mode:e.pool?"pods":"tcp",internal:!!e.internal,prependPrefix:e.prependPrefix}))}),{ports:j,routes:C,services:k,stop:n(async()=>{if(!a){a=!0,clearInterval(te),clearInterval(se),d.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),K.stop(),H?.close(),(0,workspace_registry_1.unregisterWorkspace)(m);for(const e of k)b.killSpawned(e,"gateway shutdown");await A.reapAll(),await Promise.all(N.map(e=>new Promise(o=>{let y=!1;const R=n(()=>{y||(y=!0,o())},"done"),i=e;e.close(()=>R());try{i.closeIdleConnections?.()}catch{}setTimeout(()=>{try{i.closeAllConnections?.()}catch{}R()},types_1.GATEWAY_STOP_GRACE_MS).unref?.()}))),I.closeProxy(),I.destroyAgents(),(0,workspace_log_1.closeWorkspaceLogs)()}},"stop"),...p.lensSupervisor?{lensSupervisor:p.lensSupervisor}:{}}}S(startGateway,"startGateway"),n(startGateway,"startGateway");
3
+ `),_.destroy()}),o.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,o.headersTimeout=e.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,q>0&&(o.maxConnections=q);const w=e.connectionsCheckingIntervalMs??types_1.GATEWAY_CONN_CHECK_MS;if(w>0&&(o.connectionsCheckingInterval=w),R!=null){const y=o,{NGHTTP2_CANCEL:_}=require("node:http2").constants;y.on("session",c=>{if(!$.admitSession(c)){c.on("error",()=>{});return}let E=0,M=0;c.on("stream",A=>{$.admitStream(c,A)&&A.once("close",()=>{c.destroyed?M+=1:A.rstCode===_&&(E+=1)})}),c.once("close",()=>{M!==0&&(console.warn(`[gateway] h2 session closed with ${M} stream(s) in flight after ${E} 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/foodguard/14).`),f.emit("warning",`h2 session lost with ${M} stream(s) in flight`,"gateway-h2-session-lost",{kind:"h2-session-lost",project:"gateway",inflight:M,clientResets:E}))}),c.on("error",A=>{const ce=A.code??A.message;console.warn(`[gateway] h2 session error (${ce}) \u2014 dropping this session only`),c.destroyed||c.destroy()})}),y.on("sessionError",c=>{console.warn(`[gateway] h2 sessionError: ${c.code??c.message}`)})}await new Promise(y=>{o.listen(t,()=>{const _=o.address()?.port??t;I.push(_),console.log(`[gateway] Listening on ${R?"https":"http"}://localhost:${_}`),y()})}),j.push(o)}k&&(U=n(()=>{const t=k();for(const o of j)o.setSecureContext?.({key:t,cert:t})},"refreshCert")),console.log("[gateway] Routes:");for(const t of N){const o=t.pool?`pods@${t.pool.dir}`:t.target;console.log(`[gateway] ${t.host??"(default)"} \u2192 ${o}${t.prependPrefix?` (+${t.prependPrefix})`:""}${t.internal?" [internal: no middleware]":""} [${t.project}]`)}W&&console.log(`[gateway] CA: ${W} \u2014 one-time setup via the trust executor.`);const D=n(t=>t>0?String(t):"unbounded","unbounded");return console.log(`[gateway] front-door budget: \u2264${D(q)} connections, \u2264${D(x.maxSessions)} h2 sessions (each \u2264512 MB session memory, \u2264512 streams), \u2264${D(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).`),f.emit("info",`gateway up: ${N.length} routes on ${I.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:I,routes:N.map(t=>({host:t.host??"(default)",project:t.project,mode:t.pool?"pods":"tcp",internal:!!t.internal,prependPrefix:t.prependPrefix}))}),{ports:I,routes:N,services:v,stop:n(async()=>{if(!l){l=!0,clearInterval(ae),clearInterval(ie),f.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),X.stop(),B?.close(),(0,workspace_registry_1.unregisterWorkspace)(i);for(const t of v)P.killSpawned(t,"gateway shutdown");await O.reapAll(),await Promise.all(j.map(t=>new Promise(o=>{let w=!1;const y=n(()=>{w||(w=!0,o())},"done"),_=t;t.close(()=>y());try{_.closeIdleConnections?.()}catch{}setTimeout(()=>{try{_.closeAllConnections?.()}catch{}y()},types_1.GATEWAY_STOP_GRACE_MS).unref?.()}))),L.closeProxy(),L.destroyAgents(),$.dispose(),(0,workspace_log_1.closeWorkspaceLogs)()}},"stop"),...T.lensSupervisor?{lensSupervisor:T.lensSupervisor}:{}}}m(startGateway,"startGateway"),n(startGateway,"startGateway");