@lensmcp/cluster 1.21.3 → 1.21.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/create-webpack-dev.d.ts +45 -0
- package/create-webpack-dev.js +1 -1
- package/executors/gateway/body-stall.d.ts +37 -0
- package/executors/gateway/body-stall.js +1 -0
- package/executors/gateway/canonical-path.d.ts +65 -0
- package/executors/gateway/canonical-path.js +1 -0
- package/executors/gateway/client-ip.d.ts +44 -0
- package/executors/gateway/client-ip.js +1 -0
- package/executors/gateway/jwks-verify.js +1 -1
- package/executors/gateway/main.prod-gateway.js +2 -2
- package/executors/gateway/manifest.d.ts +27 -3
- package/executors/gateway/manifest.js +1 -1
- package/executors/gateway/prod-config.d.ts +74 -0
- package/executors/gateway/prod-config.js +1 -0
- package/executors/gateway/prod-runtime/access-log.js +1 -1
- package/executors/gateway/prod-runtime/app.d.ts +6 -0
- package/executors/gateway/prod-runtime/app.js +1 -1
- package/executors/gateway/prod-runtime/auth.d.ts +1 -1
- package/executors/gateway/prod-runtime/auth.js +1 -1
- package/executors/gateway/prod-runtime/body-stall.d.ts +5 -0
- package/executors/gateway/prod-runtime/body-stall.js +1 -0
- package/executors/gateway/prod-runtime/cors.js +1 -1
- package/executors/gateway/prod-runtime/edge.d.ts +19 -3
- package/executors/gateway/prod-runtime/edge.js +1 -1
- package/executors/gateway/prod-runtime/handler.js +1 -1
- package/executors/gateway/prod-runtime/rollout.d.ts +11 -4
- package/executors/gateway/prod-runtime/rollout.js +1 -1
- package/executors/gateway/prod-runtime/routing.js +1 -1
- package/executors/gateway/prod-runtime/server.js +1 -1
- package/executors/gateway/prod-runtime/types.d.ts +30 -3
- package/executors/gateway/prod-runtime/upgrade.d.ts +25 -0
- package/executors/gateway/prod-runtime/upgrade.js +10 -2
- package/executors/gateway/prod-runtime/upstream.d.ts +3 -1
- package/executors/gateway/prod-runtime/upstream.js +1 -1
- package/executors/gateway/providers-prod.d.ts +13 -4
- package/executors/gateway/providers-prod.js +1 -1
- package/executors/gateway/registry-source.js +1 -1
- package/executors/gateway/rollout-ops.js +2 -2
- package/executors/gateway/runtime/auth.d.ts +6 -1
- package/executors/gateway/runtime/auth.js +1 -1
- package/executors/gateway/runtime/control.js +2 -1
- package/executors/gateway/runtime/dev-auth.d.ts +6 -2
- package/executors/gateway/runtime/dev-auth.js +1 -1
- package/executors/gateway/runtime/discovery.js +1 -1
- package/executors/gateway/runtime/edge.d.ts +19 -0
- package/executors/gateway/runtime/edge.js +1 -1
- package/executors/gateway/runtime/handler.js +1 -1
- package/executors/gateway/runtime/lens-children.d.ts +7 -0
- package/executors/gateway/runtime/lens-children.js +1 -1
- package/executors/gateway/runtime/lifecycle.d.ts +69 -7
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/pod-probe.d.ts +37 -0
- package/executors/gateway/runtime/pod-probe.js +1 -0
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/scope.d.ts +15 -0
- package/executors/gateway/runtime/scope.js +1 -1
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/service-keys.d.ts +42 -1
- package/executors/gateway/runtime/service-keys.js +1 -1
- package/executors/gateway/runtime/types.d.ts +193 -1
- package/executors/gateway/runtime/types.js +1 -1
- package/executors/gateway/runtime/upgrade.js +1 -1
- package/executors/gateway/runtime/workspace-log.d.ts +7 -0
- package/executors/gateway/runtime/workspace-log.js +1 -0
- package/main.devserver.js +7 -7
- package/package.json +4 -4
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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");
|