@lensmcp/cluster 1.21.4 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/executors/gateway/jwks-verify.js +1 -1
- package/executors/gateway/jwt-claims.d.ts +34 -0
- package/executors/gateway/jwt-claims.js +1 -0
- package/executors/gateway/main.prod-gateway.js +2 -2
- package/executors/gateway/prod-config.d.ts +27 -0
- package/executors/gateway/prod-config.js +1 -1
- package/executors/gateway/rate-limit.d.ts +9 -0
- package/executors/gateway/rate-limit.js +2 -2
- package/executors/gateway/runtime/auth.js +1 -1
- package/executors/gateway/runtime/chooser.d.ts +30 -9
- package/executors/gateway/runtime/chooser.js +9 -9
- package/executors/gateway/runtime/control.d.ts +17 -2
- package/executors/gateway/runtime/control.js +2 -2
- package/executors/gateway/runtime/edge.d.ts +29 -3
- package/executors/gateway/runtime/edge.js +1 -1
- package/executors/gateway/runtime/handler.js +1 -1
- package/executors/gateway/runtime/lens-children.js +1 -1
- package/executors/gateway/runtime/lifecycle.d.ts +21 -0
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/proxy.d.ts +25 -0
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/server.d.ts +90 -0
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/types.d.ts +20 -0
- package/executors/gateway/runtime/types.js +1 -1
- package/executors/gateway/runtime/upgrade.js +1 -1
- package/executors/serve/serve.impl.js +1 -1
- package/executors/trust/hostname.d.ts +20 -0
- package/executors/trust/hostname.js +1 -0
- package/executors/trust/trust.impl.js +5 -3
- package/main.devserver.d.ts +50 -0
- package/main.devserver.js +7 -7
- package/package.json +6 -6
|
@@ -536,6 +536,21 @@ export declare const GATEWAY_CONN_CHECK_MS: number;
|
|
|
536
536
|
* Override `LENSMCP_BODY_STALL_MS`. Default 60s.
|
|
537
537
|
*/
|
|
538
538
|
export declare const BODY_STALL_MS: number;
|
|
539
|
+
/**
|
|
540
|
+
* The h2 front door's RST_STREAM budget, PER SESSION (issues/foodguard/14). nghttp2's Rapid-Reset mitigation
|
|
541
|
+
* (CVE-2023-44487) allows 1000 resets per session, refilling at 33/s, and CLOSES the session (GOAWAY
|
|
542
|
+
* INTERNAL_ERROR) on the next one — with NO 'error' event on the server side. A browser keeps ONE h2 session
|
|
543
|
+
* per origin for the life of the tab and cancels every in-flight stream on each navigation (a Vite dev page:
|
|
544
|
+
* hundreds of modules, plus the API fetches React StrictMode aborts), so the cancels ACCUMULATE across
|
|
545
|
+
* navigations until the budget is spent; then every in-flight module fails at once (`ERR_HTTP2_PROTOCOL_ERROR`
|
|
546
|
+
* ×N in the console) and the page boots into its blank/fallback state. Measured in foodguard 2026-09-07:
|
|
547
|
+
* 19 of 134 screens in one capture sweep; reproduced on round 3 of a 450-cancels-per-round probe. A dev
|
|
548
|
+
* front door serves one developer's browser, so the budget is sized far past any real navigation rate.
|
|
549
|
+
* Node enforces the limit only when BOTH values are set. Override `LENSMCP_H2_RESET_BURST` /
|
|
550
|
+
* `LENSMCP_H2_RESET_RATE` (resets per second).
|
|
551
|
+
*/
|
|
552
|
+
export declare const GATEWAY_H2_RESET_BURST: number;
|
|
553
|
+
export declare const GATEWAY_H2_RESET_RATE: number;
|
|
539
554
|
/** Test-tunable knobs on top of the user-facing schema. */
|
|
540
555
|
export interface GatewayRuntimeOptions extends GatewayExecutorSchema {
|
|
541
556
|
/** Traffic-edge flush period (ms). Default 5000. */
|
|
@@ -560,6 +575,11 @@ export interface GatewayRuntimeOptions extends GatewayExecutorSchema {
|
|
|
560
575
|
connectionsCheckingIntervalMs?: number;
|
|
561
576
|
/** Abort a transfer that has moved NOTHING for this long, either direction (`LENSMCP_BODY_STALL_MS`, default 60s; 0 off). */
|
|
562
577
|
bodyStallMs?: number;
|
|
578
|
+
/** Per-session h2 RST_STREAM budget — burst (`LENSMCP_H2_RESET_BURST`, default 100k) and refill per second
|
|
579
|
+
* (`LENSMCP_H2_RESET_RATE`, default 10k). nghttp2's 1000 / 33 default tears a dev browser's session down
|
|
580
|
+
* after a few navigations (issues/foodguard/14). Both must be set for Node to apply either. */
|
|
581
|
+
h2ResetBurst?: number;
|
|
582
|
+
h2ResetRate?: number;
|
|
563
583
|
/** Customer-supplied lifecycle hooks layered on top of the built-in pipeline. */
|
|
564
584
|
hooks?: GatewayHooks;
|
|
565
585
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var M=Object.defineProperty;var e=(_,E)=>M(_,"name",{value:E,configurable:!0});var T=Object.defineProperty,R=e((_,E)=>T(_,"name",{value:E,configurable:!0}),"R");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_RESET_RATE=exports.GATEWAY_H2_RESET_BURST=exports.BODY_STALL_MS=exports.GATEWAY_CONN_CHECK_MS=exports.GATEWAY_MAX_CONNECTIONS=exports.GATEWAY_STOP_GRACE_MS=exports.GATEWAY_HEADERS_TIMEOUT_MS=exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=exports.LENS_REQUEST_RECOVERY_WAIT_MS=exports.LENS_REPLACEMENT_DRAIN_MS=exports.LENS_REPLACEMENT_READY_MS=exports.LENS_RESTART_MODE=exports.LENS_RECYCLE_KILL_GRACE_MS=exports.LENS_PARKED_PORT_REPROBE_MS=exports.MAX_PORT_CONFLICT_RETRIES=exports.CHILD_START_FAIL_MS=exports.PORT_FREE_POLL_MS=exports.PORT_FREE_WAIT_MS=exports.LENS_PROBE_BOOT_GRACE_MS=exports.LENS_PROBE_COOLDOWN_MS=exports.LENS_PROBE_TIMEOUT_MS=exports.LENS_PROBE_FAILS=exports.LENS_PROBE_GRACE_MS=exports.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,L=!1){const S=Number(process.env[_]);return!Number.isFinite(S)||S<0?E:S===0?L?0:E:S}e(envInt,"envInt"),R(envInt,"envInt"),exports.SERVICE_ERROR_THROTTLE_MS=5e3,exports.SERVICE_MAX_RESTARTS=5,exports.SERVICE_RESTART_WINDOW_MS=6e4,exports.UPSTREAM_HEAL_WINDOW_MS=8e3,exports.UPSTREAM_HEAL_STEP_MS=300,exports.POD_RESPONSE_TIMEOUT_MS=(()=>{const _=Number(process.env.LENSMCP_POD_RESPONSE_TIMEOUT_MS);return Number.isFinite(_)&&_>0?_:3e4})(),exports.POD_SLOW_FIRST_BYTE_MS=envInt("LENSMCP_POD_SLOW_FIRST_BYTE_MS",10*6e4,!0),exports.POD_ALIVE_PROBE_TIMEOUT_MS=envInt("LENSMCP_POD_ALIVE_PROBE_TIMEOUT_MS",1e3),exports.POD_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),exports.GATEWAY_H2_RESET_BURST=envInt("LENSMCP_H2_RESET_BURST",1e5),exports.GATEWAY_H2_RESET_RATE=envInt("LENSMCP_H2_RESET_RATE",1e4);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var k=Object.defineProperty;var
|
|
1
|
+
"use strict";var k=Object.defineProperty;var p=(a,s)=>k(a,"name",{value:s,configurable:!0});var m=Object.defineProperty,l=p((a,s)=>m(a,"name",{value:s,configurable:!0}),"l");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 accountUpgrade(a,s){const n=a.svc;if(!n||s.destroyed)return;n.inflight+=1,n.lastUsed=Date.now();let d=!1;s.once("close",()=>{d||(d=!0,n.inflight-=1,n.lastUsed=Date.now())})}p(accountUpgrade,"accountUpgrade"),l(accountUpgrade,"accountUpgrade");function createUpgrade(a){const{rt:s,auth:n,proxy:d,hooks:u}=a,h=l(async(r,o,y)=>{for(const t of edge_1.GATEWAY_OWNED_HEADERS)delete r.headers[t];const c=(0,edge_1.canonicalize)(r.url??"/");if(c===void 0){o.destroy();return}r.url=c.path+c.query;const v=(r.headers.host||"").split(":")[0],e=(0,manifest_1.matchRoute)(s.routes,v,c.policy+c.query);if(!e){o.destroy();return}(0,edge_1.resolveClientIp)(s,r);const i=u.upgradeContext(r,o,void 0);if(i.route=e,e.internal){const t=n.internal(r,e,void 0);if(!t.ok){o.destroy();return}i.caller=t.caller}else{const t=n.public(r,e,void 0,c.policy);if(!t.ok){o.destroy();return}if(i.claims=t.claims,u.has("afterAuth")&&(await u.run("afterAuth",i),i.responded()))return}const g=e.pool?s.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)]:void 0;if(g&&(r.headers["x-internal-token"]=g),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),(0,edge_1.stampForwardingHeaders)(s,r),u.has("onUpgrade")&&(await u.run("onUpgrade",i),i.responded()))return;const f=e.pool?(()=>{const t=(0,discovery_1.pickSock)(e.pool,s.scanTtlMs);return t?{socketPath:t}:void 0})():e.target;if(!f){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)}accountUpgrade(e,o),d.proxy.ws(r,o,y,{target:f,changeOrigin:!e.pool,agent:d.agentForTarget(f)},()=>o.destroy())},"run");return(r,o,y)=>{o.on("error",()=>o.destroy()),h(r,o,y).catch(()=>o.destroy())}}p(createUpgrade,"createUpgrade"),l(createUpgrade,"createUpgrade");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var X=Object.defineProperty;var l=(e,r)=>X(e,"name",{value:r,configurable:!0});var J=Object.defineProperty,a=l((e,r)=>J(e,"name",{value:r,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0});const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path")),fs=tslib_1.__importStar(require("node:fs")),node_child_process_1=require("node:child_process"),yaml=tslib_1.__importStar(require("js-yaml")),create_webpack_dev_1=require("../../create-webpack-dev"),webpack=require("webpack");async function*serveExecutor(e,r){process.env.NODE_ENV="development";const D=r.projectsConfigurations.projects[r.projectName],i=r.root,d=path.join(i,D.root),R=e.outputPath?path.join(i,e.outputPath):path.join(i,"dist",D.root),T=e.configEnv||"development",_=path.join(i,"config",`config.${T}.yaml`),S=3050;let u=S,n=r.projectName,h={};fs.existsSync(_)?(h=yaml.load(fs.readFileSync(_,"utf8")),u=Number(h.port)||u,n=h.serviceName||n):console.warn(`[serve] ${r.projectName}: no config file at ${_} \u2014 public devserver port defaults to ${S}. If another service also lacks its config, both bind ${S} \u2192 EADDRINUSE. Add config/config.${T}.yaml with a distinct \`port:\` (matching this project's cluster.port).`),e.serviceName&&(n=e.serviceName);const y=e.servePrefix??"",p=e.debugPort??(Number(process.env.LENSMCP_DEBUG_PORT)||void 0),x=/^(1|true|yes|on)$/i.test((process.env.LENSMCP_DEBUG_FIXED_PORTS??"").trim()),c=e.logFile?path.resolve(i,e.logFile):process.env.LENSMCP_LOG_FILE||void 0;if(c){fs.mkdirSync(path.dirname(c),{recursive:!0}),fs.writeFileSync(c,"",{mode:384});try{fs.chmodSync(c,384)}catch{}}let N,P;e.gateway?.middleware&&(N=path.resolve(d,e.gateway.middleware),P=JSON.stringify(h));const A=e.entryFile||"./src/deployments/service/main.ts",$=e.tsConfigFile||"./tsconfig.app.json",G=path.resolve(d,A),F=path.resolve(d,$),U=(e.workers||[]).map(t=>({entryName:t.name,entryPath:path.resolve(d,t.entryPath)})),C=(0,create_webpack_dev_1.createDevWebpackConfig)({appName:r.projectName,appRoot:d,outputDir:R,main:G,tsConfig:F,assets:e.assets||[],additionalEntryPoints:U,port:u,serviceName:n,memoryLimit:e.memoryLimit||8192,buildLibsFromSource:e.buildLibsFromSource!==!1,workspaceRoot:i,orgScopes:e.orgScopes||[],bundlePackages:e.bundlePackages||[],nodeExternalsConfig:e.nodeExternalsConfig,webpackConfigPath:e.webpackConfigPath,httpsReload:e.gateway?.https===!0});let s=null,b=!1;const O=path.join(__dirname,"../../main.devserver.js"),k=path.join(__dirname,"../../main.devserver.ts"),I=!fs.existsSync(O)&&fs.existsSync(k),w=I?k:O,M=path.join(R,"main.js");function v(){s&&!s.killed&&(s.kill("SIGTERM"),s=null)}l(v,"g"),a(v,"cleanup");const W=["SIGINT","SIGTERM","SIGHUP","SIGQUIT"].map(t=>{const o=a(()=>v(),"handler");return process.on(t,o),{sig:t,handler:o}});process.on("exit",v);function g(){if(!fs.existsSync(w)){console.error(`[serve] devserver script not found at ${w}`);return}const t=["--enable-source-maps"];I&&t.unshift("--require","@swc-node/register"),s=(0,node_child_process_1.fork)(w,{cwd:i,env:{...process.env,PORT:String(u),SERVICE_NAME:n,SERVE_PREFIX:y,LENSMCP_PROJECT:process.env.LENSMCP_PROJECT||r.projectName||n,LENSMCP_EVENT_FILE:process.env.LENSMCP_EVENT_FILE||path.join(i,".lensmcp","events.jsonl"),CHILD_COUNT:String(e.childCount||process.env.CHILD_COUNT||1),BUNDLE_PATH:M,...N&&{GATEWAY_MIDDLEWARE:N},...P&&{GATEWAY_CONFIG:P},...e.gateway?.routes?.length&&{GATEWAY_ROUTES:JSON.stringify(e.gateway.routes)},...e.gateway?.https&&{GATEWAY_HTTPS:"1"},...e.gateway?.extraPorts?.length&&{GATEWAY_EXTRA_PORTS:JSON.stringify(e.gateway.extraPorts)},...e.internalPort&&{INTERNAL_PORT:String(e.internalPort)},...p&&{LENSMCP_DEBUG_PORT:String(p)},...c&&{LENSMCP_LOG_FILE:c},...e.workers?.length&&{WORKERS:JSON.stringify(e.workers.map(o=>o.name))}},stdio:["inherit","inherit","inherit","ipc"],execArgv:t}),s.on("exit",(o,f)=>{s=null,!f&&(console.error(`[serve] devserver exited unexpectedly (code=${o}, signal=${f}) \u2014 respawning in 1.5s`),setTimeout(()=>{!s&&b&&g()},1500).unref?.())}),console.log(`[serve] Devserver started on port ${u} (service: ${n}${y?`, prefix: /${y}`:""})`),p?(console.warn(`[serve] DEBUGGER ENABLED for ${n}. Node's inspector has NO authentication \u2014 the ws:// UUID is the whole credential, and any account on this host that obtains it runs code as you. Do not enable this on a shared machine.`),x?console.warn(`[serve] LENSMCP_DEBUG_FIXED_PORTS=1 \u2014 pod inspectors bind PREDICTABLE ports 127.0.0.1:${p}+slot (workers ${p}+20+i), reachable by any account on this host. Attach WebStorm (Attach to Node.js/Chrome) to ${p}. Unset it to bind ephemeral ports instead.`):console.log(`[serve] Debug: pod inspectors bind an EPHEMERAL 127.0.0.1 port \u2014 click the "Debugger listening" link in this console, or run \`lensmcp debug ${n}\` to get the ws:// URLs over the owner-only control socket. Set LENSMCP_DEBUG_FIXED_PORTS=1 if your IDE needs a fixed attach port.`)):console.log(`[serve] Debug on demand: \`lensmcp debug ${n}\` opens pod inspectors without a restart`),c&&console.log(`[serve] Logs teed to ${c} \u2014 tail it in WebStorm via Run/Debug configuration \u2192 Logs tab`)}l(g,"v"),a(g,"startDevServer");function B(){if(!s||s.killed){console.log("[serve] Devserver not running, restarting..."),g();return}fetch(`http://localhost:${u}/webpack/reload`,{method:"POST",headers:{"content-type":"application/json"},body:"{}"}).then(()=>console.log("[serve] Notified devserver to reload")).catch(t=>{console.warn("[serve] Failed to notify devserver, restarting...",t.message),v(),g()})}l(B,"V"),a(B,"_notifyDevServerReload");const{watch:Y,...q}=C,H=webpack(q);let E=null;const L=[];function m(t){if(E){const o=E;E=null,o(t)}else L.push(t)}l(m,"b"),a(m,"pushResult");function j(){return L.length>0?Promise.resolve(L.shift()):new Promise(t=>{E=t})}l(j,"W"),a(j,"waitForNext");const V=H.watch(C.watchOptions||{},(t,o)=>{if(t){console.error("[serve] Webpack error:",t),m({success:!1});return}const f=o.hasErrors();if(console.info(o.toString({colors:!0,chunks:!1,assets:!0})),f){m({success:!1});return}(!b||!s)&&(b=!0,g()),m({success:!0,baseUrl:`http://localhost:${u}`})});try{for(;;)yield await j()}finally{V.close(()=>{}),v(),W.forEach(({sig:t,handler:o})=>process.removeListener(t,o))}}l(serveExecutor,"serveExecutor"),a(serveExecutor,"serveExecutor"),exports.default=serveExecutor,module.exports=serveExecutor,module.exports.default=serveExecutor;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hostname validation for the trust executor.
|
|
3
|
+
*
|
|
4
|
+
* `nx trust` collects host strings from every project's `project.json`, from embedded route options,
|
|
5
|
+
* and from its own `hosts` option, then writes them into `/etc/hosts` as root. Workspace
|
|
6
|
+
* configuration therefore reaches a privileged command, and the only check was "does not contain
|
|
7
|
+
* `*`" (issues/codex-security/33).
|
|
8
|
+
*
|
|
9
|
+
* This is the first of two defences. The second — removing the shell — is what actually closes the
|
|
10
|
+
* injection; this one means a value that could never be a hostname fails loudly at the point it is
|
|
11
|
+
* read, rather than becoming a confusing `/etc/hosts` line or a silent no-op.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Is this a hostname we are willing to write into `/etc/hosts`?
|
|
15
|
+
*
|
|
16
|
+
* Deliberately strict: a hostname that needs quoting is not a hostname. Wildcards are rejected here
|
|
17
|
+
* too, preserving the executor's existing (and correct) refusal to write a `*` entry — `/etc/hosts`
|
|
18
|
+
* cannot express one.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isValidHostname(host: string): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var s=Object.defineProperty;var a=(e,t)=>s(e,"name",{value:t,configurable:!0});var n=Object.defineProperty,r=a((e,t)=>n(e,"name",{value:t,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isValidHostname=isValidHostname;const LABEL=/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/,MAX_HOSTNAME_LENGTH=253;function isValidHostname(e){return typeof e!="string"||e.length===0||e.length>MAX_HOSTNAME_LENGTH?!1:e.split(".").every(t=>LABEL.test(t))}a(isValidHostname,"isValidHostname"),r(isValidHostname,"isValidHostname");
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
"use strict";var v=Object.defineProperty;var p=(
|
|
2
|
-
`).some(S=>{const
|
|
1
|
+
"use strict";var v=Object.defineProperty;var p=(o,t)=>v(o,"name",{value:t,configurable:!0});var m=Object.defineProperty,d=p((o,t)=>m(o,"name",{value:t,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=trustExecutor,exports.collectHosts=collectHosts;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),basic_ssl_1=require("../../basic-ssl"),hostname_1=require("./hostname");async function trustExecutor(o,t){const r=path.join(t.root,"node_modules",".cache","davnx-webpack"),i=(0,basic_ssl_1.ensureCaSync)(r),a=process.env.LENSMCP_TRUST_HOSTS_ONLY==="1",u=collectHosts(o,t);if(u.length===0&&console.log("[trust] no concrete hostnames found (only wildcards?) \u2014 keychain step only."),process.platform!=="darwin"){console.log("[trust] non-macOS platform \u2014 do these manually:"),console.log(` \u2022 trust ${i} in your OS/browser store (Linux: update-ca-certificates / certutil)`);for(const s of u)console.log(` \u2022 /etc/hosts: "127.0.0.1 ${s}" and "::1 ${s}"`);return{success:!0}}const e=caSha256Fingerprint(i),h=a||e!==void 0&&(0,node_child_process_1.spawnSync)("security",["find-certificate","-a","-c","lensmcp local dev CA","-Z","/Library/Keychains/System.keychain"],{encoding:"utf8"}).stdout?.includes(e)===!0;if(a)console.log("[trust] hosts-only: a shared daemon serves TLS with its own CA \u2014 skipping the keychain step.");else if(h)console.log("[trust] CA already in the System keychain \u2014 skipping.");else{if(console.log(`[trust] adding CA to the System keychain (sudo will prompt): ${i}`),(0,node_child_process_1.spawnSync)("sudo",["security","add-trusted-cert","-d","-r","trustRoot","-k","/Library/Keychains/System.keychain",i],{stdio:"inherit"}).status!==0)return console.error("[trust] add-trusted-cert failed \u2014 rerun in a terminal so sudo can prompt."),{success:!1};console.log("[trust] CA trusted. Restart your browser once (it caches certificate verdicts).")}const c=fs.readFileSync("/etc/hosts","utf8"),n=d((s,g)=>c.split(`
|
|
2
|
+
`).some(S=>{const f=S.trim();if(f.startsWith("#"))return!1;const y=f.split(/\s+/);return y[0]===s&&y.slice(1).includes(g)}),"hasEntry"),l=[];for(const s of u)n("127.0.0.1",s)||l.push(`127.0.0.1 ${s}`),n("::1",s)||l.push(`::1 ${s}`);if(l.length===0)console.log("[trust] /etc/hosts already complete (both families for every hostname).");else{if(console.log(`[trust] appending to /etc/hosts (sudo):
|
|
3
3
|
${l.join(`
|
|
4
|
-
`)}`)
|
|
4
|
+
`)}`),(0,node_child_process_1.spawnSync)("sudo",["tee","-a","/etc/hosts"],{input:l.join(`
|
|
5
|
+
`)+`
|
|
6
|
+
`,stdio:["pipe","ignore","inherit"]}).status!==0)return console.error("[trust] /etc/hosts update failed."),{success:!1};(0,node_child_process_1.spawnSync)("sudo",["dscacheutil","-flushcache"],{stdio:"inherit"}),(0,node_child_process_1.spawnSync)("sudo",["killall","-HUP","mDNSResponder"],{stdio:"inherit"})}if(!a){const s=(0,node_child_process_1.spawnSync)("security",["verify-cert","-c",i,"-p","basic"],{stdio:"ignore"});console.log(`[trust] CA trust check: ${s.status===0?"OK":"NOT TRUSTED YET (browser restart may be needed)"}`)}for(const s of u)console.log(`[trust] ${s} \u2192 https ready (hosts + cert SAN ride the gateway config)`);return{success:!0}}p(trustExecutor,"trustExecutor"),d(trustExecutor,"trustExecutor");function caSha256Fingerprint(o){const t=(0,node_child_process_1.spawnSync)("openssl",["x509","-in",o,"-noout","-fingerprint","-sha256"],{encoding:"utf8"});if(t.status!==0||!t.stdout)return;const r=t.stdout.split("=")[1]?.replaceAll(":","").trim().toUpperCase();return r&&/^[0-9A-F]{64}$/.test(r)?r:void 0}p(caSha256Fingerprint,"caSha256Fingerprint"),d(caSha256Fingerprint,"caSha256Fingerprint");function collectHosts(o,t){const r=[],i=t.projectName?t.projectsConfigurations?.projects[t.projectName]:void 0,a=o.serveTarget??"serve-hmr",u=i?.targets?.[a]?.options?.gateway?.routes??[];for(const e of u)e.host&&(0,hostname_1.isValidHostname)(e.host)?r.push(e.host):e.host&&console.warn(`[trust] ignoring invalid hostname in ${a} routes: ${JSON.stringify(e.host)}`);for(const e of Object.values(t.projectsConfigurations?.projects??{}))try{const h=JSON.parse(fs.readFileSync(path.join(t.root,e.root,"project.json"),"utf8")),c=h.cluster??h.davnx,n=c?.host;n&&!(0,hostname_1.isValidHostname)(n)&&console.warn(`[trust] ignoring invalid hostname in ${e.root}/project.json: ${JSON.stringify(n)}`),n&&(0,hostname_1.isValidHostname)(n)&&(r.push(n),c?.service&&c.internalHost!==!1&&r.push(typeof c.internalHost=="string"?c.internalHost:`internal.${n}`))}catch{}return[...new Set([...r,"lensmcp.local",...o.hosts??[]])]}p(collectHosts,"collectHosts"),d(collectHosts,"collectHosts");
|
package/main.devserver.d.ts
CHANGED
|
@@ -13,3 +13,53 @@ export interface GatewayRoute {
|
|
|
13
13
|
*/
|
|
14
14
|
prependPrefix?: string;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Did this request arrive on the OWNER-ONLY control socket, rather than the public TCP port?
|
|
18
|
+
*
|
|
19
|
+
* `requestHandler` serves both: the Unix socket at `$TMPDIR/<ws>/<svc>-devserver/parent.sock`
|
|
20
|
+
* (0700 dir, see issues/codex-security/23) and `server.listen(publicPort)`. Admin branches were
|
|
21
|
+
* reachable on both, so `POST /webpack/debug` — which opens a Node inspector in every pod and
|
|
22
|
+
* returns the ws:// URLs, and whose UUID is the only thing guarding `Runtime.evaluate` — could be
|
|
23
|
+
* called by anything that could reach the port (issues/codex-security/05).
|
|
24
|
+
*
|
|
25
|
+
* Node's `Server.address()` returns the PATH STRING for a pipe/Unix socket and an object
|
|
26
|
+
* `{ address, family, port }` for TCP. That is the discriminator, and it needs no extra plumbing
|
|
27
|
+
* through the handler.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isOwnerOnlyTransport(req: {
|
|
30
|
+
socket?: unknown;
|
|
31
|
+
}): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Opt-out that restores the pre-`06` PREDICTABLE inspector ports (`base+slot`, `base+20+i`).
|
|
34
|
+
*
|
|
35
|
+
* issues/codex-security/06: with `debugPort` (or `LENSMCP_DEBUG_PORT`) set, every pod and worker
|
|
36
|
+
* opened a Node inspector on a port derived from a configured, PRINTED base. Node's inspector has no
|
|
37
|
+
* application authentication — `GET /json/list` on that port returns the session UUID, and the UUID is
|
|
38
|
+
* the whole credential for `Runtime.evaluate`, i.e. arbitrary code execution inside the pod with the
|
|
39
|
+
* developer's environment, files and network. Binding `127.0.0.1` excludes other HOSTS. It does not
|
|
40
|
+
* exclude other ACCOUNTS on the same machine, and this project's threat model refuses to trust them.
|
|
41
|
+
*
|
|
42
|
+
* A debugger the developer cannot attach to is useless, so the feature stays. What changed is which
|
|
43
|
+
* way the default falls: the port is now EPHEMERAL (`inspector.open(0, …)`, kernel-chosen), and the
|
|
44
|
+
* resulting `ws://` URL is delivered by the pod's own console line and by `POST /webpack/debug` on the
|
|
45
|
+
* owner-only control socket (`lensmcp debug <svc>`). Guessing a random high port and then guessing a
|
|
46
|
+
* v4 UUID is a materially different proposition from `curl 127.0.0.1:9229/json/list`.
|
|
47
|
+
*
|
|
48
|
+
* The one thing an ephemeral port costs is a PRE-CONFIGURED IDE attach ("Attach to Node.js, port
|
|
49
|
+
* 9229"), which needs a stable number. `LENSMCP_DEBUG_FIXED_PORTS=1` buys that back, and every path
|
|
50
|
+
* that reads it also warns that it is host-wide.
|
|
51
|
+
*/
|
|
52
|
+
export declare const DEBUG_FIXED_PORTS_ENV = "LENSMCP_DEBUG_FIXED_PORTS";
|
|
53
|
+
export declare function usesFixedInspectorPorts(env?: NodeJS.ProcessEnv): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The port a pod/worker inspector should bind: `0` (kernel picks a free loopback port) unless the
|
|
56
|
+
* caller has explicitly opted back into fixed ports.
|
|
57
|
+
*
|
|
58
|
+
* `0` is a real value here, not "off" — the caller decides whether to open an inspector at all
|
|
59
|
+
* (`debugBasePort` being set); this only decides WHICH port. An out-of-range derived port also
|
|
60
|
+
* degrades to `0` rather than throwing at `inspector.open`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function inspectorPortFor(basePort: number, offset: number, env?: NodeJS.ProcessEnv): number;
|
|
63
|
+
export declare function redactInspectorUrls(text: string): string;
|
|
64
|
+
/** The TCP port out of a `ws://host:port/uuid` inspector URL, or `null`. */
|
|
65
|
+
export declare function inspectorPortOf(url: string | undefined): number | null;
|
package/main.devserver.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"use strict";var ge=Object.defineProperty;var g=(s,f)=>ge(s,"name",{value:f,configurable:!0});var he=Object.defineProperty,l=g((s,f)=>he(s,"name",{value:f,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0});const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path")),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),node_child_process_1=require("node:child_process"),http=tslib_1.__importStar(require("node:http")),readline=tslib_1.__importStar(require("node:readline")),inspector=tslib_1.__importStar(require("node:inspector")),util=tslib_1.__importStar(require("node:util")),httpProxy=require("http-proxy"),{prettyFactory}=require("pino-pretty"),BUNDLE_PATH=process.env.BUNDLE_PATH?.trim()||"./main.js",SERVICE_NAME=process.env.SERVICE_NAME||"",SERVICE_PREFIX=process.env.SERVE_PREFIX?`/${process.env.SERVE_PREFIX}`:"",GATEWAY_MIDDLEWARE_PATH=process.env.GATEWAY_MIDDLEWARE||"",GATEWAY_CONFIG=(()=>{const s=process.env.GATEWAY_CONFIG;if(!s)return{};try{return JSON.parse(s)}catch{return{}}})();let gatewayMiddleware=null;if(GATEWAY_MIDDLEWARE_PATH)try{const s=require(GATEWAY_MIDDLEWARE_PATH);gatewayMiddleware=typeof s=="function"?s:typeof s.default=="function"?s.default:null,gatewayMiddleware||console.warn(`[gateway] ${GATEWAY_MIDDLEWARE_PATH} does not export a function, gateway disabled`)}catch(s){console.error(`[gateway] Failed to load middleware from ${GATEWAY_MIDDLEWARE_PATH}:`,s)}function applyGatewayMiddleware(s){if(gatewayMiddleware)try{gatewayMiddleware(s,GATEWAY_CONFIG)}catch(f){console.error("[gateway] Middleware error:",f)}}g(applyGatewayMiddleware,"applyGatewayMiddleware"),l(applyGatewayMiddleware,"applyGatewayMiddleware");const GATEWAY_ROUTES=(()=>{try{return JSON.parse(process.env.GATEWAY_ROUTES||"[]")}catch{return[]}})(),GATEWAY_HTTPS=process.env.GATEWAY_HTTPS==="1";function hostMatches(s,f){if(s.startsWith("*.")){const S=s.slice(1);return f.endsWith(S)||f===s.slice(2)}return f===s}g(hostMatches,"hostMatches"),l(hostMatches,"hostMatches");function matchGatewayRoute(s){if(GATEWAY_ROUTES.length===0)return;const f=(s.headers.host||"").split(":")[0],S=s.url||"/";for(const y of GATEWAY_ROUTES)if(!(y.host&&!hostMatches(y.host,f))&&!(y.prefix&&!S.startsWith(y.prefix)))return y}g(matchGatewayRoute,"matchGatewayRoute"),l(matchGatewayRoute,"matchGatewayRoute");const WORKER_NAMES=(()=>{try{return JSON.parse(process.env.WORKERS||"[]")}catch{return[]}})(),LENSMCP_WS_KEY=process.env.LENSMCP_WS_KEY||"",SOCK_DIR=LENSMCP_WS_KEY?path.join(os.tmpdir(),LENSMCP_WS_KEY,`${SERVICE_NAME||"lensmcp-cluster"}-devserver`):path.join(os.tmpdir(),`${SERVICE_NAME||"lensmcp-cluster"}-devserver`),POD_GENERATION=(()=>{const s=Number(process.env.LENSMCP_POD_GENERATION);return Number.isFinite(s)&&s>1?Math.floor(s):1})();function childSockPath(s){return path.join(SOCK_DIR,POD_GENERATION>1?`child-${s}-g${POD_GENERATION}.sock`:`child-${s}.sock`)}g(childSockPath,"childSockPath"),l(childSockPath,"childSockPath");function sockOwnerPath(s){return`${s}.owner`}g(sockOwnerPath,"sockOwnerPath"),l(sockOwnerPath,"sockOwnerPath");function claimSock(s,f=process.pid){try{fs.mkdirSync(path.dirname(s),{recursive:!0})}catch{}try{fs.writeFileSync(sockOwnerPath(s),String(f))}catch{}try{fs.unlinkSync(s)}catch{}}g(claimSock,"claimSock"),l(claimSock,"claimSock");function cleanupSock(s,f=process.pid){if(s){try{const S=fs.readFileSync(sockOwnerPath(s),"utf8").trim();if(S&&S!==String(f))return}catch{}try{fs.unlinkSync(s)}catch{}try{fs.unlinkSync(sockOwnerPath(s))}catch{}}}g(cleanupSock,"cleanupSock"),l(cleanupSock,"cleanupSock");function serviceKeyFingerprint(){const s=process.env.LENSMCP_SERVICE_KEY;if(!s)return;const{createHash:f}=require("node:crypto");return f("sha256").update(s).digest("hex").slice(0,8)}g(serviceKeyFingerprint,"serviceKeyFingerprint"),l(serviceKeyFingerprint,"serviceKeyFingerprint");async function importFresh(spec){process.env.DEVSERVER_MODE="1";const resolved=path.isAbsolute(spec)?spec:path.join(__dirname,spec);return eval(`delete require.cache["${resolved}"]`),eval(`require("${resolved}")`)}g(importFresh,"importFresh"),l(importFresh,"importFresh");function debounce(s,f){let S=null;return(...y)=>{S&&clearTimeout(S),S=setTimeout(()=>s(...y),f)}}if(g(debounce,"debounce"),l(debounce,"debounce"),process.env.APP_RUNNER==="1"){const s=l(S=>{if(!inspector.url())try{inspector.open(S,"127.0.0.1",!1)}catch(_){console.warn(`[child] inspector.open(${S}) failed (port taken by another devserver?) \u2014 falling back to a random port:`,_.message);try{inspector.open(0,"127.0.0.1",!1)}catch{}}const y=inspector.url();y&&process.send&&process.send({type:"inspector-url",url:y})},"openInspector"),f=Number(process.env.CHILD_DEBUG_PORT||"");Number.isInteger(f)&&f>1024&&s(f);try{require("@lensmcp/node-instrumentation/register")}catch{}(async()=>{let S=null,y=!1,_,x=!1,k={app:null,handler:l((r,n)=>{n.statusCode=503,n.end("starting")},"handler"),inflight:0};const M=(()=>{const r=Number(process.env.LENSMCP_SWAP_DRAIN_MS);return Number.isFinite(r)&&r>=0?r:15*6e4})(),O=l(r=>{const n=globalThis.__lensmcpActiveWork;if(typeof n!="function")return{count:0};try{const u=n(r);return{count:Number(u?.count??0),...u?.oldestStartedAt?{oldestStartedAt:u.oldestStartedAt}:{}}}catch{return{count:0}}},"activeWork"),G=(()=>{const r=Number(process.env.LENSMCP_SWAP_MAX_DRAINING);return Number.isFinite(r)&&r>=1?Math.floor(r):1})(),P=new Set;async function v(r,n){if(!r.closed&&(r.closed=!0,P.delete(r),n&&console.error(`[child] ${n}`),!!r.app))try{await r.app.close()}catch(u){console.error("[child] error closing a previous app:",u)}}g(v,"G"),l(v,"closeGeneration");async function F(r){for(;P.size>=G;){const h=P.values().next().value;if(!h)break;await v(h,`closing a still-draining generation early \u2014 ${P.size} were pending and the cap is ${G} (each holds a whole app: DI container, DB pool, queue workers). Raise LENSMCP_SWAP_MAX_DRAINING if this pod legitimately needs more.`)}P.add(r);const n=Date.now(),u=l(()=>({inflight:r.inflight,work:O(r.workGen).count}),"busy");for(;M>0&&Date.now()-n<M;){if(r.closed)return;const h=u();if(h.inflight===0&&h.work===0)break;await new Promise(m=>{setTimeout(m,250).unref?.()})}if(r.closed)return;const E=u();E.inflight>0||E.work>0?console.error(`[child] swap drain expired after ${Date.now()-n}ms \u2014 closing the previous app with ${E.inflight} in-flight request(s) and ${E.work} background job(s) still running. A queue job killed here keeps its lock until the TTL lapses. Raise LENSMCP_SWAP_DRAIN_MS (currently ${M}ms) if this is legitimate work.`):Date.now()-n>250&&console.log(`[child] previous app drained in ${Date.now()-n}ms \u2014 closing it`),await v(r)}g(F,"V"),l(F,"drainThenClose");async function I(){if(y)return x=!0,console.log("[child] Reload deferred \u2014 will re-swap after current swap completes"),S;y=!0,process.removeAllListeners("uncaughtException"),process.removeAllListeners("unhandledRejection"),process.on("uncaughtException",(h,m)=>{console.error(`[child] uncaughtException (${m}):`,h)}),process.on("unhandledRejection",h=>{const m=h instanceof Error?h.stack||h.message:String(h);m.includes("FronteggContextScope")||m.includes("populateLoggerMetadata")||console.error("[child] unhandledRejection:",h)});try{await importFresh(BUNDLE_PATH)}catch(h){throw y=!1,h}if(typeof global.createChildApp!="function")throw y=!1,new Error(`Bundle '${BUNDLE_PATH}' does not export createChildApp() and the zero-touch bridge is not active. Either install @lensmcp/node-instrumentation (a plain NestFactory.create + app.listen main.ts then just works) or set global.createChildApp in main.ts when DEVSERVER_MODE === '1'.`);const r=global.__lensmcpBeginWorkGeneration,n=typeof r=="function"?r():void 0;let u;try{u=await global.createChildApp()}catch(h){throw y=!1,h}const E=k;return S=u,k={app:u,handler:u.handler,inflight:0,...n===void 0?{}:{workGen:n}},console.log("[child] Swapped to fresh app (previous draining in background)"),E.app&&F(E).catch(h=>console.error("[child] background close error:",h)),y=!1,x?(x=!1,I()):u}g(I,"O"),l(I,"swapNow");const J=debounce(async()=>{try{console.log("[child] /webpack/reload"),await I()}catch(r){console.error("[child] reload failed:",r)}},250),W=http.createServer((r,n)=>{if(r.method==="POST"&&r.url==="/webpack/reload"){n.writeHead(200,{"content-type":"application/json"}),n.end(JSON.stringify({ok:!0})),J();return}if(r.method==="GET"&&r.url==="/__lensmcp/alive"){const E=r.headers["x-internal-token"],h=process.env.LENSMCP_SERVICE_KEY;if(h&&E!==h){n.writeHead(404,{"content-type":"text/plain"}),n.end("not found");return}const m=O();n.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),n.end(JSON.stringify({ok:!0,pid:process.pid,swapping:y,inflight:k.inflight,oldestInflightAt:k.oldestAt,activeWork:m.count,oldestWorkAt:m.oldestStartedAt,keyFingerprint:serviceKeyFingerprint(),..._?{watcher:_}:{},...Number.isFinite(Number(process.env.LENSMCP_POD_GENERATION))&&process.env.LENSMCP_POD_GENERATION?{podGeneration:Number(process.env.LENSMCP_POD_GENERATION)}:{}}));return}const u=k;u.inflight+=1,u.oldestAt===void 0&&(u.oldestAt=Date.now()),n.on("close",()=>{u.inflight-=1,u.inflight===0&&(u.oldestAt=void 0)}),u.handler(r,n)}),A=process.env.CHILD_SOCK_PATH;process.on("disconnect",()=>{cleanupSock(A),process.exit(0)});try{await I();const r=path.isAbsolute(BUNDLE_PATH)?BUNDLE_PATH:path.join(__dirname,BUNDLE_PATH);console.log(`[child] Using bundle: ${r}`),claimSock(A),W.listen(A,()=>{console.log(`[child] Listening on ${A}`),process.send&&process.send({type:"ready",socketPath:A})})}catch(r){console.error("[child] Startup error:",r),process.send&&process.send({type:"boot-error",error:String(r?.stack||r)}),process.exitCode=1;return}process.on("message",async r=>{if(!(!r||typeof r!="object")){if(r.type==="debug-open"){const n=Number(r.port);s(Number.isInteger(n)&&n>1024?n:0);return}if(r.type==="watcher"){const n=r.testimony;n&&typeof n=="object"&&(_=n);return}if(r.type==="reload")try{await I(),process.send&&process.send({type:"reloaded",socketPath:A})}catch(n){console.error("[child] reload error:",n),process.send&&process.send({type:"reload-error",error:String(n?.stack||n)})}}}),process.on("beforeExit",()=>{try{inspector?.close?.()}catch{}});const d=l(async r=>{console.log(`[child ${r}] shutting down\u2026`);const n=S?.close().catch(E=>console.error("[child] close error:",E)),u=new Promise(E=>W.close(()=>E()));cleanupSock(A),setTimeout(()=>process.exit(0),700).unref(),await Promise.all([n,u]),process.exit(0)},"shutdown");["SIGINT","SIGTERM","SIGHUP","SIGQUIT","SIGUSR2"].forEach(r=>{process.on(r,()=>{inspector?.close?.(),d(r)})})})()}else{let s=g(function(d){P.push(d),P.length>G&&P.splice(0,P.length-G),M?.write(d.replace(O,""));for(const r of v)try{r.res.write(r.plain?d.replace(O,""):d)}catch{v.delete(r)}},"G");l(s,"emitLogLine");const f=["\x1B[36m","\x1B[33m","\x1B[35m","\x1B[32m","\x1B[34m","\x1B[31m"],S="\x1B[0m",y=Math.max(1,Number(process.env.CHILD_COUNT||1));let _=(()=>{const d=Number(process.env.LENSMCP_DEBUG_PORT||"");return Number.isInteger(d)&&d>1024?d:0})();const x=20,k=process.env.LENSMCP_LOG_FILE||"",M=k?(()=>{try{return fs.mkdirSync(path.dirname(k),{recursive:!0}),fs.createWriteStream(k,{flags:"a"})}catch(d){return console.warn(`[parent] cannot open log file ${k}:`,d.message),null}})():null,O=/\x1b\[[0-9;]*m/g,G=2e3,P=[],v=new Set;for(const d of["log","info","warn","error"]){const r=console[d].bind(console);console[d]=(...n)=>{r(...n),s(n.map(u=>typeof u=="string"?u:util.inspect(u)).join(" ")+`
|
|
2
|
-
`)}}const
|
|
3
|
-
`+String(
|
|
4
|
-
`;
|
|
5
|
-
`);return}const p=matchGatewayRoute(e);if(p?.prependPrefix&&!e.url?.startsWith(p.prependPrefix)&&(e.url=p.prependPrefix+(e.url??"/")),p?.target){applyGatewayMiddleware(e),
|
|
1
|
+
"use strict";var ge=Object.defineProperty;var g=(o,d)=>ge(o,"name",{value:d,configurable:!0});var he=Object.defineProperty,i=g((o,d)=>he(o,"name",{value:d,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DEBUG_FIXED_PORTS_ENV=void 0,exports.isOwnerOnlyTransport=isOwnerOnlyTransport,exports.usesFixedInspectorPorts=usesFixedInspectorPorts,exports.inspectorPortFor=inspectorPortFor,exports.redactInspectorUrls=redactInspectorUrls,exports.inspectorPortOf=inspectorPortOf;const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path")),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),node_child_process_1=require("node:child_process"),http=tslib_1.__importStar(require("node:http")),readline=tslib_1.__importStar(require("node:readline")),inspector=tslib_1.__importStar(require("node:inspector")),util=tslib_1.__importStar(require("node:util")),httpProxy=require("http-proxy"),{prettyFactory}=require("pino-pretty"),BUNDLE_PATH=process.env.BUNDLE_PATH?.trim()||"./main.js",SERVICE_NAME=process.env.SERVICE_NAME||"",SERVICE_PREFIX=process.env.SERVE_PREFIX?`/${process.env.SERVE_PREFIX}`:"",GATEWAY_MIDDLEWARE_PATH=process.env.GATEWAY_MIDDLEWARE||"",GATEWAY_CONFIG=(()=>{const o=process.env.GATEWAY_CONFIG;if(!o)return{};try{return JSON.parse(o)}catch{return{}}})();let gatewayMiddleware=null;if(GATEWAY_MIDDLEWARE_PATH)try{const o=require(GATEWAY_MIDDLEWARE_PATH);gatewayMiddleware=typeof o=="function"?o:typeof o.default=="function"?o.default:null,gatewayMiddleware||console.warn(`[gateway] ${GATEWAY_MIDDLEWARE_PATH} does not export a function, gateway disabled`)}catch(o){console.error(`[gateway] Failed to load middleware from ${GATEWAY_MIDDLEWARE_PATH}:`,o)}function applyGatewayMiddleware(o){if(gatewayMiddleware)try{gatewayMiddleware(o,GATEWAY_CONFIG)}catch(d){console.error("[gateway] Middleware error:",d)}}g(applyGatewayMiddleware,"applyGatewayMiddleware"),i(applyGatewayMiddleware,"applyGatewayMiddleware");const GATEWAY_ROUTES=(()=>{try{return JSON.parse(process.env.GATEWAY_ROUTES||"[]")}catch{return[]}})(),GATEWAY_HTTPS=process.env.GATEWAY_HTTPS==="1";function hostMatches(o,d){if(o.startsWith("*.")){const f=o.slice(1);return d.endsWith(f)||d===o.slice(2)}return d===o}g(hostMatches,"hostMatches"),i(hostMatches,"hostMatches");function matchGatewayRoute(o){if(GATEWAY_ROUTES.length===0)return;const d=(o.headers.host||"").split(":")[0],f=o.url||"/";for(const y of GATEWAY_ROUTES)if(!(y.host&&!hostMatches(y.host,d))&&!(y.prefix&&!f.startsWith(y.prefix)))return y}g(matchGatewayRoute,"matchGatewayRoute"),i(matchGatewayRoute,"matchGatewayRoute");const WORKER_NAMES=(()=>{try{return JSON.parse(process.env.WORKERS||"[]")}catch{return[]}})(),LENSMCP_WS_KEY=process.env.LENSMCP_WS_KEY||"",SOCK_DIR=LENSMCP_WS_KEY?path.join(os.tmpdir(),LENSMCP_WS_KEY,`${SERVICE_NAME||"lensmcp-cluster"}-devserver`):path.join(os.tmpdir(),`${SERVICE_NAME||"lensmcp-cluster"}-devserver`),POD_GENERATION=(()=>{const o=Number(process.env.LENSMCP_POD_GENERATION);return Number.isFinite(o)&&o>1?Math.floor(o):1})();function childSockPath(o){return path.join(SOCK_DIR,POD_GENERATION>1?`child-${o}-g${POD_GENERATION}.sock`:`child-${o}.sock`)}g(childSockPath,"childSockPath"),i(childSockPath,"childSockPath");function sockOwnerPath(o){return`${o}.owner`}g(sockOwnerPath,"sockOwnerPath"),i(sockOwnerPath,"sockOwnerPath");function isOwnerOnlyTransport(o){try{return typeof o.socket?.server?.address?.()=="string"}catch{return!1}}g(isOwnerOnlyTransport,"isOwnerOnlyTransport"),i(isOwnerOnlyTransport,"isOwnerOnlyTransport"),exports.DEBUG_FIXED_PORTS_ENV="LENSMCP_DEBUG_FIXED_PORTS";function usesFixedInspectorPorts(o=process.env){const d=(o[exports.DEBUG_FIXED_PORTS_ENV]??"").trim().toLowerCase();return d==="1"||d==="true"||d==="yes"||d==="on"}g(usesFixedInspectorPorts,"usesFixedInspectorPorts"),i(usesFixedInspectorPorts,"usesFixedInspectorPorts");function inspectorPortFor(o,d,f=process.env){if(!usesFixedInspectorPorts(f))return 0;const y=Math.trunc(o)+Math.trunc(d);return Number.isInteger(y)&&y>1024&&y<=65535?y:0}g(inspectorPortFor,"inspectorPortFor"),i(inspectorPortFor,"inspectorPortFor");const INSPECTOR_TOKEN_RE=/((?:wss?:\/\/[^\s/]+\/|ws=[^\s/]+\/))([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;function redactInspectorUrls(o){return o.replace(INSPECTOR_TOKEN_RE,"$1<inspector-token-redacted>")}g(redactInspectorUrls,"redactInspectorUrls"),i(redactInspectorUrls,"redactInspectorUrls");function inspectorPortOf(o){const d=/^wss?:\/\/[^/]*?:(\d+)\//.exec(o??""),f=d?Number(d[1]):NaN;return Number.isInteger(f)&&f>0?f:null}g(inspectorPortOf,"inspectorPortOf"),i(inspectorPortOf,"inspectorPortOf");function ensureOwnerOnlyDir(o){try{fs.mkdirSync(o,{recursive:!0,mode:448})}catch{}try{const d=fs.lstatSync(o);d.isDirectory()&&(d.mode&63)!==0&&fs.chmodSync(o,448)}catch{}}g(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),i(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function claimSock(o,d=process.pid){ensureOwnerOnlyDir(path.dirname(o));try{const f=sockOwnerPath(o);try{fs.unlinkSync(f)}catch{}fs.writeFileSync(f,String(d),{flag:"wx",mode:384})}catch{}try{fs.unlinkSync(o)}catch{}}g(claimSock,"claimSock"),i(claimSock,"claimSock");function cleanupSock(o,d=process.pid){if(o){try{const f=fs.readFileSync(sockOwnerPath(o),"utf8").trim();if(f&&f!==String(d))return}catch{}try{fs.unlinkSync(o)}catch{}try{fs.unlinkSync(sockOwnerPath(o))}catch{}}}g(cleanupSock,"cleanupSock"),i(cleanupSock,"cleanupSock");function serviceKeyFingerprint(){const o=process.env.LENSMCP_SERVICE_KEY;if(!o)return;const{createHash:d}=require("node:crypto");return d("sha256").update(o).digest("hex").slice(0,8)}g(serviceKeyFingerprint,"serviceKeyFingerprint"),i(serviceKeyFingerprint,"serviceKeyFingerprint");async function importFresh(spec){process.env.DEVSERVER_MODE="1";const resolved=path.isAbsolute(spec)?spec:path.join(__dirname,spec);return eval(`delete require.cache["${resolved}"]`),eval(`require("${resolved}")`)}g(importFresh,"importFresh"),i(importFresh,"importFresh");function debounce(o,d){let f=null;return(...y)=>{f&&clearTimeout(f),f=setTimeout(()=>o(...y),d)}}if(g(debounce,"debounce"),i(debounce,"debounce"),process.env.APP_RUNNER==="1"){const o=i(f=>{if(!inspector.url())try{inspector.open(f,"127.0.0.1",!1)}catch(w){console.warn(`[child] inspector.open(${f}) failed (port taken by another devserver?) \u2014 falling back to a random port:`,w.message);try{inspector.open(0,"127.0.0.1",!1)}catch{}}const y=inspector.url();y&&process.send&&process.send({type:"inspector-url",url:y})},"openInspector"),d=process.env.CHILD_DEBUG_PORT;if(d!==void 0&&d!==""){const f=Number(d);Number.isInteger(f)&&(f===0||f>1024)&&o(f)}try{require("@lensmcp/node-instrumentation/register")}catch{}(async()=>{let f=null,y=!1,w,C=!1,k={app:null,handler:i((r,s)=>{s.statusCode=503,s.end("starting")},"handler"),inflight:0};const W=(()=>{const r=Number(process.env.LENSMCP_SWAP_DRAIN_MS);return Number.isFinite(r)&&r>=0?r:15*6e4})(),G=i(r=>{const s=globalThis.__lensmcpActiveWork;if(typeof s!="function")return{count:0};try{const h=s(r);return{count:Number(h?.count??0),...h?.oldestStartedAt?{oldestStartedAt:h.oldestStartedAt}:{}}}catch{return{count:0}}},"activeWork"),M=(()=>{const r=Number(process.env.LENSMCP_SWAP_MAX_DRAINING);return Number.isFinite(r)&&r>=1?Math.floor(r):1})(),N=new Set;async function R(r,s){if(!r.closed&&(r.closed=!0,N.delete(r),s&&console.error(`[child] ${s}`),!!r.app))try{await r.app.close()}catch(h){console.error("[child] error closing a previous app:",h)}}g(R,"L"),i(R,"closeGeneration");async function B(r){for(;N.size>=M;){const E=N.values().next().value;if(!E)break;await R(E,`closing a still-draining generation early \u2014 ${N.size} were pending and the cap is ${M} (each holds a whole app: DI container, DB pool, queue workers). Raise LENSMCP_SWAP_MAX_DRAINING if this pod legitimately needs more.`)}N.add(r);const s=Date.now(),h=i(()=>({inflight:r.inflight,work:G(r.workGen).count}),"busy");for(;W>0&&Date.now()-s<W;){if(r.closed)return;const E=h();if(E.inflight===0&&E.work===0)break;await new Promise(S=>{setTimeout(S,250).unref?.()})}if(r.closed)return;const m=h();m.inflight>0||m.work>0?console.error(`[child] swap drain expired after ${Date.now()-s}ms \u2014 closing the previous app with ${m.inflight} in-flight request(s) and ${m.work} background job(s) still running. A queue job killed here keeps its lock until the TTL lapses. Raise LENSMCP_SWAP_DRAIN_MS (currently ${W}ms) if this is legitimate work.`):Date.now()-s>250&&console.log(`[child] previous app drained in ${Date.now()-s}ms \u2014 closing it`),await R(r)}g(B,"V"),i(B,"drainThenClose");async function $(){if(y)return C=!0,console.log("[child] Reload deferred \u2014 will re-swap after current swap completes"),f;y=!0,process.removeAllListeners("uncaughtException"),process.removeAllListeners("unhandledRejection"),process.on("uncaughtException",(E,S)=>{console.error(`[child] uncaughtException (${S}):`,E)}),process.on("unhandledRejection",E=>{const S=E instanceof Error?E.stack||E.message:String(E);S.includes("FronteggContextScope")||S.includes("populateLoggerMetadata")||console.error("[child] unhandledRejection:",E)});try{await importFresh(BUNDLE_PATH)}catch(E){throw y=!1,E}if(typeof global.createChildApp!="function")throw y=!1,new Error(`Bundle '${BUNDLE_PATH}' does not export createChildApp() and the zero-touch bridge is not active. Either install @lensmcp/node-instrumentation (a plain NestFactory.create + app.listen main.ts then just works) or set global.createChildApp in main.ts when DEVSERVER_MODE === '1'.`);const r=global.__lensmcpBeginWorkGeneration,s=typeof r=="function"?r():void 0;let h;try{h=await global.createChildApp()}catch(E){throw y=!1,E}const m=k;return f=h,k={app:h,handler:h.handler,inflight:0,...s===void 0?{}:{workGen:s}},console.log("[child] Swapped to fresh app (previous draining in background)"),m.app&&B(m).catch(E=>console.error("[child] background close error:",E)),y=!1,C?(C=!1,$()):h}g($,"G"),i($,"swapNow");const J=debounce(async()=>{try{console.log("[child] /webpack/reload"),await $()}catch(r){console.error("[child] reload failed:",r)}},250),U=http.createServer((r,s)=>{if(r.method==="POST"&&r.url==="/webpack/reload"){s.writeHead(200,{"content-type":"application/json"}),s.end(JSON.stringify({ok:!0})),J();return}if(r.method==="GET"&&r.url==="/__lensmcp/alive"){const m=r.headers["x-internal-token"],E=process.env.LENSMCP_SERVICE_KEY;if(E&&m!==E){s.writeHead(404,{"content-type":"text/plain"}),s.end("not found");return}const S=G();s.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),s.end(JSON.stringify({ok:!0,pid:process.pid,swapping:y,inflight:k.inflight,oldestInflightAt:k.oldestAt,activeWork:S.count,oldestWorkAt:S.oldestStartedAt,keyFingerprint:serviceKeyFingerprint(),...w?{watcher:w}:{},...Number.isFinite(Number(process.env.LENSMCP_POD_GENERATION))&&process.env.LENSMCP_POD_GENERATION?{podGeneration:Number(process.env.LENSMCP_POD_GENERATION)}:{}}));return}const h=k;h.inflight+=1,h.oldestAt===void 0&&(h.oldestAt=Date.now()),s.on("close",()=>{h.inflight-=1,h.inflight===0&&(h.oldestAt=void 0)}),h.handler(r,s)}),b=process.env.CHILD_SOCK_PATH;process.on("disconnect",()=>{cleanupSock(b),process.exit(0)});try{await $();const r=path.isAbsolute(BUNDLE_PATH)?BUNDLE_PATH:path.join(__dirname,BUNDLE_PATH);console.log(`[child] Using bundle: ${r}`),claimSock(b),U.listen(b,()=>{console.log(`[child] Listening on ${b}`),process.send&&process.send({type:"ready",socketPath:b})})}catch(r){console.error("[child] Startup error:",r),process.send&&process.send({type:"boot-error",error:String(r?.stack||r)}),process.exitCode=1;return}process.on("message",async r=>{if(!(!r||typeof r!="object")){if(r.type==="debug-open"){const s=Number(r.port);o(Number.isInteger(s)&&s>1024?s:0);return}if(r.type==="watcher"){const s=r.testimony;s&&typeof s=="object"&&(w=s);return}if(r.type==="reload")try{await $(),process.send&&process.send({type:"reloaded",socketPath:b})}catch(s){console.error("[child] reload error:",s),process.send&&process.send({type:"reload-error",error:String(s?.stack||s)})}}}),process.on("beforeExit",()=>{try{inspector?.close?.()}catch{}});const u=i(async r=>{console.log(`[child ${r}] shutting down\u2026`);const s=f?.close().catch(m=>console.error("[child] close error:",m)),h=new Promise(m=>U.close(()=>m()));cleanupSock(b),setTimeout(()=>process.exit(0),700).unref(),await Promise.all([s,h]),process.exit(0)},"shutdown");["SIGINT","SIGTERM","SIGHUP","SIGQUIT","SIGUSR2"].forEach(r=>{process.on(r,()=>{inspector?.close?.(),u(r)})})})()}else{let o=g(function(u){N.push(u),N.length>M&&N.splice(0,N.length-M),W?.write(u.replace(G,""));for(const r of R)try{const s=r.plain?u.replace(G,""):u;r.res.write(r.redact?redactInspectorUrls(s):s)}catch{R.delete(r)}},"L");i(o,"emitLogLine");const d=["\x1B[36m","\x1B[33m","\x1B[35m","\x1B[32m","\x1B[34m","\x1B[31m"],f="\x1B[0m",y=Math.max(1,Number(process.env.CHILD_COUNT||1));let w=(()=>{const u=Number(process.env.LENSMCP_DEBUG_PORT||"");return Number.isInteger(u)&&u>1024?u:0})();const C=20;w&&(console.warn("[parent] DEBUGGER ENABLED \u2014 pods and workers open a Node inspector. Node's inspector has NO authentication: the ws:// URL's UUID is the whole credential, and any account on this host that obtains it can run code as you. Do not enable this on a shared machine. (issues/codex-security/06)"),console.warn(usesFixedInspectorPorts()?`[parent] ${exports.DEBUG_FIXED_PORTS_ENV} is set \u2014 inspectors bind PREDICTABLE ports (${w}+slot, workers ${w}+${C}+i). Any account on this host can connect to 127.0.0.1:${w} and read the session UUID from /json/list. Unset it to use ephemeral ports.`:`[parent] Inspectors bind an EPHEMERAL loopback port; attach via the printed "Debugger listening" URL, or \`lensmcp debug <service>\` (control socket). Set ${exports.DEBUG_FIXED_PORTS_ENV}=1 for fixed ports if your IDE needs a pre-configured attach port \u2014 any account on this host can then reach them.`));const k=process.env.LENSMCP_LOG_FILE||"",W=k?(()=>{try{fs.mkdirSync(path.dirname(k),{recursive:!0});const u=fs.createWriteStream(k,{flags:"a",mode:384});try{fs.chmodSync(k,384)}catch{}return u}catch(u){return console.warn(`[parent] cannot open log file ${k}:`,u.message),null}})():null,G=/\x1b\[[0-9;]*m/g,M=2e3,N=[],R=new Set;for(const u of["log","info","warn","error"]){const r=console[u].bind(console);console[u]=(...s)=>{r(...s),o(s.map(h=>typeof h=="string"?h:util.inspect(h)).join(" ")+`
|
|
2
|
+
`)}}const B=i(u=>d[(u-1)%d.length],"colorFor"),$=i(u=>{if(y===1)return"";const r=`[child#${u}] `;return`${B(u)}${r}${f}`},"tagFor"),J=i(u=>u.startsWith("{")&&u.endsWith("}"),"isJsonLog"),U=i((u,r,s)=>{const h=prettyFactory({sync:!0,colorize:!0,crlf:!0,messageKey:"message",errorLikeObjectKeys:["err","error"],errorProps:"type,message,stack",ignore:["logContext","context","hostname","req","res","err.driverError","module","cloudEnvironment","frontegg-application-id","frontegg-tenant-id","frontegg-trace-id","frontegg-vendor-id","host","service","version","err","error"].join(","),messageFormat:"{if module}[{module}] {end}{if context}[{context}] {end}{if logContext}[{logContext}] {end}{message}",customPrettifiers:{stack:i(S=>`
|
|
3
|
+
`+String(S),"stack")}}),m=i((S,T)=>{const I=S==="stdout"?process.stdout:process.stderr;try{s?.(T)}catch{}const A=J(T)?h(T):`${T}
|
|
4
|
+
`;I.write(`${r}${A}`),o(`${r}${A}`)},"write"),E=i((S,T)=>{if(!S)return;const I=readline.createInterface({input:S});I.on("line",A=>m(T,A)),I.on("close",()=>{})},"attach");E(u.stdout,"stdout"),E(u.stderr,"stderr")},"wireProcLogging"),b=i(u=>U(u.proc,$(u.id)),"wireChildLogging");(async()=>{ensureOwnerOnlyDir(SOCK_DIR);const u=httpProxy.createProxyServer({});let r=Number(process.env.PORT)||0;r||(r=9090);const s=[],h=new Set;function m(){let e=0;for(;h.has(e);)e++;return h.add(e),e}g(m,"S"),i(m,"allocDebugSlot");let E=1,S=0,T=!1,I=y,A=[];function z(){return s.filter(e=>e.healthy&&typeof e.socketPath=="string")}g(z,"q"),i(z,"healthyChildren");let Y;function Q(e){if(!e||typeof e!="object")return;const t=e;if(t.schemaVersion===1){Y=t;for(const p of s)try{p.proc.send?.({type:"watcher",testimony:t})}catch{}}}g(Q,"ee"),i(Q,"recordWatcherTestimony");function Z(e,t=256*1024){return new Promise(p=>{let c="",l=!1;e.on("data",n=>{l||(c+=n,c.length>t&&(l=!0))}),e.on("end",()=>{if(l||!c)return p(void 0);try{p(JSON.parse(c))}catch{p(void 0)}}),e.on("error",()=>p(void 0))})}g(Z,"te"),i(Z,"readJsonBody");function L(){const e=z();if(e.length===0)return null;const t=S%e.length;return S=(S+1)%e.length,e[t]}g(L,"j"),i(L,"pickChild");async function q(){const e=E++,t=childSockPath(e);cleanupSock(t);const p=["--enable-source-maps"];__filename.endsWith(".ts")&&p.unshift("--require","@swc-node/register");const c=w?m():-1,l=(0,node_child_process_1.fork)(__filename,{env:{...process.env,APP_RUNNER:"1",CHILD_SOCK_PATH:t,...c>=0&&{CHILD_DEBUG_PORT:String(inspectorPortFor(w,c))}},stdio:["inherit","pipe","pipe","ipc"],execArgv:p}),n={id:e,proc:l,healthy:!1,lastError:null,debugSlot:c};return l.on("message",_=>{if(!_||typeof _!="object")return;const a=_;if(a.type==="ready"){if(n.socketPath=String(a.socketPath),n.healthy=!0,console.log(`[parent] Child#${n.id} ready on socket ${n.socketPath}`),Y)try{l.send?.({type:"watcher",testimony:Y})}catch{}}else a.type==="inspector-url"?(n.inspectorUrl=String(a.url),console.log(`[parent] Child#${n.id} Debugger listening on ${a.url}`)):a.type==="reloaded"?(n.socketPath=String(a.socketPath),n.healthy=!0,console.log(`[parent] Child#${n.id} hot-swapped on socket ${n.socketPath}`)):a.type==="reload-error"?(n.lastError=String(a.error||"unknown reload error"),console.error(`[parent] Child#${n.id} reload error: ${n.lastError}`),n.healthy=!1):a.type==="boot-error"&&(n.lastError=String(a.error||"unknown boot error"),console.error(`[parent] Child#${n.id} boot error: ${n.lastError}`),n.healthy=!1)}),l.on("exit",(_,a)=>{n.debugSlot>=0&&(h.delete(n.debugSlot),n.debugSlot=-1);const P=a==="SIGINT"||a==="SIGTERM"||_===0;if(P?console.log(`[parent] Child#${n.id} exited cleanly (code=${_}, signal=${a??"none"})`):(console.error(`[parent] Child#${n.id} crashed (code=${_}, signal=${a??"none"})`),n.lastError||(n.lastError=`Child exited abnormally (code=${_}, signal=${a??"none"})`)),n.healthy=!1,cleanupSock(n.socketPath,n.proc.pid),n.socketPath=void 0,!P&&!T){const v=s.indexOf(n);v>=0&&s.splice(v,1);const j=Date.now();if(A=A.filter(D=>j-D<3e4),A.length>=5){console.error(`[parent] crash-loop detected (${A.length} respawns/30s) \u2014 pausing auto-respawn; POST /webpack/reload to retry`);return}A.push(j),setTimeout(()=>{const D=s.filter(fe=>fe.proc.killed===!1).length;!T&&D<I&&(console.log(`[parent] respawning a crashed pod (${D}/${I} live)`),q().catch(()=>{}))},1500).unref?.()}}),s.push(n),b(n),n}g(q,"J"),i(q,"spawnChild");async function ee(e){I=Math.max(I,e);const t=s.filter(c=>c.proc.killed===!1),p=e-t.length;for(let c=0;c<p;c++)await q()}g(ee,"re"),i(ee,"ensurePoolSize");async function se(){const e=s.filter(t=>!t.healthy||typeof t.socketPath!="string");await Promise.all(e.map(async t=>{try{t.proc.kill("SIGTERM")}catch{}cleanupSock(t.socketPath,t.proc.pid),t.socketPath=void 0;const p=s.indexOf(t);p>=0&&s.splice(p,1),await q()}))}g(se,"ae"),i(se,"respawnUnhealthy");const O=[];function te(e){const t=path.dirname(path.isAbsolute(BUNDLE_PATH)?BUNDLE_PATH:path.resolve(BUNDLE_PATH)),p=path.join(t,`${e}.js`);if(!fs.existsSync(p))return console.error(`[parent] Worker "${e}" bundle not found: ${p}`),null;const c=["--enable-source-maps"];__filename.endsWith(".ts")&&c.unshift("--require","@swc-node/register");try{c.push("--require",require.resolve("@lensmcp/node-instrumentation/register"))}catch{}const l=inspectorPortFor(w||9229,C+Math.max(0,WORKER_NAMES.indexOf(e)));c.push(w?`--inspect=127.0.0.1:${l}`:`--inspect-port=127.0.0.1:${l}`);const n=(0,node_child_process_1.fork)(p,{env:process.env,stdio:["inherit","pipe","pipe","ipc"],execArgv:c}),_={name:e,proc:n,debugPort:l};return O.push(_),U(n,`\x1B[35m[worker:${e}] \x1B[0m`,a=>{const P=/Debugger listening on (wss?:\/\/\S+)/.exec(a);P&&(_.inspectorUrl=P[1])}),n.on("exit",(a,P)=>{P==="SIGINT"||P==="SIGTERM"||a===0||console.error(`[parent] Worker "${e}" crashed (code=${a}, signal=${P??"none"})`);const v=O.indexOf(_);v>=0&&O.splice(v,1)}),console.log(`[parent] Worker "${e}" started (pid=${n.pid})`),_}g(te,"oe"),i(te,"spawnWorkerProcess");function ce(){for(const e of[...O])try{e.proc.kill("SIGTERM")}catch{}O.length=0;for(const e of WORKER_NAMES)te(e)}g(ce,"le"),i(ce,"restartWorkers");const de=debounce(async()=>{const e=z();if(e.length>0){console.log(`[parent] Forwarding reload to ${e.length} child(ren)`);for(const t of e)t.proc.send?.({type:"reload"})}await se(),WORKER_NAMES.length>0&&ce()},200),F=i(async(e,t)=>{if(e.method==="POST"&&e.url==="/webpack/reload"){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0})),Z(e).then(l=>Q(l?.watcher)),de();return}if(e.method==="POST"&&e.url==="/webpack/watching"){const l=await Z(e);Q(l),t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0,relayed:Y!==void 0}));return}if(e.method==="POST"&&e.url==="/webpack/scale"){let l="";e.on("data",n=>{l+=n}),e.on("end",async()=>{let n=0;try{n=Number(JSON.parse(l||"{}").pods)||0}catch{}n=Math.max(1,Math.min(n,16)),await ee(n),t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0,pods:n}))});return}if(e.method==="GET"&&e.url?.startsWith("/webpack/logs")){const l=new URL(e.url,"http://localhost").searchParams,n=Math.min(Math.max(Number(l.get("tail")??"100")||0,0),M),_=l.get("follow")!=="0",a=l.get("plain")==="1",P=!isOwnerOnlyTransport(e);t.writeHead(200,{"content-type":"text/plain; charset=utf-8","cache-control":"no-cache"});const v=n===0?"":N.slice(-n).join(""),j=a?v.replace(G,""):v;if(t.write(P?redactInspectorUrls(j):j),!_){t.end();return}const D={res:t,plain:a,redact:P};R.add(D),e.on("close",()=>R.delete(D));return}if(e.method==="POST"&&e.url?.startsWith("/webpack/debug")){if(!isOwnerOnlyTransport(e)){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not found"}));return}const l=Number(new URL(e.url,"http://localhost").searchParams.get("port")||"");Number.isInteger(l)&&l>1024&&(w=l),w||(w=9229);const n=s.filter(a=>a.proc.exitCode===null&&a.proc.signalCode===null);for(const a of n){a.debugSlot<0&&(a.debugSlot=m());try{a.proc.send?.({type:"debug-open",port:inspectorPortFor(w,a.debugSlot)})}catch{}}for(const a of O)try{a.proc.kill("SIGUSR1")}catch{}const _=Date.now()+2e3;for(;Date.now()<_&&n.some(a=>!a.inspectorUrl);)await new Promise(a=>setTimeout(a,50));t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0,basePort:w,fixedPorts:usesFixedInspectorPorts(),pods:n.map(a=>({id:a.id,inspectorUrl:a.inspectorUrl??null})),workers:O.map(a=>({name:a.name,port:a.debugPort||inspectorPortOf(a.inspectorUrl),inspectorUrl:a.inspectorUrl??null}))},null,2)+`
|
|
5
|
+
`);return}const p=matchGatewayRoute(e);if(p?.prependPrefix&&!e.url?.startsWith(p.prependPrefix)&&(e.url=p.prependPrefix+(e.url??"/")),p?.target){applyGatewayMiddleware(e),u.web(e,t,{target:p.target,autoRewrite:typeof p.target=="string",changeOrigin:!0},l=>{console.error(`[parent] proxy error to ${p.target}:`,l),t.statusCode=502,t.setHeader("content-type","text/plain; charset=utf-8"),t.end(`Upstream error from ${p.target}.`)});return}if(SERVICE_PREFIX){if(!e.url?.startsWith(SERVICE_PREFIX)){t.writeHead(404,{"content-type":"text/plain; charset=utf-8"}),t.end(`Not found. This service is mounted at ${SERVICE_PREFIX}/`);return}e.url=e.url.slice(SERVICE_PREFIX.length)||"/"}applyGatewayMiddleware(e);const c=L();if(!c){t.statusCode=503,t.setHeader("content-type","text/plain; charset=utf-8");const l=s.map(n=>`#${n.id}: ${n.lastError??"no error recorded"}`).join(`
|
|
6
6
|
`);t.end(`No healthy children available. POST /webpack/reload to recover.
|
|
7
7
|
|
|
8
8
|
Last errors:
|
|
9
|
-
${
|
|
10
|
-
${c.lastError}`)})},"requestHandler");let
|
|
9
|
+
${l}`);return}u.web(e,t,{target:{socketPath:c.socketPath},headers:{"x-proxy-child-id":String(c.id)}},l=>{console.error(`[parent] proxy error to Child#${c.id}:`,l),c.healthy=!1,c.lastError=String(l?.stack||l),t.statusCode=502,t.setHeader("content-type","text/plain; charset=utf-8"),t.end(`Upstream error from Child#${c.id}. Try /webpack/reload.
|
|
10
|
+
${c.lastError}`)})},"requestHandler");let x,H,re;const K=[];if(GATEWAY_HTTPS){const e=require("./basic-ssl"),t=GATEWAY_ROUTES.map(c=>c.host).filter(c=>!!c),p=path.join(process.cwd(),"node_modules",".cache","davnx-webpack");H=e.getCertificateSync(p,SERVICE_NAME||"lensmcp.dev",t),re=e.caCertPath(),x=require("node:https").createServer({key:H,cert:H},F)}else x=http.createServer(F);const ie=i((e,t,p)=>{const c=matchGatewayRoute(e);if(c?.prependPrefix&&!e.url?.startsWith(c.prependPrefix)&&(e.url=c.prependPrefix+(e.url??"/")),c?.target){u.ws(e,t,p,{target:c.target,changeOrigin:!0},()=>t.destroy());return}const l=L();if(!l){t.destroy();return}u.ws(e,t,p,{target:{socketPath:l.socketPath}},()=>t.destroy())},"upgradeHandler");x.on("upgrade",ie);const ae=2e3;let le=!1;x.on("error",e=>{if(e.code==="EADDRINUSE"){le||(le=!0,console.warn(`[parent] public port ${r} already in use \u2014 continuing on the unix-socket pool only (gateway routing is unaffected) and retrying the bind every ${ae/1e3}s. Give this service a distinct cluster.port if this is not a rolling handoff.`)),setTimeout(()=>{!T&&!x.listening&&x.listen(r)},ae).unref?.();return}console.error("[parent] public server error:",e)}),await ee(y);for(const e of WORKER_NAMES)te(e);x.listen(r,()=>{const e=GATEWAY_HTTPS?"https":"http";console.log(`[parent] Listening on ${e}://localhost:${r}`),SERVICE_PREFIX&&console.log(`[parent] Service prefix: "${SERVICE_PREFIX}" (enforced \u2014 requests without it will get 404)`),gatewayMiddleware&&console.log(`[parent] Gateway middleware: ACTIVE (${GATEWAY_MIDDLEWARE_PATH})`),GATEWAY_ROUTES.length>0&&console.log(`[parent] Gateway routes: ${GATEWAY_ROUTES.map(t=>`${t.host??"*"}${t.prefix??"/"}\u2192${t.target??"children"}`).join(" ")}`),re&&(console.log("[parent] HTTPS dev CA (trust ONCE, then rotations/new hostnames stay green):"),console.log(`[parent] sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${re}`)),console.log(`POST ${e}://localhost:${r}/webpack/reload to trigger rolling swap/respawn`)});const V=Number(process.env.INTERNAL_PORT||0);if(V>0){const e=i((p,c)=>{const l=L();if(!l){c.statusCode=503,c.setHeader("content-type","text/plain; charset=utf-8"),c.end("No healthy children available.");return}u.web(p,c,{target:{socketPath:l.socketPath}},n=>{l.healthy=!1,l.lastError=String(n?.stack||n),c.statusCode=502,c.end(`Upstream error from Child#${l.id}.`)})},"internalHandler"),t=http.createServer(e);t.on("upgrade",(p,c,l)=>{const n=L();if(!n){c.destroy();return}u.ws(p,c,l,{target:{socketPath:n.socketPath}},()=>c.destroy())}),t.on("error",p=>{console.warn(`[parent] internal port ${V} unavailable (${p.code}).`)}),t.listen(V,()=>{console.log(`[parent] Internal (service-to-service, no gateway middleware) on http://localhost:${V}`)}),K.push(t)}const ue=(()=>{try{return JSON.parse(process.env.GATEWAY_EXTRA_PORTS||"[]")}catch{return[]}})();for(const e of ue){if(!Number.isInteger(e)||e===Number(r))continue;const t=GATEWAY_HTTPS?require("node:https").createServer({key:H,cert:H},F):http.createServer(F);t.on("upgrade",ie),t.on("error",p=>{console.warn(`[parent] extra port ${e} unavailable (${p.code}) \u2014 main port still up.`)}),t.listen(e,()=>{console.log(`[parent] Also listening on ${GATEWAY_HTTPS?"https":"http"}://localhost:${e}`)}),K.push(t)}const oe=path.join(SOCK_DIR,"parent.sock");claimSock(oe);const ne=http.createServer(F);ne.on("error",e=>{console.warn(`[parent] control socket unavailable (${e.code}) \u2014 lensmcp logs/debug attach disabled.`)}),ne.listen(oe),K.push(ne);const pe=i(async e=>{T=!0,console.log(`[${e}] [parent] shutting down\u2026`),cleanupSock(oe);for(const t of K)try{t.close()}catch{}for(const t of O)try{t.proc.kill("SIGTERM")}catch{}for(const t of s){try{t.proc.kill("SIGTERM")}catch{}t.socketPath&&cleanupSock(t.socketPath,t.proc.pid)}try{fs.rmdirSync(SOCK_DIR)}catch{}setTimeout(()=>process.exit(0),700).unref()},"shutdown");["SIGINT","SIGTERM","SIGHUP","SIGQUIT","SIGUSR2"].forEach(e=>process.on(e,()=>{pe(e)}));const X=Number(process.env.LENSMCP_GATEWAY_PID??0);if(Number.isInteger(X)&&X>0){const e=setInterval(()=>{try{process.kill(X,0)}catch(t){if(t.code==="EPERM")return;clearInterval(e),console.error(`[parent] gateway (pid ${X}) is gone \u2014 shutting the pod down to avoid an orphan tree.`),pe("gateway-gone")}},5e3);e.unref()}})()}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@lensmcp/core": "1.
|
|
61
|
-
"@lensmcp/node-instrumentation": "1.
|
|
62
|
-
"@lensmcp/nx-plugin": "1.
|
|
63
|
-
"fastify": "^5.
|
|
64
|
-
"@fastify/reply-from": "^12.
|
|
60
|
+
"@lensmcp/core": "1.22.0",
|
|
61
|
+
"@lensmcp/node-instrumentation": "1.22.0",
|
|
62
|
+
"@lensmcp/nx-plugin": "1.22.0",
|
|
63
|
+
"fastify": "^5.8.5",
|
|
64
|
+
"@fastify/reply-from": "^12.6.2",
|
|
65
65
|
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
66
66
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
67
67
|
"glob": "^11.0.0",
|