@lensmcp/cluster 1.21.3 → 1.21.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/create-webpack-dev.d.ts +45 -0
  2. package/create-webpack-dev.js +1 -1
  3. package/executors/gateway/body-stall.d.ts +37 -0
  4. package/executors/gateway/body-stall.js +1 -0
  5. package/executors/gateway/canonical-path.d.ts +65 -0
  6. package/executors/gateway/canonical-path.js +1 -0
  7. package/executors/gateway/client-ip.d.ts +44 -0
  8. package/executors/gateway/client-ip.js +1 -0
  9. package/executors/gateway/jwks-verify.js +1 -1
  10. package/executors/gateway/main.prod-gateway.js +2 -2
  11. package/executors/gateway/manifest.d.ts +27 -3
  12. package/executors/gateway/manifest.js +1 -1
  13. package/executors/gateway/prod-config.d.ts +74 -0
  14. package/executors/gateway/prod-config.js +1 -0
  15. package/executors/gateway/prod-runtime/access-log.js +1 -1
  16. package/executors/gateway/prod-runtime/app.d.ts +6 -0
  17. package/executors/gateway/prod-runtime/app.js +1 -1
  18. package/executors/gateway/prod-runtime/auth.d.ts +1 -1
  19. package/executors/gateway/prod-runtime/auth.js +1 -1
  20. package/executors/gateway/prod-runtime/body-stall.d.ts +5 -0
  21. package/executors/gateway/prod-runtime/body-stall.js +1 -0
  22. package/executors/gateway/prod-runtime/cors.js +1 -1
  23. package/executors/gateway/prod-runtime/edge.d.ts +19 -3
  24. package/executors/gateway/prod-runtime/edge.js +1 -1
  25. package/executors/gateway/prod-runtime/handler.js +1 -1
  26. package/executors/gateway/prod-runtime/rollout.d.ts +11 -4
  27. package/executors/gateway/prod-runtime/rollout.js +1 -1
  28. package/executors/gateway/prod-runtime/routing.js +1 -1
  29. package/executors/gateway/prod-runtime/server.js +1 -1
  30. package/executors/gateway/prod-runtime/types.d.ts +30 -3
  31. package/executors/gateway/prod-runtime/upgrade.d.ts +25 -0
  32. package/executors/gateway/prod-runtime/upgrade.js +10 -2
  33. package/executors/gateway/prod-runtime/upstream.d.ts +3 -1
  34. package/executors/gateway/prod-runtime/upstream.js +1 -1
  35. package/executors/gateway/providers-prod.d.ts +13 -4
  36. package/executors/gateway/providers-prod.js +1 -1
  37. package/executors/gateway/registry-source.js +1 -1
  38. package/executors/gateway/rollout-ops.js +2 -2
  39. package/executors/gateway/runtime/auth.d.ts +6 -1
  40. package/executors/gateway/runtime/auth.js +1 -1
  41. package/executors/gateway/runtime/control.js +2 -1
  42. package/executors/gateway/runtime/dev-auth.d.ts +6 -2
  43. package/executors/gateway/runtime/dev-auth.js +1 -1
  44. package/executors/gateway/runtime/discovery.js +1 -1
  45. package/executors/gateway/runtime/edge.d.ts +19 -0
  46. package/executors/gateway/runtime/edge.js +1 -1
  47. package/executors/gateway/runtime/handler.js +1 -1
  48. package/executors/gateway/runtime/lens-children.d.ts +7 -0
  49. package/executors/gateway/runtime/lens-children.js +1 -1
  50. package/executors/gateway/runtime/lifecycle.d.ts +69 -7
  51. package/executors/gateway/runtime/lifecycle.js +3 -3
  52. package/executors/gateway/runtime/pod-probe.d.ts +37 -0
  53. package/executors/gateway/runtime/pod-probe.js +1 -0
  54. package/executors/gateway/runtime/proxy.js +1 -1
  55. package/executors/gateway/runtime/scope.d.ts +15 -0
  56. package/executors/gateway/runtime/scope.js +1 -1
  57. package/executors/gateway/runtime/server.js +2 -2
  58. package/executors/gateway/runtime/service-keys.d.ts +42 -1
  59. package/executors/gateway/runtime/service-keys.js +1 -1
  60. package/executors/gateway/runtime/types.d.ts +193 -1
  61. package/executors/gateway/runtime/types.js +1 -1
  62. package/executors/gateway/runtime/upgrade.js +1 -1
  63. package/executors/gateway/runtime/workspace-log.d.ts +7 -0
  64. package/executors/gateway/runtime/workspace-log.js +1 -0
  65. package/main.devserver.js +7 -7
  66. package/package.json +4 -4
@@ -0,0 +1,37 @@
1
+ import { type PodWatcherTestimony } from './types';
2
+ /**
3
+ * Is the pod behind this socket ALIVE, or actually wedged?
4
+ *
5
+ * The first-byte deadline cannot tell the two apart — a stale socket left by a hot-swap
6
+ * and an endpoint doing a 44-second model call both send nothing. So before evicting,
7
+ * ask. `GET /__lensmcp/alive` is served by the devserver child's own raw HTTP server,
8
+ * ahead of the app handler (`main.devserver.ts`), so it answers while the app is busy on
9
+ * I/O and falls silent only when the event loop is genuinely dead — which is exactly the
10
+ * distinction the deadline was missing.
11
+ *
12
+ * Same shape as `probeWatcherHealth` in `lens-children.ts`, and for the same reason: a
13
+ * probe that throws, hangs or 404s must read as "no evidence", never as an error the
14
+ * caller has to handle. A pod running an older `@lensmcp/cluster` has no such route, so
15
+ * it answers 404 → `undefined` → the caller keeps today's evict-on-deadline behaviour.
16
+ */
17
+ export interface PodAliveness {
18
+ /** In-flight requests the pod's CURRENT generation is serving. */
19
+ inflight: number;
20
+ /** In-process work that is not a request (queue jobs). */
21
+ activeWork: number;
22
+ /** True while the pod is mid hot-swap. */
23
+ swapping: boolean;
24
+ pid?: number;
25
+ /** Short hash of the pod's `LENSMCP_SERVICE_KEY` — lets a daemon verify an adopted pod. */
26
+ keyFingerprint?: string;
27
+ /** The pod's webpack-watcher testimony (absent on a pod whose devserver bundle predates it). */
28
+ watcher?: PodWatcherTestimony;
29
+ /** The pod GENERATION its tree was spawned as (`LENSMCP_POD_GENERATION`) — a rolling recycle waits for it. */
30
+ podGeneration?: number;
31
+ }
32
+ /** Parse the `watcher` block defensively — it crosses a process boundary from an arbitrary bundle version. */
33
+ export declare function parsePodWatcherTestimony(raw: unknown): PodWatcherTestimony | undefined;
34
+ export declare function probePodAlive(socketPath: string, timeoutMs?: number,
35
+ /** The target service's east-west key. The pod refuses the route without it, so that a
36
+ * client coming through the gateway cannot read its diagnostics. */
37
+ serviceKey?: string): Promise<PodAliveness | undefined>;
@@ -0,0 +1 @@
1
+ "use strict";var h=Object.defineProperty;var c=(r,t)=>h(r,"name",{value:t,configurable:!0});var f=Object.defineProperty,a=c((r,t)=>f(r,"name",{value:t,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.parsePodWatcherTestimony=parsePodWatcherTestimony,exports.probePodAlive=probePodAlive;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),types_1=require("./types");function parsePodWatcherTestimony(r){if(!r||typeof r!="object")return;const t=r;if(t.schemaVersion!==1)return;const o=a(n=>typeof n=="number"&&Number.isFinite(n)?n:0,"num"),s=t.degraded;return{schemaVersion:1,lastWatchRunAt:o(t.lastWatchRunAt),lastBuildAt:o(t.lastBuildAt),buildsSinceSpawn:o(t.buildsSinceSpawn),degraded:typeof s=="string"&&s?s:s===!0?"degraded":!1,recentPaths:Array.isArray(t.recentPaths)?t.recentPaths.filter(n=>typeof n=="string").slice(-32):[]}}c(parsePodWatcherTestimony,"parsePodWatcherTestimony"),a(parsePodWatcherTestimony,"parsePodWatcherTestimony");async function probePodAlive(r,t=types_1.POD_ALIVE_PROBE_TIMEOUT_MS,o){return new Promise(s=>{let n=!1;const d=a(i=>{n||(n=!0,s(i))},"finish");let p;try{p=http.request({socketPath:r,path:types_1.POD_ALIVE_PATH,method:"GET",timeout:t,agent:!1,...o?{headers:{"x-internal-token":o}}:{}},i=>{if(i.statusCode!==200){i.resume(),d(void 0);return}let u="";i.setEncoding("utf8"),i.on("data",e=>{u+=e}),i.on("end",()=>{try{const e=JSON.parse(u);d(e.ok===!0?{inflight:Number(e.inflight??0),activeWork:Number(e.activeWork??0),swapping:e.swapping===!0,...typeof e.pid=="number"?{pid:e.pid}:{},...typeof e.keyFingerprint=="string"?{keyFingerprint:e.keyFingerprint}:{},...(()=>{const l=parsePodWatcherTestimony(e.watcher);return l?{watcher:l}:{}})(),...typeof e.podGeneration=="number"&&Number.isFinite(e.podGeneration)?{podGeneration:e.podGeneration}:{}}:void 0)}catch{d(void 0)}})})}catch{d(void 0);return}p.on("timeout",()=>{p.destroy(),d(void 0)}),p.on("error",()=>d(void 0)),p.end()})}c(probePodAlive,"probePodAlive"),a(probePodAlive,"probePodAlive");
@@ -1 +1 @@
1
- "use strict";var L=Object.defineProperty;var x=(E,f)=>L(E,"name",{value:f,configurable:!0});var H=Object.defineProperty,c=x((E,f)=>H(E,"name",{value:f,configurable:!0}),"c");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"),types_1=require("./types");function isConnError(E){const f=E.code??"";return f==="ECONNREFUSED"||f==="ECONNRESET"||f==="ETIMEDOUT"||f==="EPIPE"||f==="EHOSTUNREACH"||f==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(E.message)}x(isConnError,"isConnError"),c(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 createProxy(E,f,w){const{recordEdge:C,finishTrace:N,traceStep:T,emit:q}=f,k=c(async r=>{if(!r.lens||!E.lensSupervisor)return"unsupported";let t;const e=new Promise(l=>{t=setTimeout(()=>l("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),t.unref?.()}),p=E.lensSupervisor.recover(r.lens,"proxy-connection").then(l=>l?"ready":"failed").catch(()=>"failed"),i=await Promise.race([p,e]);t&&clearTimeout(t);const g=E.lensSupervisor.status(r.lens);return i==="failed"&&(!g||g.state==="ready")?"unsupported":i},"waitForManagedRecovery"),b=c((r,t,e,p,i,g)=>{if(e.headersSent||e.writableEnded||e.destroyed){e.destroyed||e.destroy();return}const l=r.lens?E.lensSupervisor?.status(r.lens):void 0,u=g==="timeout"||l?.state==="starting"||l?.state==="replacing",s=(0,gateway_errors_1.ensureRequestId)(t),o=r.lens?{project:r.project,reason:l?.reason??"connection-failure",generation:l?.generation??0,...u?{retryAfterMs:2e3}:{}}:void 0,a=u?503:502,d=u?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${s} project=${r.project} target=${String(p)} state=${l?.state??"unmanaged"} generation=${l?.generation??0}: ${i.message}`),q("warning",`upstream unavailable: ${r.project}`,`cluster-upstream:${r.project}`,{kind:"upstream-error",traceId:s,project:r.project,target:String(p),state:l?.state??"unmanaged",generation:l?.generation,error:i.message}),(0,edge_1.sendError)(e,t,a,d,u?`Upstream ${r.project} is restarting.`:`Upstream ${r.project} unavailable.`,o,u?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),M={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},P=new http.Agent(M),O=new(require("node:https")).Agent(M),$=c(r=>typeof r=="string"&&r.startsWith("https:")?O:P,"agentForTarget"),S=httpProxy.createProxyServer({xfwd:!0,secure:!1,proxyTimeout:12e4,agent:P}),j=c((r,t,e)=>{const p=c(()=>{e()||r.destroyed||r.destroy()},"onEdgeClose");t.once("close",p),r.once("close",()=>t.removeListener("close",p))},"propagateEdgeCancel");S.on("proxyReq",(r,t,e)=>{j(r,e,()=>e.writableEnded)}),S.on("proxyRes",(r,t,e)=>{const p=t.__lensmcpPod;p&&(r.headers["x-lensmcp-pod"]=p),delete r.headers["x-powered-by"],r.headers.server="LensMCP",e&&j(r,e,()=>e.writableEnded)});const R=c((r,t,e,p)=>{const i={};for(const[n,m]of Object.entries(t.headers))m===void 0||n.startsWith(":")||HOP_BY_HOP.has(n)||(i[n]=m);const g=t.socket?.remoteAddress??"",l=t.headers["x-forwarded-for"];i["x-forwarded-for"]=l?`${String(l)}, ${g}`:g,i["x-forwarded-proto"]="https",i["x-forwarded-host"]||(i["x-forwarded-host"]=t.headers.host??"");let u=http,s;if(typeof r=="object")s={socketPath:r.socketPath,path:t.url,method:t.method,headers:i,agent:P};else{const n=new URL(r),m=n.protocol==="https:";u=m?require("node:https"):http,i.host=n.host,s={protocol:n.protocol,hostname:n.hostname,port:n.port||(m?443:80),path:t.url,method:t.method,headers:i,agent:$(r)}}const o=e;o.__lensmcpErrContained||(o.__lensmcpErrContained=!0,e.on("error",()=>{}));let a;const d=c(()=>{h.destroy()},"onReqError"),y=c(()=>{a&&!a.destroyed&&a.destroy()},"onEdgeClose"),_=c(()=>{t.removeListener("error",d),e.removeListener("close",y)},"detach"),h=u.request(s,n=>{if(a=n,p.onResponse(),e.headersSent||e.writableEnded||e.destroyed){n.destroy();return}const m={};for(const[v,D]of Object.entries(n.headers))D===void 0||HOP_BY_HOP.has(v)||(m[v]=D);delete m["x-powered-by"],m.server="LensMCP";const U=t.__lensmcpPod;U&&(m["x-lensmcp-pod"]=U);try{e.writeHead(n.statusCode??502,m)}catch(v){console.error("[gateway] h2 relay: bad response header, resetting stream:",v.message),n.destroy(),e.destroyed||e.destroy(v);return}n.pipe(e);const A=c(()=>{!e.writableEnded&&!e.destroyed&&e.destroy()},"abortEdge");n.once("error",A),n.once("close",A),e.on("close",y)});h.once("error",n=>p.onError(n)),h.once("close",_),t.on("error",d),t.method==="GET"||t.method==="HEAD"||t.method==="OPTIONS"?h.end():t.pipe(h)},"pipeUpstream"),I=c(async(r,t,e)=>{const p=t.__lensmcpTrace;if(r.pool){const s=r.svc;s&&(s.lastUsed=Date.now(),s.inflight+=1,e.on("close",()=>{s.inflight-=1,s.lastUsed=Date.now()}));let o=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs);if(!o&&s&&(T(p,"cold-start",{project:r.project}),await w.ensureUp(s)&&(o=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs))),s&&o&&w.markUp(s,"observed"),s&&w.scaleUp(s),!o){const h=s?.lastErrors?.length?` Service errors: ${s.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(e,t,503,"unavailable",`Service ${r.project} is not reachable (no pods in ${r.pool.dir}).${h} The watcher recovers and the next request retries automatically.`);return}const a=path.basename(o,".sock");t.__lensmcpPod=a,T(p,"pod-select",{pod:a,pods:r.pool.socks.length,strategy:"round-robin"});let d=!1;const y=c(h=>{r.pool.socks=r.pool.socks.filter(n=>n!==o),r.pool.idx=-1;try{fs.unlinkSync(o)}catch{}console.error(`[gateway] pod ${a} of ${r.project}: ${h}`)},"evictPod"),_=setTimeout(()=>{d||e.headersSent||e.writableEnded||(d=!0,y(`no response within ${types_1.POD_RESPONSE_TIMEOUT_MS}ms (wedged socket) \u2014 evicted + respawning`),(0,edge_1.sendError)(e,t,502,"unavailable",`Pod ${a} of ${r.project} was unresponsive \u2014 evicted, retry.`))},types_1.POD_RESPONSE_TIMEOUT_MS);_.unref?.(),e.on("close",()=>clearTimeout(_)),R({socketPath:o},t,e,{onResponse:c(()=>{clearTimeout(_),d=!0},"onResponse"),onError:c(h=>{clearTimeout(_),!d&&(d=!0,y(h.message),(0,edge_1.sendError)(e,t,502,"unavailable",`Pod ${a} of ${r.project} unavailable \u2014 retry.`))},"onError")});return}const i=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let g=0,l=!1;const u=c(()=>{const s=r.target;R(s,t,e,{onResponse:c(()=>{},"onResponse"),onError:c(o=>{if(isConnError(o)&&(g+=1),r.lens&&isConnError(o)&&g>=2&&!l&&!e.headersSent&&!e.writableEnded&&!e.destroyed){l=!0,k(r).then(a=>{if(!(e.headersSent||e.writableEnded||e.destroyed)){if(a==="ready"){u();return}if(a==="unsupported"&&Date.now()<i){setTimeout(u,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}b(r,t,e,r.target,o,a)}});return}if(isConnError(o)&&Date.now()<i&&!e.headersSent&&!e.writableEnded&&!e.destroyed){setTimeout(u,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(e.headersSent||e.writableEnded||e.destroyed){e.destroyed||e.destroy();return}b(r,t,e,s,o)},"onError")})},"tryTcp");u()},"forwardNative");return{proxy:S,forward:c(async(r,t,e)=>{const p=Date.now(),i=t.__lensmcpTrace;if(e.on("close",()=>{C(r,Date.now()-p,e.statusCode??0,t.__lensmcpCaller),N(i,r.project,e.statusCode??0,{...t.__lensmcpPod?{pod:t.__lensmcpPod}:{},...t.__lensmcpCaller?{caller:t.__lensmcpCaller}:{}})}),r.prependPrefix&&!t.url?.startsWith(r.prependPrefix)&&(t.url=r.prependPrefix+(t.url??"/")),(t.httpVersionMajor??1)>=2){await I(r,t,e);return}if(r.pool){const o=r.svc;o&&(o.lastUsed=Date.now(),o.inflight+=1,e.on("close",()=>{o.inflight-=1,o.lastUsed=Date.now()}));let a=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs);if(!a&&o&&(T(i,"cold-start",{project:r.project}),await w.ensureUp(o)&&(a=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs))),o&&a&&w.markUp(o,"observed"),o&&w.scaleUp(o),!a){const n=o?.lastErrors?.length?` Service errors: ${o.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(e,t,503,"unavailable",`Service ${r.project} is not reachable (no pods in ${r.pool.dir}).${n} The watcher recovers and the next request retries automatically.`);return}const d=path.basename(a,".sock");t.__lensmcpPod=d,T(i,"pod-select",{pod:d,pods:r.pool.socks.length,strategy:"round-robin"});let y=!1;const _=c(n=>{r.pool.socks=r.pool.socks.filter(m=>m!==a),r.pool.idx=-1;try{fs.unlinkSync(a)}catch{}console.error(`[gateway] pod ${d} of ${r.project}: ${n}`)},"evictPod"),h=setTimeout(()=>{y||e.headersSent||e.writableEnded||(y=!0,_(`no response within ${types_1.POD_RESPONSE_TIMEOUT_MS}ms (wedged socket) \u2014 evicted + respawning`),(0,edge_1.sendError)(e,t,502,"unavailable",`Pod ${d} of ${r.project} was unresponsive \u2014 evicted, retry.`))},types_1.POD_RESPONSE_TIMEOUT_MS);h.unref?.(),e.on("close",()=>clearTimeout(h)),S.web(t,e,{target:{socketPath:a},agent:P},n=>{clearTimeout(h),!y&&(y=!0,_(n.message),(0,edge_1.sendError)(e,t,502,"unavailable",`Pod ${d} of ${r.project} unavailable \u2014 retry.`))});return}const g=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let l=0,u=!1;const s=c(()=>{const o=r.target,a={target:o,autoRewrite:typeof o=="string",changeOrigin:!0,agent:$(o)};S.web(t,e,a,d=>{if(isConnError(d)&&(l+=1),r.lens&&isConnError(d)&&l>=2&&!u&&!e.headersSent&&!e.writableEnded&&!e.destroyed){u=!0,k(r).then(y=>{if(!(e.headersSent||e.writableEnded||e.destroyed)){if(y==="ready"){s();return}if(y==="unsupported"&&Date.now()<g){setTimeout(s,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}b(r,t,e,r.target,d,y)}});return}if(isConnError(d)&&Date.now()<g&&!e.headersSent&&!e.writableEnded&&!e.destroyed){setTimeout(s,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}e.destroyed||b(r,t,e,o,d)})},"tryTcp");s()},"forward"),agentForTarget:$,destroyAgents:c(()=>{P.destroy(),O.destroy()},"destroyAgents"),closeProxy:c(()=>{try{S.close?.()}catch{}},"closeProxy")}}x(createProxy,"createProxy"),c(createProxy,"createProxy");
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");
@@ -35,5 +35,20 @@ export declare function deriveMcpHttpPort(key: string): number;
35
35
  * globs), matched exactly like the built-in excludes. Empty when unset.
36
36
  */
37
37
  export declare function readSourceSetExclude(root: string): Set<string>;
38
+ /** Everything `.lensmcp/config.json` → `sourceSet` says about the staleness scan, in one read. */
39
+ export interface SourceSetConfig {
40
+ /** {@link readSourceSetExclude} — top-level dir names that are not source. */
41
+ exclude: Set<string>;
42
+ /**
43
+ * `sourceSet.ignoreTests` (default `true`): test/story files (`*.spec.*`, `*.test.*`, `*.e2e.*`,
44
+ * `*.stories.*`, anything under `__tests__/`/`__mocks__/`) never count as a source-set change. No
45
+ * running app can import them, so their arrival cannot make a dev server stale — yet every spec an
46
+ * agent session wrote rolled BOTH lens vites and scaled six pods to zero (foodguard 2026-09-06:
47
+ * `shared/contracts/src/haccp/__tests__/zz-r3probe.spec.ts`; issues/dev-gateway/01). `false` restores
48
+ * the old count-everything behaviour.
49
+ */
50
+ ignoreTests: boolean;
51
+ }
52
+ export declare function readSourceSetOptions(root: string): SourceSetConfig;
38
53
  export declare function lensKeyFrom(root: string): string;
39
54
  export declare function lensSlug(input: string): string;
@@ -1 +1 @@
1
- "use strict";var p=Object.defineProperty;var n=(t,r)=>p(t,"name",{value:r,configurable:!0});var c=Object.defineProperty,s=n((t,r)=>c(t,"name",{value:r,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.baseDomainOf=baseDomainOf,exports.readLensScope=readLensScope,exports.deriveMcpHttpPort=deriveMcpHttpPort,exports.readSourceSetExclude=readSourceSetExclude,exports.lensKeyFrom=lensKeyFrom,exports.lensSlug=lensSlug;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));function baseDomainOf(t){const r=t.filter(o=>!!o&&!o.startsWith("*."));if(r.length===0)return"local";const e=r.reduce((o,i)=>i.length<o.length?i:o);if(r.every(o=>o===e||o.endsWith("."+e)))return e;const a=e.split(".");return a.slice(-Math.min(a.length,3)).join(".")}n(baseDomainOf,"baseDomainOf"),s(baseDomainOf,"baseDomainOf");const BASE_PORTS={dashboard:4321,mcpHttp:4500};function hashToRange(t,r){let e=2166136261;for(let a=0;a<t.length;a++)e^=t.charCodeAt(a),e=Math.imul(e,16777619);return Math.abs(e|0)%r}n(hashToRange,"hashToRange"),s(hashToRange,"hashToRange");function readLensScope(t){try{const e=JSON.parse(fs.readFileSync(path.join(t,".lensmcp","config.json"),"utf8"));if(e&&typeof e.key=="string")return{key:e.key,dashboardPort:e.ports?.dashboard??BASE_PORTS.dashboard,mcpHttpPort:e.ports?.mcpHttp??deriveMcpHttpPort(e.key),basePath:e.dashboardBasePath??"/"+e.key}}catch{}const r=lensKeyFrom(t);return{key:r,dashboardPort:BASE_PORTS.dashboard,mcpHttpPort:deriveMcpHttpPort(r),basePath:"/"+r}}n(readLensScope,"readLensScope"),s(readLensScope,"readLensScope");function deriveMcpHttpPort(t){return BASE_PORTS.mcpHttp+hashToRange(t,200)}n(deriveMcpHttpPort,"deriveMcpHttpPort"),s(deriveMcpHttpPort,"deriveMcpHttpPort");function readSourceSetExclude(t){try{const r=JSON.parse(fs.readFileSync(path.join(t,".lensmcp","config.json"),"utf8"))?.sourceSet?.exclude;if(Array.isArray(r))return new Set(r.filter(e=>typeof e=="string"&&e.length>0&&!e.includes("/")))}catch{}return new Set}n(readSourceSetExclude,"readSourceSetExclude"),s(readSourceSetExclude,"readSourceSetExclude");function lensKeyFrom(t){try{const r=JSON.parse(fs.readFileSync(path.join(t,"package.json"),"utf8"));if(r?.name)return lensSlug(r.name)}catch{}return lensSlug(path.basename(t))}n(lensKeyFrom,"lensKeyFrom"),s(lensKeyFrom,"lensKeyFrom");function lensSlug(t){return t.toLowerCase().replace(/^@[^/]+\//,"").replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,"")||"workspace"}n(lensSlug,"lensSlug"),s(lensSlug,"lensSlug");
1
+ "use strict";var u=Object.defineProperty;var s=(e,r)=>u(e,"name",{value:r,configurable:!0});var a=Object.defineProperty,o=s((e,r)=>a(e,"name",{value:r,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.baseDomainOf=baseDomainOf,exports.readLensScope=readLensScope,exports.deriveMcpHttpPort=deriveMcpHttpPort,exports.readSourceSetExclude=readSourceSetExclude,exports.readSourceSetOptions=readSourceSetOptions,exports.lensKeyFrom=lensKeyFrom,exports.lensSlug=lensSlug;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));function baseDomainOf(e){const r=e.filter(n=>!!n&&!n.startsWith("*."));if(r.length===0)return"local";const t=r.reduce((n,i)=>i.length<n.length?i:n);if(r.every(n=>n===t||n.endsWith("."+t)))return t;const c=t.split(".");return c.slice(-Math.min(c.length,3)).join(".")}s(baseDomainOf,"baseDomainOf"),o(baseDomainOf,"baseDomainOf");const BASE_PORTS={dashboard:4321,mcpHttp:4500};function hashToRange(e,r){let t=2166136261;for(let c=0;c<e.length;c++)t^=e.charCodeAt(c),t=Math.imul(t,16777619);return Math.abs(t|0)%r}s(hashToRange,"hashToRange"),o(hashToRange,"hashToRange");function readLensScope(e){try{const t=JSON.parse(fs.readFileSync(path.join(e,".lensmcp","config.json"),"utf8"));if(t&&typeof t.key=="string")return{key:t.key,dashboardPort:t.ports?.dashboard??BASE_PORTS.dashboard,mcpHttpPort:t.ports?.mcpHttp??deriveMcpHttpPort(t.key),basePath:t.dashboardBasePath??"/"+t.key}}catch{}const r=lensKeyFrom(e);return{key:r,dashboardPort:BASE_PORTS.dashboard,mcpHttpPort:deriveMcpHttpPort(r),basePath:"/"+r}}s(readLensScope,"readLensScope"),o(readLensScope,"readLensScope");function deriveMcpHttpPort(e){return BASE_PORTS.mcpHttp+hashToRange(e,200)}s(deriveMcpHttpPort,"deriveMcpHttpPort"),o(deriveMcpHttpPort,"deriveMcpHttpPort");function readSourceSetExclude(e){return readSourceSetOptions(e).exclude}s(readSourceSetExclude,"readSourceSetExclude"),o(readSourceSetExclude,"readSourceSetExclude");function readSourceSetOptions(e){const r={exclude:new Set,ignoreTests:!0};try{const t=JSON.parse(fs.readFileSync(path.join(e,".lensmcp","config.json"),"utf8")),c=t?.sourceSet?.exclude;Array.isArray(c)&&(r.exclude=new Set(c.filter(n=>typeof n=="string"&&n.length>0&&!n.includes("/")))),t?.sourceSet?.ignoreTests===!1&&(r.ignoreTests=!1)}catch{}return r}s(readSourceSetOptions,"readSourceSetOptions"),o(readSourceSetOptions,"readSourceSetOptions");function lensKeyFrom(e){try{const r=JSON.parse(fs.readFileSync(path.join(e,"package.json"),"utf8"));if(r?.name)return lensSlug(r.name)}catch{}return lensSlug(path.basename(e))}s(lensKeyFrom,"lensKeyFrom"),o(lensKeyFrom,"lensKeyFrom");function lensSlug(e){return e.toLowerCase().replace(/^@[^/]+\//,"").replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,"")||"workspace"}s(lensSlug,"lensSlug"),o(lensSlug,"lensSlug");
@@ -1,3 +1,3 @@
1
- "use strict";var re=Object.defineProperty;var _=(r,t)=>re(r,"name",{value:t,configurable:!0});var ee=Object.defineProperty,n=_((r,t)=>ee(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"),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((y,g)=>{const h=(r.startsWith("https:")?require("node:https"):require("node:http")).request(r,{method:"GET",headers:t?.headers,rejectUnauthorized:!1},E=>{const l=[];E.on("data",u=>l.push(u)),E.on("end",()=>{const u=E.statusCode??0,a=Buffer.concat(l).toString("utf8");y({ok:u>=200&&u<300,status:u,json:n(async()=>JSON.parse(a),"json")})})});h.on("error",g),h.end()})}_(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(r){const t=process.env.LENSMCP_GW_JWKS_URL,y=process.env.LENSMCP_GW_JWT_ISS,g=new Map;let h;const E=n((l,u)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${l}${u}`);const a=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:l,...y?{issuer:y}:{},fetchImpl:insecureLocalFetch});return a.refresh().catch(w=>console.warn("[gateway] edge JWKS warm failed (will retry):",w.message)),a},"make");return{for(l){if(t)return h??=E(t,"");const u=l.split(":")[0].toLowerCase();let a;for(const v of r()){const m=v.host;if(typeof m!="string"||!/^auth\./.test(m)||m.startsWith("internal."))continue;const S=m.slice(5);(u===S||u.endsWith(`.${S}`))&&(!a||S.length>a.length)&&(a=S)}if(!a)return;let w=g.get(a);return w||(w=E(`https://auth.${a}/jwks.json`,` (auto-derived for apex ${a})`),g.set(a,w)),w},stop(){h?.stop();for(const l of g.values())l.stop()}}}_(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"}}_(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 y=>{const g=Date.now(),h=t.get(y)??0;return g-h<r?!1:(t.set(y,g),!0)}}_(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()})}_(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");async function startGateway(r,t){const y=r.https!==!1,g=r.ports?.length?r.ports:[443],h=r.scanTtlMs??1500,E=r.startupTimeoutMs??12e4,l=r.sweepMs??1e4,u=r.trafficFlushMs??5e3;let a=!1;const w=process.env.LENSMCP_EVENT_FILE??path.join(t.root,".lensmcp","events.jsonl"),v=(0,scope_1.readLensScope)(t.root).key,m=t.projectsConfigurations?.projects??{},S=(0,discovery_1.projectSourceRoots)(t.root,m,t.projectDependencies),{routes:N,services:T}=(0,discovery_1.discoverRoutes)(t.root,m,void 0,v,S);if(N.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:v,root:t.root,routes:N,services:T});const k=[];let I=n(()=>{},"refreshCert");const L=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(k,q),I()},"rebuildRoutes");L();const{serviceKeys:B,keyToProject:J}=(0,service_keys_1.loadServiceKeys)(t.root,T),f={root:t.root,wsKey:v,eventFile:w,routes:k,registry:q,rebuildRoutes:L,services:T,serviceKeys:B,keyToProject:J,scanTtlMs:h,startupTimeoutMs:E,sweepMs:l,trafficFlushMs:u,https:y,projectRoots:Object.fromEntries(Object.entries(t.projectsConfigurations?.projects??{}).map(([e,o])=>[e,path.resolve(t.root,o.root)])),projectSourceRoots:S,stopped:n(()=>a,"stopped")},p=(0,observability_1.createObservability)(w,{trafficFlushMs:u}),Y=createClientErrorLogGate(),b=(0,lifecycle_1.createServiceLayer)(f,p),C=(0,lens_children_1.createLensChildren)(f,p,r),A=new Map;let W;try{W=(0,control_1.startControlServer)({rt:f,killService:n((e,o)=>b.killSpawned(e,o),"killService"),onRegister:n(async e=>{const o=await C.hostWorkspaceDashboard(e),i=await C.hostWorkspaceApps(e);A.set(e.wsKey,()=>{o?.(),i()})},"onRegister"),onUnregister:n(e=>{A.get(e.wsKey)?.(),A.delete(e.wsKey)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const R=(0,proxy_1.createProxy)(f,p,b),G=buildEdgeVerifierPool(()=>f.routes),K=(0,auth_1.createEdgeAuth)(f,p,G);let M;if(r.middleware){const e=require(path.resolve(t.root,r.middleware));M=e.default??e}const z=n(e=>{if(M)try{M(e.req)}catch(o){p.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:o.message}),e.route&&p.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${o.message}`)}},"builtinMiddleware"),U=r.hooks??{},F=(0,hooks_1.createHooks)({...U,afterAuth:[z,...U.afterAuth??[]]}),H=(0,handler_1.createHandler)({rt:f,obs:p,auth:K,proxy:R,hooks:F}),Q=(0,upgrade_1.createUpgrade)({rt:f,auth:K,proxy:R,hooks:F}),X=p.startEdgeFlusher(),Z=b.startSweeper();await C.bootSingletonsAndApps();for(const e of T)e.decl.eager&&b.ensureUp(e);let P,O,$;if(y){const e=require("../../../basic-ssl"),o=(0,service_keys_1.gatewayCacheDir)(t.root);$=n(()=>e.getCertificateSync(o,"gateway",f.routes.map(i=>i.host).filter(i=>!!i)),"mintCert"),P=$(),O=e.caCertPath()}const x=[],j=[];for(const e of g){const o=P?require("node:http2").createSecureServer({key:P,cert:P,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512},(i,s)=>H(i,s)):http.createServer(H);if(o.on("upgrade",Q),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 d=i,c=classifyClientError(d.code,d.message);if(c.report&&Y(c.cause)){const V=s.remoteAddress??"unknown",D=c.status===null?"dropped":String(c.status);console.warn(`[gateway] inbound H1 clientError from ${V}: ${c.cause} (${d.code??d.message}${d.bytesParsed!==void 0?`, bytesParsed=${d.bytesParsed}`:""}) \u2192 ${D}. This status is the GATEWAY's, not the upstream service's.`),p.emit("warning",`inbound clientError: ${c.cause} \u2192 ${D}`,`gateway-client-error:${c.cause}`,{kind:"gateway-client-error",cause:c.cause,code:d.code??null,status:c.status,peer:V,bytesParsed:d.bytesParsed??null})}c.status!==null&&s.writable&&s.end(`HTTP/1.1 ${c.status} ${CLIENT_ERROR_REASON[c.status]}\r
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
2
2
  \r
3
- `),s.destroy()}),o.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,o.headersTimeout<types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS+5e3&&(o.headersTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS+5e3),P!=null){const i=o;i.on("session",s=>{s.on("error",d=>{const c=d.code??d.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()})}),x.push(o)}$&&(I=n(()=>{const e=$();for(const o of x)o.setSecureContext?.({key:e,cert:e})},"refreshCert")),console.log("[gateway] Routes:");for(const e of k){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.`),p.emit("info",`gateway up: ${k.length} routes on ${j.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:j,routes:k.map(e=>({host:e.host??"(default)",project:e.project,mode:e.pool?"pods":"tcp",internal:!!e.internal,prependPrefix:e.prependPrefix}))}),{ports:j,routes:k,services:T,stop:n(async()=>{if(!a){a=!0,clearInterval(X),clearInterval(Z),p.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),G.stop(),W?.close(),(0,workspace_registry_1.unregisterWorkspace)(v);for(const e of T)b.killSpawned(e,"gateway shutdown");await C.reapAll(),await Promise.all(x.map(e=>new Promise(o=>e.close(()=>o())))),R.closeProxy(),R.destroyAgents()}},"stop")}}_(startGateway,"startGateway"),n(startGateway,"startGateway");
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");
@@ -1,10 +1,51 @@
1
1
  import type { ServiceCtl } from './types';
2
2
  /** The gateway's on-disk cache root — shared by service keys AND the TLS cert cache. */
3
3
  export declare function gatewayCacheDir(root: string): string;
4
+ /**
5
+ * The RUNTIME key for `rt.serviceKeys` — a workspace-qualified project id.
6
+ *
7
+ * One daemon serves many workspaces, and project names are NOT unique across them: the two
8
+ * workspaces on the machine this was found on share five (`auth`, `plan`, `iam`,
9
+ * `knowledge`, `channels`). Keying the runtime map by bare project name therefore let a
10
+ * guest's `auth` key overwrite the host's the moment it registered — handing the host's
11
+ * pods a foreign `LENSMCP_SERVICE_KEY`, mis-attributing their east-west calls to the
12
+ * guest's service via `x-api-key-id`, and making the adoption check recycle perfectly
13
+ * healthy host pods on a fingerprint that was never theirs.
14
+ *
15
+ * The ON-DISK file stays keyed by bare project name. It already lives under a single
16
+ * workspace's root, so it cannot collide there — and re-keying it would regenerate every
17
+ * key and lock out every pod currently running with the old one.
18
+ */
19
+ export declare function serviceKeyId(wsKey: string | undefined, project: string): string;
4
20
  /** Load (or generate + persist) a stable key per pod service, plus the inverse
5
21
  * key→project map used to authenticate internal callers. Tolerant of a
6
22
  * read-only fs (keys then live only for this process). */
7
- export declare function loadServiceKeys(root: string, services: ServiceCtl[]): {
23
+ export declare function loadServiceKeys(root: string, services: ServiceCtl[], wsKey?: string): {
8
24
  serviceKeys: Record<string, string>;
9
25
  keyToProject: Map<string, string>;
26
+ keyToWorkspace: Map<string, string>;
27
+ byProject: Record<string, string>;
28
+ };
29
+ /**
30
+ * Mint + adopt keys for a workspace that JOINS a running daemon.
31
+ *
32
+ * `loadServiceKeys` runs exactly once, at gateway boot, over the DAEMON workspace's own
33
+ * root and services. A guest workspace arrives later, through `POST /register` — and
34
+ * nothing extended the key maps for it. So `eastWestEnv` found no key for a guest
35
+ * project and started its pods with no `LENSMCP_SERVICE_KEY`, while the gateway injected
36
+ * no `x-internal-token`; every `internal.<host>` call from that workspace then failed the
37
+ * `x-api-key` check with a 401, which the caller's membership gateway classified as
38
+ * transient and retried forever. That is the `auth.unavailable` 503 storm on
39
+ * `/token/tenant` filed as "adopted pods keep the old daemon's keys" — the keys were
40
+ * never stale, the guest simply never had any.
41
+ *
42
+ * Keys are read from and written to the GUEST's own cache dir, so they are stable for
43
+ * that workspace across daemon restarts and across which daemon happens to host it.
44
+ */
45
+ export declare function adoptServiceKeys(target: {
46
+ serviceKeys: Record<string, string>;
47
+ keyToProject: Map<string, string>;
48
+ keyToWorkspace?: Map<string, string>;
49
+ }, root: string, services: ServiceCtl[], wsKey?: string): {
50
+ added: string[];
10
51
  };
@@ -1 +1 @@
1
- "use strict";var d=Object.defineProperty;var i=(e,t)=>d(e,"name",{value:t,configurable:!0});var u=Object.defineProperty,c=i((e,t)=>u(e,"name",{value:t,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.gatewayCacheDir=gatewayCacheDir,exports.loadServiceKeys=loadServiceKeys;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));function gatewayCacheDir(e){return path.join(e,"node_modules",".cache","davnx-webpack")}i(gatewayCacheDir,"gatewayCacheDir"),c(gatewayCacheDir,"gatewayCacheDir");function loadServiceKeys(e,t){const a=gatewayCacheDir(e),n=path.join(a,"service-keys.json");let r={};try{r=JSON.parse(fs.readFileSync(n,"utf8"))}catch{}let s=!1;for(const o of t)r[o.project]||(r[o.project]=(0,node_crypto_1.randomBytes)(16).toString("hex"),s=!0);if(s)try{fs.mkdirSync(a,{recursive:!0}),fs.writeFileSync(n,JSON.stringify(r,null,2))}catch{}const y=new Map(Object.entries(r).map(([o,p])=>[p,o]));return{serviceKeys:r,keyToProject:y}}i(loadServiceKeys,"loadServiceKeys"),c(loadServiceKeys,"loadServiceKeys");
1
+ "use strict";var l=Object.defineProperty;var i=(e,r)=>l(e,"name",{value:r,configurable:!0});var j=Object.defineProperty,p=i((e,r)=>j(e,"name",{value:r,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.gatewayCacheDir=gatewayCacheDir,exports.serviceKeyId=serviceKeyId,exports.loadServiceKeys=loadServiceKeys,exports.adoptServiceKeys=adoptServiceKeys;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));function gatewayCacheDir(e){return path.join(e,"node_modules",".cache","davnx-webpack")}i(gatewayCacheDir,"gatewayCacheDir"),p(gatewayCacheDir,"gatewayCacheDir");function serviceKeyId(e,r){return`${e??""} ${r}`}i(serviceKeyId,"serviceKeyId"),p(serviceKeyId,"serviceKeyId");function loadServiceKeys(e,r,a){const u=gatewayCacheDir(e),n=path.join(u,"service-keys.json");let t={};try{t=JSON.parse(fs.readFileSync(n,"utf8"))}catch{}let y=!1;for(const c of r)t[c.project]||(t[c.project]=(0,node_crypto_1.randomBytes)(16).toString("hex"),y=!0);if(y)try{fs.mkdirSync(u,{recursive:!0}),fs.writeFileSync(n,JSON.stringify(t,null,2))}catch{}const o=a??r[0]?.wsKey,d={};for(const[c,v]of Object.entries(t))d[serviceKeyId(o,c)]=v;const s=new Map(Object.entries(t).map(([c,v])=>[v,c])),f=new Map(o?Object.values(t).map(c=>[c,o]):[]);return{serviceKeys:d,keyToProject:s,keyToWorkspace:f,byProject:t}}i(loadServiceKeys,"loadServiceKeys"),p(loadServiceKeys,"loadServiceKeys");function adoptServiceKeys(e,r,a,u){const n=u??a[0]?.wsKey,{byProject:t}=loadServiceKeys(r,a,n),y=[];for(const o of a){const d=serviceKeyId(o.wsKey??n,o.project),s=t[o.project];if(!s||e.serviceKeys[d]===s)continue;e.serviceKeys[d]=s,e.keyToProject.set(s,o.project);const f=o.wsKey??n;f&&e.keyToWorkspace?.set(s,f),y.push(o.project)}return{added:y}}i(adoptServiceKeys,"adoptServiceKeys"),p(adoptServiceKeys,"adoptServiceKeys");
@@ -6,6 +6,7 @@
6
6
  * only place that wires concrete layers together.
7
7
  */
8
8
  import type { ChildProcess } from 'node:child_process';
9
+ import type { ClientIpResolver } from '../client-ip';
9
10
  import type * as http from 'node:http';
10
11
  import type * as stream from 'node:stream';
11
12
  import type { GatewayExecutorSchema } from '../schema';
@@ -63,6 +64,16 @@ export interface ClusterProjectDecl {
63
64
  serveTarget?: string;
64
65
  /** Devserver admin endpoint for scaling (default from port: http(s)://localhost:<port>). */
65
66
  adminUrl?: string;
67
+ /**
68
+ * How long THIS service's pods may take to send the first response byte before the
69
+ * proxy treats the socket as wedged. Defaults to {@link POD_RESPONSE_TIMEOUT_MS}.
70
+ *
71
+ * Raise it for a service with a legitimately slow synchronous route (a fold over
72
+ * documents behind a model call — foodguard's `…/extract` measured 44s on six
73
+ * documents, against a 30s default). The pod is probed for liveness before eviction
74
+ * either way, so this is a tuning knob, not the safety mechanism.
75
+ */
76
+ firstByteMs?: number;
66
77
  /**
67
78
  * FRONTEND apps only: run this app UNDER the lens. When true, the gateway
68
79
  * SPAWNS the app's own Vite dev server with `@lensmcp/vite-plugin`
@@ -122,6 +133,14 @@ export interface SourceSetChange {
122
133
  removed: string[];
123
134
  /** More paths changed than the diagnostic payload retains. */
124
135
  truncated: boolean;
136
+ /**
137
+ * How far back a watcher stamp may lie and still vouch for THIS change. The scan stamps `changedAt` at its
138
+ * COMPLETION, but the fs event happened anywhere after the PREVIOUS scan started — so the window is
139
+ * "now − previous scan start" (+ the settle margin), not a fixed fraction of the scan period. The old
140
+ * fixed `min(scanPeriod, 15s) + settle` = 23s against a 30s period falsely read a heard add as `missed`
141
+ * whenever the event fell in the first 7+ seconds of the period (issues/dev-gateway/03).
142
+ */
143
+ windowMs?: number;
125
144
  }
126
145
  export interface LensWatcherEvent {
127
146
  sequence: number;
@@ -138,6 +157,30 @@ export interface LensWatcherHealth {
138
157
  fsSetEvents: number;
139
158
  recentEvents: LensWatcherEvent[];
140
159
  }
160
+ /**
161
+ * What a POD's webpack watcher has been doing — the pod-side twin of {@link LensWatcherHealth}. Stamped by
162
+ * `DevServerReloadPlugin` (`create-webpack-dev.ts`) on every `watchRun` / `done` / `failed` / `watchClose`,
163
+ * relayed by the devserver parent to its children over IPC, served in the child's `/__lensmcp/alive`, and
164
+ * read by the pod sweeper (`lifecycle.ts` `podWatcherVerdict`) before a `stale-source-set` recycle.
165
+ *
166
+ * webpack cannot testify per PATH for an add the way chokidar can (watchpack only reports the files and
167
+ * context dirs the compilation asked for), so this is a proof of LIFE, not of hearing: a watcher that woke
168
+ * for anything inside the window is alive, and a live watchpack cannot be wedged by an add — it already
169
+ * watches every `missingDependency`, so the file an importer was waiting for triggers its own rebuild.
170
+ */
171
+ export interface PodWatcherTestimony {
172
+ schemaVersion: 1;
173
+ /** Last `watchRun` (the watcher woke for a change). 0 = never this generation. */
174
+ lastWatchRunAt: number;
175
+ /** Last successful `done`. 0 = never this generation. */
176
+ lastBuildAt: number;
177
+ buildsSinceSpawn: number;
178
+ /** A watcher error / `watchClose` / `failed` — its fs view IS stale from here on. The string names why. */
179
+ degraded: false | string;
180
+ /** Bounded, newest-last absolute paths of the last few `watchRun`s (diagnostics only). */
181
+ recentPaths: string[];
182
+ }
183
+ export type PodWatcherVerdict = 'heard' | 'silent' | 'degraded' | 'unknown';
141
184
  export interface LensFrontendRecoveryStatus {
142
185
  state: 'starting' | 'ready' | 'replacing' | 'failed';
143
186
  generation: number;
@@ -164,6 +207,19 @@ export interface ServiceCtl {
164
207
  state: 'down' | 'starting' | 'up';
165
208
  lastUsed: number;
166
209
  inflight: number;
210
+ /** In-process background work the pod reports (queue jobs). Refreshed on the sweep from
211
+ * `/__lensmcp/alive`; `inflight` alone cannot see a pod busy only with queue work. */
212
+ activeWork?: number;
213
+ /** The pod's webpack-watcher testimony from its last `/__lensmcp/alive` probe ({@link PodWatcherTestimony}). */
214
+ watcher?: PodWatcherTestimony;
215
+ /** The `sourceSetChangedAt` this pod's watcher last vouched for — the sweeper does not re-ask. */
216
+ staleVerifiedAt?: number;
217
+ /** The `sourceSetChangedAt` the sweeper last logged a "kept" verdict for — one line per change, not per tick. */
218
+ staleKeptAt?: number;
219
+ /** A rolling recycle in flight ({@link POD_RECYCLE_MODE}): the standby generation's promise. */
220
+ rolling?: Promise<boolean>;
221
+ /** The pod GENERATION the current `proc` was spawned as (`LENSMCP_POD_GENERATION`, echoed in `/__lensmcp/alive`). */
222
+ generation?: number;
167
223
  lastScaleAt: number;
168
224
  spawnAt?: number;
169
225
  lastErrors?: string[];
@@ -214,6 +270,30 @@ export declare const UPSTREAM_HEAL_STEP_MS = 300;
214
270
  * Only fires BEFORE any response byte (a legit long request streams AFTER headers, unaffected).
215
271
  * Override with `LENSMCP_POD_RESPONSE_TIMEOUT_MS`. */
216
272
  export declare const POD_RESPONSE_TIMEOUT_MS: number;
273
+ /**
274
+ * The ceiling on a pod that is SLOW but demonstrably ALIVE.
275
+ *
276
+ * {@link POD_RESPONSE_TIMEOUT_MS} cannot tell a wedged socket from an endpoint that is
277
+ * legitimately thinking: both send no headers. It assumed wedged, so any route doing a
278
+ * long synchronous call before writing headers was 502'd *and had its pod evicted* —
279
+ * reproducible on every `POST …/extract` in foodguard, where a six-document fold behind
280
+ * a model call measured 44s. The eviction then reads in `gateway.log` as
281
+ * `Swapped to fresh app`, indistinguishable from an HMR restart, which is what made it
282
+ * cost two golden-set runs to diagnose (2026-08-16).
283
+ *
284
+ * The fix is evidence: on the deadline the proxy asks the pod's own
285
+ * `GET /__lensmcp/alive` (served by the devserver's raw HTTP server, ahead of the app
286
+ * handler, so it answers while the app is busy on I/O). A pod that answers is BUSY, not
287
+ * wedged — keep waiting, up to this ceiling. A pod that stays silent is evicted exactly
288
+ * as before. Per-service override: `cluster.firstByteMs` in project.json.
289
+ * Override with `LENSMCP_POD_SLOW_FIRST_BYTE_MS`; `0` restores evict-on-deadline.
290
+ */
291
+ export declare const POD_SLOW_FIRST_BYTE_MS: number;
292
+ /** Per-probe timeout for `GET /__lensmcp/alive`. A live raw server answers a loopback
293
+ * unix-socket request in microseconds, so anything slower is indistinguishable from dead. */
294
+ export declare const POD_ALIVE_PROBE_TIMEOUT_MS: number;
295
+ /** Internal liveness route served by the devserver child itself (`main.devserver.ts`). */
296
+ export declare const POD_ALIVE_PATH = "/__lensmcp/alive";
217
297
  /**
218
298
  * Self-heal: how long a service may sit WEDGED — its gateway-spawned parent process ALIVE but with ZERO
219
299
  * live pods (an HMR hot-swap evicted the child's socket, and `ensureUp` won't respawn because its
@@ -279,6 +359,62 @@ export declare const LENS_PROBE_ENABLED: boolean;
279
359
  * recycles exactly as before; a pod without the endpoint (older plugin) keeps the legacy kill-first path.
280
360
  * Set `LENSMCP_LENS_WATCHER_PROBE=0` to restore kill-first unconditionally. */
281
361
  export declare const LENS_WATCHER_PROBE_ENABLED: boolean;
362
+ /**
363
+ * What to do when the watcher can NEITHER confirm nor deny — an unreachable probe, a pod
364
+ * with no `/__lensmcp/watcher-health` (an older `@lensmcp/vite-plugin`), or the probe
365
+ * switched off.
366
+ *
367
+ * `recycle` (default) keeps the historical behaviour: no proof of innocence ⇒ kill, on
368
+ * the reasoning that a false recycle costs a cold start while a MISSED change costs a
369
+ * wedged dev server. That is the right default, but it is not right for everyone: a
370
+ * workspace where the probe is structurally unavailable pays the full "every add
371
+ * reloads every tab" tax, which is what drove foodguard to `LENSMCP_POD_RECYCLE=0` —
372
+ * disabling the ZOMBIE heal wholesale, including the cases it is genuinely needed for.
373
+ * `keep` is the narrower instrument: act only on a watcher that demonstrably MISSED the
374
+ * change, and leave unverifiable pods alone.
375
+ * Set `LENSMCP_LENS_WATCHER_UNVERIFIED=keep`.
376
+ */
377
+ export declare const LENS_WATCHER_UNVERIFIED: 'recycle' | 'keep';
378
+ /**
379
+ * Hand each lens vite its SOURCE SCOPE (`LENSMCP_WATCH_DIRS`) so `@lensmcp/vite-plugin` can add those
380
+ * roots to Vite's chokidar. Vite watches only `config.root`; a file outside it is watched one-at-a-time
381
+ * as it is imported, so a NEW file in `shared/` or `web/libs/` never produced an `add` event, the
382
+ * watcher-health endpoint never stamped it, and the verdict above was `missed` BY CONSTRUCTION for every
383
+ * out-of-root add — 16 of 16 rolling handoffs in one foodguard day (issues/dev-gateway/02). With the scope
384
+ * watched, the add is heard, the verdict is `heard`, and Vite's own create-handling works for those files
385
+ * too. Set `LENSMCP_LENS_WATCH_SCOPE=0` to hand nothing over (inotify-constrained Linux hosts).
386
+ */
387
+ export declare const LENS_WATCH_SCOPE_ENABLED: boolean;
388
+ /**
389
+ * POD watcher testimony ({@link PodWatcherTestimony}) — the pod-side twin of the two lens knobs above.
390
+ * The stale-source-set signal scaled EVERY pod to zero on EVERY source-set change in its scope — 83 cold
391
+ * recycles of six services in one foodguard day, each a ~30-60s API outage the app rendered as "not
392
+ * connected" (issues/dev-gateway/07). Now the sweeper reads the pod's testimony first.
393
+ *
394
+ * `LENSMCP_POD_WATCHER_UNVERIFIED` — no testimony at all (a pod on an older `@lensmcp/cluster` bundle,
395
+ * or an unreachable probe). `recycle` (default) is today's behaviour for those pods; `keep` leaves them.
396
+ */
397
+ export declare const POD_WATCHER_UNVERIFIED: 'recycle' | 'keep';
398
+ /**
399
+ * `LENSMCP_POD_WATCHER_SILENT` — testimony present, watcher healthy (no error, not closed), but no
400
+ * `watchRun`/build inside the change window. `keep` (default): a live watchpack already watches every
401
+ * `missingDependency`, so an add cannot wedge it — only a DEAD watcher can, and a dead one reports
402
+ * `degraded` (recycled regardless of this knob) or stops building forever (the `max-age` backstop).
403
+ * `recycle` restores kill-on-no-evidence for workspaces that have seen silent watcher death.
404
+ */
405
+ export declare const POD_WATCHER_SILENT: 'recycle' | 'keep';
406
+ /**
407
+ * HOW a pod is recycled (stale-source-set / max-age) — issues/dev-gateway/09. `rolling` (default): spawn a
408
+ * STANDBY `serve-hmr` generation for the same service, wait until one of its pods answers
409
+ * `/__lensmcp/alive` with the new generation number, then retire the old tree — the pool dir is shared, the
410
+ * devserver's bind-steal hands each socket path to the newer pod, and the gateway's rotation never runs
411
+ * dry. `in-place` restores kill-first (a cold outage per recycle). Set `LENSMCP_POD_RECYCLE_MODE`.
412
+ */
413
+ export declare const POD_RECYCLE_MODE: 'rolling' | 'in-place';
414
+ /** How long a standby pod generation may take to answer alive before the roll is abandoned and the old
415
+ * generation kept (a cold nx + webpack compile of a large service can take minutes). Override
416
+ * `LENSMCP_POD_ROLL_READY_MS`. Default 3 min. */
417
+ export declare const POD_ROLL_READY_MS: number;
282
418
  /** Spawn grace: a child is never probed (and so never recycled) until it has had this long to BIND its port
283
419
  * — a cold vite/nx boot in a big monorepo takes many seconds, and killing a still-booting dev server would
284
420
  * be a self-inflicted outage. Override `LENSMCP_LENS_PROBE_GRACE_MS`. Default 45s. */
@@ -371,6 +507,35 @@ export declare const LENS_REQUEST_RECOVERY_WAIT_MS: number;
371
507
  * `classifyClientError`). Node's server default is 5s and a Node client agent's is ~5s, i.e. exactly tied.
372
508
  * Override `LENSMCP_KEEPALIVE_TIMEOUT_MS`. Default 76s. */
373
509
  export declare const GATEWAY_KEEPALIVE_TIMEOUT_MS: number;
510
+ /**
511
+ * The HEADER-phase bound, INDEPENDENT of `keepAliveTimeout` (issues/dev-gateway/12, the prod-gateway-2 #14
512
+ * twin). The old derivation (`keepAliveTimeout + 5s` = 81s) rested on a premise verified obsolete on Node
513
+ * 24: an idle keep-alive socket is NOT closed by a shorter `headersTimeout` — only a connection that has
514
+ * begun a request and stalled mid-headers is. So a partial-header hold was 81s for no reason; Node's own
515
+ * 60s is the default now. Override `LENSMCP_HEADERS_TIMEOUT_MS`.
516
+ */
517
+ export declare const GATEWAY_HEADERS_TIMEOUT_MS: number;
518
+ /**
519
+ * How long `stop()` lets an IN-FLIGHT request finish before the front door's remaining connections are
520
+ * closed outright. `server.close()` alone resolves only once every connection has ended — and with a
521
+ * 76s keep-alive window an IDLE browser socket held a `gateway stop`/`restart` open for over a minute
522
+ * (measured: a suite of five gateways took ~4 min to tear down, all of it waiting on idle sockets).
523
+ * Idle connections are closed immediately; this bounds the busy ones. Override `LENSMCP_STOP_GRACE_MS`.
524
+ */
525
+ export declare const GATEWAY_STOP_GRACE_MS: number;
526
+ /** `server.maxConnections`. 0 (default) = unlimited. Override `LENSMCP_MAX_CONNECTIONS`. */
527
+ export declare const GATEWAY_MAX_CONNECTIONS: number;
528
+ /** How often Node sweeps its connection timers (its 30s default means a 60s bound fires between 60s and
529
+ * 90s). 0 (default) keeps Node's value. Override `LENSMCP_CONN_CHECK_MS`. */
530
+ export declare const GATEWAY_CONN_CHECK_MS: number;
531
+ /**
532
+ * The transfer STALL bound (issues/dev-gateway/12, the prod-gateway-2 #06 / round-1 #10 twins): a request
533
+ * body or a response that has moved NOTHING for this long is cut, in either direction. A slow-but-moving
534
+ * upload, a draining download and an idle SSE stream (nothing queued) are never candidates. Dev streams
535
+ * long (MCP streamable-HTTP, SSE), so this is a stall bound, never a duration bound. `0` disables.
536
+ * Override `LENSMCP_BODY_STALL_MS`. Default 60s.
537
+ */
538
+ export declare const BODY_STALL_MS: number;
374
539
  /** Test-tunable knobs on top of the user-facing schema. */
375
540
  export interface GatewayRuntimeOptions extends GatewayExecutorSchema {
376
541
  /** Traffic-edge flush period (ms). Default 5000. */
@@ -381,6 +546,20 @@ export interface GatewayRuntimeOptions extends GatewayExecutorSchema {
381
546
  scanTtlMs?: number;
382
547
  /** Cold-start wait budget (ms). Default 120000. */
383
548
  startupTimeoutMs?: number;
549
+ /** Trusted `X-Forwarded-For` hop count (default `LENSMCP_TRUST_PROXY`, else 0 — the socket peer is the client). */
550
+ trustProxyHops?: number;
551
+ /** A front proxy's client-address header, honoured from trusted peers only (`LENSMCP_CLIENT_IP_HEADER`). */
552
+ clientIpHeader?: string;
553
+ /** CIDRs that may assert `clientIpHeader` (`LENSMCP_CLIENT_IP_TRUSTED_PROXIES`; default loopback). */
554
+ clientIpTrustedProxies?: string[];
555
+ /** Bound on the HEADER phase of a request (`LENSMCP_HEADERS_TIMEOUT_MS`, default 60s — Node's own). */
556
+ headersTimeoutMs?: number;
557
+ /** `server.maxConnections` (`LENSMCP_MAX_CONNECTIONS`; default unlimited). */
558
+ maxConnections?: number;
559
+ /** How often Node sweeps its connection timers (`LENSMCP_CONN_CHECK_MS`; default Node's 30s). */
560
+ connectionsCheckingIntervalMs?: number;
561
+ /** Abort a transfer that has moved NOTHING for this long, either direction (`LENSMCP_BODY_STALL_MS`, default 60s; 0 off). */
562
+ bodyStallMs?: number;
384
563
  /** Customer-supplied lifecycle hooks layered on top of the built-in pipeline. */
385
564
  hooks?: GatewayHooks;
386
565
  }
@@ -390,6 +569,9 @@ export interface GatewayHandle {
390
569
  routes: Route[];
391
570
  services: ServiceCtl[];
392
571
  stop: () => Promise<void>;
572
+ /** The lens-frontend supervisor (rolling handoffs) — `recover(route.lens, reason)` rolls an app on demand.
573
+ * Present when the gateway runs in `rolling` mode; what an operator command or an e2e drives. */
574
+ lensSupervisor?: LensFrontendSupervisor;
393
575
  }
394
576
  export interface GwTrace {
395
577
  startedAt: number;
@@ -433,7 +615,17 @@ export interface GatewayRuntime {
433
615
  readonly rebuildRoutes: () => void;
434
616
  readonly services: ServiceCtl[];
435
617
  readonly serviceKeys: Record<string, string>;
436
- readonly keyToProject: ReadonlyMap<string, string>;
618
+ /** key → project, for authenticating an internal caller's `x-api-key`. Mutable: a
619
+ * workspace that joins a running daemon via `POST /register` adopts its keys here. */
620
+ readonly keyToProject: Map<string, string>;
621
+ /** key → owning workspace slug, the twin of `keyToProject` — so an internal caller can be attributed to
622
+ * `<wsKey>/<project>`, not just a bare project name (issues/dev-gateway/13). Mutable like its twin. */
623
+ readonly keyToWorkspace: Map<string, string>;
624
+ /** The client-address resolver (shared with prod — `../client-ip`), built from options/env at boot. Optional
625
+ * only so a partial runtime in a spec still works: the edge helpers fall back to a peer-only resolver. */
626
+ readonly clientIp?: ClientIpResolver;
627
+ /** The transfer stall bound the proxy arms (options/env; see `BODY_STALL_MS`). */
628
+ readonly bodyStallMs?: number;
437
629
  readonly scanTtlMs: number;
438
630
  readonly startupTimeoutMs: number;
439
631
  readonly sweepMs: number;
@@ -1 +1 @@
1
- "use strict";var O=Object.defineProperty;var L=(E,_)=>O(E,"name",{value:_,configurable:!0});var M=Object.defineProperty,R=L((E,_)=>M(E,"name",{value:_,configurable:!0}),"R");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=exports.LENS_REQUEST_RECOVERY_WAIT_MS=exports.LENS_REPLACEMENT_DRAIN_MS=exports.LENS_REPLACEMENT_READY_MS=exports.LENS_RESTART_MODE=exports.LENS_RECYCLE_KILL_GRACE_MS=exports.LENS_PARKED_PORT_REPROBE_MS=exports.MAX_PORT_CONFLICT_RETRIES=exports.CHILD_START_FAIL_MS=exports.PORT_FREE_POLL_MS=exports.PORT_FREE_WAIT_MS=exports.LENS_PROBE_BOOT_GRACE_MS=exports.LENS_PROBE_COOLDOWN_MS=exports.LENS_PROBE_TIMEOUT_MS=exports.LENS_PROBE_FAILS=exports.LENS_PROBE_GRACE_MS=exports.LENS_WATCHER_PROBE_ENABLED=exports.LENS_PROBE_ENABLED=exports.POD_STALE_RECYCLE_COOLDOWN_MS=exports.POD_STALE_SCAN_MS=exports.POD_RECYCLE_SETTLE_MS=exports.POD_STALE_GRACE_MS=exports.POD_MAX_AGE_MS=exports.POD_RECYCLE_ENABLED=exports.WEDGE_RECYCLE_GRACE_MS=exports.POD_RESPONSE_TIMEOUT_MS=exports.UPSTREAM_HEAL_STEP_MS=exports.UPSTREAM_HEAL_WINDOW_MS=exports.SERVICE_RESTART_WINDOW_MS=exports.SERVICE_MAX_RESTARTS=exports.SERVICE_ERROR_THROTTLE_MS=void 0;function envInt(E,_,e=!1){const S=Number(process.env[E]);return!Number.isFinite(S)||S<0?_:S===0?e?0:_:S}L(envInt,"envInt"),R(envInt,"envInt"),exports.SERVICE_ERROR_THROTTLE_MS=5e3,exports.SERVICE_MAX_RESTARTS=5,exports.SERVICE_RESTART_WINDOW_MS=6e4,exports.UPSTREAM_HEAL_WINDOW_MS=8e3,exports.UPSTREAM_HEAL_STEP_MS=300,exports.POD_RESPONSE_TIMEOUT_MS=(()=>{const E=Number(process.env.LENSMCP_POD_RESPONSE_TIMEOUT_MS);return Number.isFinite(E)&&E>0?E:3e4})(),exports.WEDGE_RECYCLE_GRACE_MS=(()=>{const E=Number(process.env.LENSMCP_WEDGE_RECYCLE_GRACE_MS);return Number.isFinite(E)&&E>0?E:15e3})(),exports.POD_RECYCLE_ENABLED=process.env.LENSMCP_POD_RECYCLE!=="0",exports.POD_MAX_AGE_MS=envInt("LENSMCP_POD_MAX_AGE_MS",240*6e4,!0),exports.POD_STALE_GRACE_MS=envInt("LENSMCP_POD_STALE_GRACE_MS",1e4),exports.POD_RECYCLE_SETTLE_MS=envInt("LENSMCP_POD_RECYCLE_SETTLE_MS",8e3),exports.POD_STALE_SCAN_MS=envInt("LENSMCP_POD_STALE_SCAN_MS",3e4),exports.POD_STALE_RECYCLE_COOLDOWN_MS=envInt("LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS",6e4),exports.LENS_PROBE_ENABLED=process.env.LENSMCP_LENS_PROBE!=="0",exports.LENS_WATCHER_PROBE_ENABLED=process.env.LENSMCP_LENS_WATCHER_PROBE!=="0",exports.LENS_PROBE_GRACE_MS=envInt("LENSMCP_LENS_PROBE_GRACE_MS",45e3),exports.LENS_PROBE_FAILS=envInt("LENSMCP_LENS_PROBE_FAILS",2),exports.LENS_PROBE_TIMEOUT_MS=envInt("LENSMCP_LENS_PROBE_TIMEOUT_MS",1e3),exports.LENS_PROBE_COOLDOWN_MS=envInt("LENSMCP_LENS_PROBE_COOLDOWN_MS",3e4),exports.LENS_PROBE_BOOT_GRACE_MS=envInt("LENSMCP_LENS_PROBE_BOOT_GRACE_MS",24e4),exports.PORT_FREE_WAIT_MS=envInt("LENSMCP_PORT_FREE_WAIT_MS",1e4),exports.PORT_FREE_POLL_MS=envInt("LENSMCP_PORT_FREE_POLL_MS",250),exports.CHILD_START_FAIL_MS=envInt("LENSMCP_CHILD_START_FAIL_MS",1e4),exports.MAX_PORT_CONFLICT_RETRIES=envInt("LENSMCP_MAX_PORT_CONFLICT_RETRIES",5),exports.LENS_PARKED_PORT_REPROBE_MS=envInt("LENSMCP_PARKED_PORT_REPROBE_MS",3e5),exports.LENS_RECYCLE_KILL_GRACE_MS=envInt("LENSMCP_RECYCLE_KILL_GRACE_MS",5e3),exports.LENS_RESTART_MODE=process.env.LENSMCP_LENS_RESTART_MODE==="in-place"?"in-place":"rolling",exports.LENS_REPLACEMENT_READY_MS=envInt("LENSMCP_LENS_REPLACEMENT_READY_MS",3e4),exports.LENS_REPLACEMENT_DRAIN_MS=envInt("LENSMCP_LENS_REPLACEMENT_DRAIN_MS",2e3,!0),exports.LENS_REQUEST_RECOVERY_WAIT_MS=envInt("LENSMCP_LENS_REQUEST_RECOVERY_WAIT_MS",15e3),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=envInt("LENSMCP_KEEPALIVE_TIMEOUT_MS",76e3);
1
+ "use strict";var O=Object.defineProperty;var e=(_,E)=>O(_,"name",{value:E,configurable:!0});var M=Object.defineProperty,L=e((_,E)=>M(_,"name",{value:E,configurable:!0}),"L");Object.defineProperty(exports,"__esModule",{value:!0}),exports.BODY_STALL_MS=exports.GATEWAY_CONN_CHECK_MS=exports.GATEWAY_MAX_CONNECTIONS=exports.GATEWAY_STOP_GRACE_MS=exports.GATEWAY_HEADERS_TIMEOUT_MS=exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=exports.LENS_REQUEST_RECOVERY_WAIT_MS=exports.LENS_REPLACEMENT_DRAIN_MS=exports.LENS_REPLACEMENT_READY_MS=exports.LENS_RESTART_MODE=exports.LENS_RECYCLE_KILL_GRACE_MS=exports.LENS_PARKED_PORT_REPROBE_MS=exports.MAX_PORT_CONFLICT_RETRIES=exports.CHILD_START_FAIL_MS=exports.PORT_FREE_POLL_MS=exports.PORT_FREE_WAIT_MS=exports.LENS_PROBE_BOOT_GRACE_MS=exports.LENS_PROBE_COOLDOWN_MS=exports.LENS_PROBE_TIMEOUT_MS=exports.LENS_PROBE_FAILS=exports.LENS_PROBE_GRACE_MS=exports.POD_ROLL_READY_MS=exports.POD_RECYCLE_MODE=exports.POD_WATCHER_SILENT=exports.POD_WATCHER_UNVERIFIED=exports.LENS_WATCH_SCOPE_ENABLED=exports.LENS_WATCHER_UNVERIFIED=exports.LENS_WATCHER_PROBE_ENABLED=exports.LENS_PROBE_ENABLED=exports.POD_STALE_RECYCLE_COOLDOWN_MS=exports.POD_STALE_SCAN_MS=exports.POD_RECYCLE_SETTLE_MS=exports.POD_STALE_GRACE_MS=exports.POD_MAX_AGE_MS=exports.POD_RECYCLE_ENABLED=exports.WEDGE_RECYCLE_GRACE_MS=exports.POD_ALIVE_PATH=exports.POD_ALIVE_PROBE_TIMEOUT_MS=exports.POD_SLOW_FIRST_BYTE_MS=exports.POD_RESPONSE_TIMEOUT_MS=exports.UPSTREAM_HEAL_STEP_MS=exports.UPSTREAM_HEAL_WINDOW_MS=exports.SERVICE_RESTART_WINDOW_MS=exports.SERVICE_MAX_RESTARTS=exports.SERVICE_ERROR_THROTTLE_MS=void 0;function envInt(_,E,R=!1){const S=Number(process.env[_]);return!Number.isFinite(S)||S<0?E:S===0?R?0:E:S}e(envInt,"envInt"),L(envInt,"envInt"),exports.SERVICE_ERROR_THROTTLE_MS=5e3,exports.SERVICE_MAX_RESTARTS=5,exports.SERVICE_RESTART_WINDOW_MS=6e4,exports.UPSTREAM_HEAL_WINDOW_MS=8e3,exports.UPSTREAM_HEAL_STEP_MS=300,exports.POD_RESPONSE_TIMEOUT_MS=(()=>{const _=Number(process.env.LENSMCP_POD_RESPONSE_TIMEOUT_MS);return Number.isFinite(_)&&_>0?_:3e4})(),exports.POD_SLOW_FIRST_BYTE_MS=envInt("LENSMCP_POD_SLOW_FIRST_BYTE_MS",10*6e4,!0),exports.POD_ALIVE_PROBE_TIMEOUT_MS=envInt("LENSMCP_POD_ALIVE_PROBE_TIMEOUT_MS",1e3),exports.POD_ALIVE_PATH="/__lensmcp/alive",exports.WEDGE_RECYCLE_GRACE_MS=(()=>{const _=Number(process.env.LENSMCP_WEDGE_RECYCLE_GRACE_MS);return Number.isFinite(_)&&_>0?_:15e3})(),exports.POD_RECYCLE_ENABLED=process.env.LENSMCP_POD_RECYCLE!=="0",exports.POD_MAX_AGE_MS=envInt("LENSMCP_POD_MAX_AGE_MS",240*6e4,!0),exports.POD_STALE_GRACE_MS=envInt("LENSMCP_POD_STALE_GRACE_MS",1e4),exports.POD_RECYCLE_SETTLE_MS=envInt("LENSMCP_POD_RECYCLE_SETTLE_MS",8e3),exports.POD_STALE_SCAN_MS=envInt("LENSMCP_POD_STALE_SCAN_MS",3e4),exports.POD_STALE_RECYCLE_COOLDOWN_MS=envInt("LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS",6e4),exports.LENS_PROBE_ENABLED=process.env.LENSMCP_LENS_PROBE!=="0",exports.LENS_WATCHER_PROBE_ENABLED=process.env.LENSMCP_LENS_WATCHER_PROBE!=="0",exports.LENS_WATCHER_UNVERIFIED=process.env.LENSMCP_LENS_WATCHER_UNVERIFIED==="keep"?"keep":"recycle",exports.LENS_WATCH_SCOPE_ENABLED=process.env.LENSMCP_LENS_WATCH_SCOPE!=="0",exports.POD_WATCHER_UNVERIFIED=process.env.LENSMCP_POD_WATCHER_UNVERIFIED==="keep"?"keep":"recycle",exports.POD_WATCHER_SILENT=process.env.LENSMCP_POD_WATCHER_SILENT==="recycle"?"recycle":"keep",exports.POD_RECYCLE_MODE=process.env.LENSMCP_POD_RECYCLE_MODE==="in-place"?"in-place":"rolling",exports.POD_ROLL_READY_MS=envInt("LENSMCP_POD_ROLL_READY_MS",18e4),exports.LENS_PROBE_GRACE_MS=envInt("LENSMCP_LENS_PROBE_GRACE_MS",45e3),exports.LENS_PROBE_FAILS=envInt("LENSMCP_LENS_PROBE_FAILS",2),exports.LENS_PROBE_TIMEOUT_MS=envInt("LENSMCP_LENS_PROBE_TIMEOUT_MS",1e3),exports.LENS_PROBE_COOLDOWN_MS=envInt("LENSMCP_LENS_PROBE_COOLDOWN_MS",3e4),exports.LENS_PROBE_BOOT_GRACE_MS=envInt("LENSMCP_LENS_PROBE_BOOT_GRACE_MS",24e4),exports.PORT_FREE_WAIT_MS=envInt("LENSMCP_PORT_FREE_WAIT_MS",1e4),exports.PORT_FREE_POLL_MS=envInt("LENSMCP_PORT_FREE_POLL_MS",250),exports.CHILD_START_FAIL_MS=envInt("LENSMCP_CHILD_START_FAIL_MS",1e4),exports.MAX_PORT_CONFLICT_RETRIES=envInt("LENSMCP_MAX_PORT_CONFLICT_RETRIES",5),exports.LENS_PARKED_PORT_REPROBE_MS=envInt("LENSMCP_PARKED_PORT_REPROBE_MS",3e5),exports.LENS_RECYCLE_KILL_GRACE_MS=envInt("LENSMCP_RECYCLE_KILL_GRACE_MS",5e3),exports.LENS_RESTART_MODE=process.env.LENSMCP_LENS_RESTART_MODE==="in-place"?"in-place":"rolling",exports.LENS_REPLACEMENT_READY_MS=envInt("LENSMCP_LENS_REPLACEMENT_READY_MS",3e4),exports.LENS_REPLACEMENT_DRAIN_MS=envInt("LENSMCP_LENS_REPLACEMENT_DRAIN_MS",2e3,!0),exports.LENS_REQUEST_RECOVERY_WAIT_MS=envInt("LENSMCP_LENS_REQUEST_RECOVERY_WAIT_MS",15e3),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=envInt("LENSMCP_KEEPALIVE_TIMEOUT_MS",76e3),exports.GATEWAY_HEADERS_TIMEOUT_MS=envInt("LENSMCP_HEADERS_TIMEOUT_MS",6e4),exports.GATEWAY_STOP_GRACE_MS=envInt("LENSMCP_STOP_GRACE_MS",3e3),exports.GATEWAY_MAX_CONNECTIONS=envInt("LENSMCP_MAX_CONNECTIONS",0,!0),exports.GATEWAY_CONN_CHECK_MS=envInt("LENSMCP_CONN_CHECK_MS",0,!0),exports.BODY_STALL_MS=envInt("LENSMCP_BODY_STALL_MS",6e4,!0);
@@ -1 +1 @@
1
- "use strict";var m=Object.defineProperty;var u=(i,s)=>m(i,"name",{value:s,configurable:!0});var g=Object.defineProperty,d=u((i,s)=>g(i,"name",{value:s,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpgrade=createUpgrade;const manifest_1=require("../manifest"),discovery_1=require("./discovery"),edge_1=require("./edge"),hmr_reload_1=require("./hmr-reload");function createUpgrade(i){const{rt:s,auth:f,proxy:h,hooks:n}=i,y=d(r=>(0,manifest_1.matchRoute)(s.routes,(r.headers.host||"").split(":")[0],r.url??"/"),"match"),v=d(async(r,o,l)=>{for(const t of edge_1.GATEWAY_OWNED_HEADERS)delete r.headers[t];const e=y(r);if(!e){o.destroy();return}const a=n.upgradeContext(r,o,void 0);if(a.route=e,e.internal){const t=f.internal(r,e,void 0);if(!t.ok){o.destroy();return}a.caller=t.caller}else{const t=f.public(r,e,void 0);if(!t.ok){o.destroy();return}if(a.claims=t.claims,n.has("afterAuth")&&(await n.run("afterAuth",a),a.responded()))return}if(e.pool&&s.serviceKeys[e.project]&&(r.headers["x-internal-token"]=s.serviceKeys[e.project]),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),n.has("onUpgrade")&&(await n.run("onUpgrade",a),a.responded()))return;const p=e.pool?(()=>{const t=(0,discovery_1.pickSock)(e.pool,s.scanTtlMs);return t?{socketPath:t}:void 0})():e.target;if(!p){o.destroy();return}if(e.lens){const t=String(r.headers["sec-websocket-protocol"]??"").split(",").map(c=>c.trim());if(t.length===0||t.includes("vite-hmr")||t.every(c=>c==="")){const c=e.lens.workspaceKey?`${e.lens.workspaceKey}:${e.lens.project}`:e.lens.project;(0,hmr_reload_1.registerHmrClient)(s,c,o)}}h.proxy.ws(r,o,l,{target:p,changeOrigin:!e.pool,agent:h.agentForTarget(p)},()=>o.destroy())},"run");return(r,o,l)=>{o.on("error",()=>o.destroy()),v(r,o,l).catch(()=>o.destroy())}}u(createUpgrade,"createUpgrade"),d(createUpgrade,"createUpgrade");
1
+ "use strict";var k=Object.defineProperty;var u=(a,s)=>k(a,"name",{value:s,configurable:!0});var v=Object.defineProperty,p=u((a,s)=>v(a,"name",{value:s,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpgrade=createUpgrade;const manifest_1=require("../manifest"),discovery_1=require("./discovery"),edge_1=require("./edge"),hmr_reload_1=require("./hmr-reload"),service_keys_1=require("./service-keys");function createUpgrade(a){const{rt:s,auth:y,proxy:f,hooks:c}=a,g=p(async(r,o,d)=>{for(const t of edge_1.GATEWAY_OWNED_HEADERS)delete r.headers[t];const i=(0,edge_1.canonicalize)(r.url??"/");if(i===void 0){o.destroy();return}r.url=i.path+i.query;const m=(r.headers.host||"").split(":")[0],e=(0,manifest_1.matchRoute)(s.routes,m,i.policy+i.query);if(!e){o.destroy();return}(0,edge_1.resolveClientIp)(s,r);const n=c.upgradeContext(r,o,void 0);if(n.route=e,e.internal){const t=y.internal(r,e,void 0);if(!t.ok){o.destroy();return}n.caller=t.caller}else{const t=y.public(r,e,void 0,i.policy);if(!t.ok){o.destroy();return}if(n.claims=t.claims,c.has("afterAuth")&&(await c.run("afterAuth",n),n.responded()))return}const h=e.pool?s.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)]:void 0;if(h&&(r.headers["x-internal-token"]=h),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),(0,edge_1.stampForwardingHeaders)(s,r),c.has("onUpgrade")&&(await c.run("onUpgrade",n),n.responded()))return;const l=e.pool?(()=>{const t=(0,discovery_1.pickSock)(e.pool,s.scanTtlMs);return t?{socketPath:t}:void 0})():e.target;if(!l){o.destroy();return}if(e.lens&&String(r.headers["sec-websocket-protocol"]??"").split(",").map(t=>t.trim()).includes("vite-hmr")){const t=e.lens.workspaceKey?`${e.lens.workspaceKey}:${e.lens.project}`:e.lens.project;(0,hmr_reload_1.registerHmrClient)(s,t,o)}f.proxy.ws(r,o,d,{target:l,changeOrigin:!e.pool,agent:f.agentForTarget(l)},()=>o.destroy())},"run");return(r,o,d)=>{o.on("error",()=>o.destroy()),g(r,o,d).catch(()=>o.destroy())}}u(createUpgrade,"createUpgrade"),p(createUpgrade,"createUpgrade");
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A writer for this workspace root's gateway log, or undefined if it cannot be opened
3
+ * (a read-only or deleted root). Never throws: losing a log line must not affect a pod.
4
+ */
5
+ export declare function workspaceLogWriter(root: string): ((chunk: string) => void) | undefined;
6
+ /** Close every open workspace log (gateway teardown, and tests). */
7
+ export declare function closeWorkspaceLogs(): void;
@@ -0,0 +1 @@
1
+ "use strict";var a=Object.defineProperty;var r=(e,t)=>a(e,"name",{value:t,configurable:!0});var c=Object.defineProperty,n=r((e,t)=>c(e,"name",{value:t,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.workspaceLogWriter=workspaceLogWriter,exports.closeWorkspaceLogs=closeWorkspaceLogs;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),fds=new Map,written=new Map,MAX_BYTES=50*1024*1024,CHECK_EVERY=1024*1024;function ensureFd(e){const t=fds.get(e);if(t!==void 0)return t;try{fs.mkdirSync(path.dirname(e),{recursive:!0});const o=fs.openSync(e,"a");return fds.set(e,o),written.set(e,0),o}catch{return}}r(ensureFd,"ensureFd"),n(ensureFd,"ensureFd");function closeFd(e){const t=fds.get(e);if(fds.delete(e),written.delete(e),t!==void 0)try{fs.closeSync(t)}catch{}}r(closeFd,"closeFd"),n(closeFd,"closeFd");function rotateIfHuge(e){try{if(fs.statSync(e).size<=MAX_BYTES)return}catch{return}closeFd(e);try{fs.renameSync(e,`${e}.1`)}catch{}}r(rotateIfHuge,"rotateIfHuge"),n(rotateIfHuge,"rotateIfHuge");function workspaceLogWriter(e){const t=path.join(e,".lensmcp","gateway.log");if(ensureFd(t)!==void 0)return o=>{const s=ensureFd(t);if(s===void 0)return;try{fs.writeSync(s,o)}catch{closeFd(t);return}const i=(written.get(t)??0)+o.length;i>=CHECK_EVERY?(written.set(t,0),rotateIfHuge(t)):written.set(t,i)}}r(workspaceLogWriter,"workspaceLogWriter"),n(workspaceLogWriter,"workspaceLogWriter");function closeWorkspaceLogs(){for(const e of[...fds.keys()])closeFd(e)}r(closeWorkspaceLogs,"closeWorkspaceLogs"),n(closeWorkspaceLogs,"closeWorkspaceLogs");