@lensmcp/cluster 1.25.38 → 1.25.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/config-files.d.ts +20 -0
  2. package/config-files.js +1 -1
  3. package/devserver-notifier.d.ts +1 -1
  4. package/executors/gateway/prod-runtime/handler.js +1 -1
  5. package/executors/gateway/prod-runtime/observability.d.ts +4 -2
  6. package/executors/gateway/prod-runtime/observability.js +1 -1
  7. package/executors/gateway/runtime/awake.d.ts +42 -0
  8. package/executors/gateway/runtime/awake.js +1 -0
  9. package/executors/gateway/runtime/control.d.ts +11 -1
  10. package/executors/gateway/runtime/control.js +2 -2
  11. package/executors/gateway/runtime/discovery.js +1 -1
  12. package/executors/gateway/runtime/instance.d.ts +19 -3
  13. package/executors/gateway/runtime/instance.js +3 -2
  14. package/executors/gateway/runtime/lifecycle.d.ts +4 -1
  15. package/executors/gateway/runtime/lifecycle.js +3 -3
  16. package/executors/gateway/runtime/observability.d.ts +8 -3
  17. package/executors/gateway/runtime/observability.js +3 -3
  18. package/executors/gateway/runtime/proxy.js +1 -1
  19. package/executors/gateway/runtime/server.js +2 -2
  20. package/executors/gateway/runtime/shared-build.d.ts +1 -1
  21. package/executors/gateway/runtime/types.d.ts +46 -2
  22. package/executors/gateway/runtime/warm-start.d.ts +18 -0
  23. package/executors/gateway/runtime/warm-start.js +1 -1
  24. package/executors/serve/serve.impl.d.ts +1 -1
  25. package/executors/serve/serve.impl.js +1 -1
  26. package/package.json +4 -4
  27. package/perf/heavy-barrel-import.d.ts +1 -1
  28. package/shared-build/attach.d.ts +9 -0
  29. package/shared-build/attach.js +3 -3
  30. package/shared-build/protocol.d.ts +1 -1
  31. package/tsgo-check-plugin.d.ts +15 -0
  32. package/tsgo-check-plugin.js +4 -4
package/config-files.d.ts CHANGED
@@ -31,4 +31,24 @@ export declare class ConfigFilesPlugin {
31
31
  constructor(options: ConfigFilesPluginOptions);
32
32
  apply(compiler: ConfigFilesCompiler): void;
33
33
  }
34
+ /** How often {@link watchConfigFiles} reads the files' stats. */
35
+ export declare const CONFIG_POLL_MS = 1000;
36
+ export interface WatchConfigFilesOptions {
37
+ /** The files to watch — read again after every change, so a chain that grows or shrinks is followed. */
38
+ files(): Iterable<string>;
39
+ /** Some of them changed or went, with the ones that did. */
40
+ changed(paths: string[]): void;
41
+ /** Default {@link CONFIG_POLL_MS}. */
42
+ intervalMs?: number;
43
+ }
44
+ export interface ConfigFilesWatcher {
45
+ close(): void;
46
+ }
47
+ /**
48
+ * Watches configuration files for a process that has no compiler to watch them with (issues/closed/foodguard-115): a
49
+ * service built by the build host type-checks in its own process, and its checker read the tsconfig chain when it
50
+ * started. The files are polled by their stats, not watched with `fs.watch`: an editor saves by renaming a new file
51
+ * over the old one, and a watch on the old file sees nothing after that. A handful of files a second costs nothing.
52
+ */
53
+ export declare function watchConfigFiles(options: WatchConfigFilesOptions): ConfigFilesWatcher;
34
54
  export {};
package/config-files.js CHANGED
@@ -1 +1 @@
1
- "use strict";var a=Object.defineProperty;var s=(e,o)=>a(e,"name",{value:o,configurable:!0});var l=Object.defineProperty,t=s((e,o)=>l(e,"name",{value:o,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.ConfigFilesPlugin=void 0,exports.configFilesOf=configFilesOf;const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path")),workspace_paths_1=require("./workspace-paths");function configFilesOf(e){const o=((0,workspace_paths_1.readWorkspacePaths)(e.tsConfig,e.workspaceRoot)?.configFiles??[path.resolve(e.workspaceRoot,e.tsConfig)]).filter(i=>!i.split(path.sep).includes("node_modules"));return e.webpackConfigPath&&o.push(path.resolve(e.appRoot,e.webpackConfigPath)),[...new Set(o)]}s(configFilesOf,"configFilesOf"),t(configFilesOf,"configFilesOf");class ConfigFilesPlugin{static{s(this,"ConfigFilesPlugin")}static{t(this,"ConfigFilesPlugin")}constructor(o){this.options=o}apply(o){o.hooks.afterCompile.tap("LensmcpConfigFiles",i=>{for(const n of this.options.files())i.fileDependencies.add(n)}),o.hooks.watchRun.tap("LensmcpConfigFiles",i=>{if(!i.modifiedFiles&&!i.removedFiles)return;const n=new Set(this.options.files()),r=[...i.modifiedFiles??[],...i.removedFiles??[]].filter(p=>n.has(p));r.length>0&&this.options.changed([...new Set(r)])})}}exports.ConfigFilesPlugin=ConfigFilesPlugin;
1
+ "use strict";var F=Object.defineProperty;var c=(e,i)=>F(e,"name",{value:i,configurable:!0});var d=Object.defineProperty,l=c((e,i)=>d(e,"name",{value:i,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.CONFIG_POLL_MS=exports.ConfigFilesPlugin=void 0,exports.configFilesOf=configFilesOf,exports.watchConfigFiles=watchConfigFiles;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),workspace_paths_1=require("./workspace-paths");function configFilesOf(e){const i=((0,workspace_paths_1.readWorkspacePaths)(e.tsConfig,e.workspaceRoot)?.configFiles??[path.resolve(e.workspaceRoot,e.tsConfig)]).filter(s=>!s.split(path.sep).includes("node_modules"));return e.webpackConfigPath&&i.push(path.resolve(e.appRoot,e.webpackConfigPath)),[...new Set(i)]}c(configFilesOf,"configFilesOf"),l(configFilesOf,"configFilesOf");class ConfigFilesPlugin{static{c(this,"ConfigFilesPlugin")}static{l(this,"ConfigFilesPlugin")}constructor(i){this.options=i}apply(i){i.hooks.afterCompile.tap("LensmcpConfigFiles",s=>{for(const n of this.options.files())s.fileDependencies.add(n)}),i.hooks.watchRun.tap("LensmcpConfigFiles",s=>{if(!s.modifiedFiles&&!s.removedFiles)return;const n=new Set(this.options.files()),t=[...s.modifiedFiles??[],...s.removedFiles??[]].filter(f=>n.has(f));t.length>0&&this.options.changed([...new Set(t)])})}}exports.ConfigFilesPlugin=ConfigFilesPlugin,exports.CONFIG_POLL_MS=1e3;const CONFIG_SETTLE_MS=100;function watchConfigFiles(e){const i=e.intervalMs??exports.CONFIG_POLL_MS,s=new Map,n=new Set;let t,f=!1;const g=l(()=>{if(t=void 0,f||n.size===0)return;const r=[...n].sort();n.clear(),h(),e.changed(r)},"flush"),h=l(()=>{let r;try{r=new Set(e.files())}catch{return}for(const[o,a]of s)r.has(o)||(fs.unwatchFile(o,a),s.delete(o));for(const o of r){if(s.has(o))continue;const a=l((p,u)=>{p.mtimeMs===u.mtimeMs&&p.size===u.size&&p.ino===u.ino||(n.add(o),t||(t=setTimeout(g,CONFIG_SETTLE_MS),t.unref?.()))},"listener");fs.watchFile(o,{persistent:!1,interval:i},a),s.set(o,a)}},"sync");return h(),{close(){f=!0,t&&clearTimeout(t);for(const[r,o]of s)fs.unwatchFile(r,o);s.clear()}}}c(watchConfigFiles,"watchConfigFiles"),l(watchConfigFiles,"watchConfigFiles");
@@ -8,7 +8,7 @@
8
8
  * `/webpack/watching` answers 404 and nothing is said.
9
9
  *
10
10
  * Driven by {@link DevServerReloadPlugin} on a service's own compiler, and by the serve executor itself when the
11
- * service is compiled in a shared compilation (shared-build/attach.ts, issues/open/pod-memory-05).
11
+ * service is compiled in a shared compilation (shared-build/attach.ts, issues/closed/pod-memory-05).
12
12
  */
13
13
  export declare class DevserverNotifier {
14
14
  private base;
@@ -1 +1 @@
1
- "use strict";var W=Object.defineProperty;var _=(y,n)=>W(y,"name",{value:n,configurable:!0});var z=Object.defineProperty,v=_((y,n)=>z(y,"name",{value:n,configurable:!0}),"v");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHandler=createHandler;const manifest_1=require("../manifest"),gateway_errors_1=require("../gateway-errors"),edge_1=require("./edge");function createHandler(y){const{rt:n,obs:d,cors:N,auth:M,upstream:U,rollout:w,hooks:p,reqState:$,spanByReq:D,ctxByReq:V}=y,C=!!n.opts.pods?.resolveVersion,q=!!n.opts.cookie?.secret,j=!!n.opts.pods?.bucket&&(q||!!n.opts.uidHeader),B=C||q||j,F=n.opts.upstreamRetries??1,G=v(l=>(0,edge_1.tokenMatches)(l.query?.token??(0,edge_1.hdr)(l.raw.headers["x-status-token"]),n.opts.statusToken),"statusTokenMatches"),K=v((l,o,e,f,m)=>{const a=l.raw;if(N.setCors(o,a),o.header("cache-control",edge_1.NO_STORE),a.method==="OPTIONS"){o.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(a.headers["access-control-request-headers"]??"*")).send();return}const s=n.table.routes.filter(i=>i.host&&(0,manifest_1.hostMatches)(i.host,e)&&i.prefix),h=m;if(s.length>0&&(h==="/health"||h==="/healthz")){o.code(200).type("application/json").send(JSON.stringify({status:"ok",host:e,services:s.map(i=>({service:i.service,prefix:i.prefix}))}));return}if(h==="/.well-known/lensmcp-routes"){if(!G(l)){(0,edge_1.sendError)(o,a,404,"not_found","No gateway route for this hostname.");return}o.code(200).type("application/json").send(JSON.stringify({host:e,routes:n.table.routes.filter(i=>!i.host||(0,manifest_1.hostMatches)(i.host,e)).map(i=>({host:i.host??"(default)",service:i.service,prefix:i.prefix,internal:!!i.internal,auth:i.auth}))}));return}(0,edge_1.sendError)(o,a,404,"not_found",s.length>0?`No service mounted at ${f} on ${e}.`:"No gateway route for this hostname.")},"handleNoRoute");return async(l,o)=>{const e=l.raw,f=(e.headers.host||"").split(":")[0],m=e.url??"/",a=d.beginTrace(e);(0,gateway_errors_1.ensureRequestId)(e),d.step(a,"received",{host:f,method:e.method,url:m});const s=p.any?p.httpContext(e,l,o,a):void 0;if(s&&V.set(l,s),s&&p.has("onRequest")&&(await p.run("onRequest",s),s.responded()))return o;if(e.httpVersionMajor<1)return d.finish(a,"gateway",400),(0,edge_1.sendError)(o,e,400,"bad_request","Bad Request");const h=(0,edge_1.canonicalize)(m);if(h===void 0)return d.step(a,"bad-path",{}),d.finish(a,"gateway",400),(0,edge_1.sendError)(o,e,400,"bad_request","Bad Request");const i=h.policy,b=h.path+h.query;b!==m&&(0,edge_1.rememberRawTarget)(e,m),e.url=b;const I=e.socket?.remoteAddress??l.ip,T=w.clientIpOf(e,I);if(n.opts.rateLimiter){const r=await n.opts.rateLimiter.check(T);if(!r.allowed)return o.header("retry-after",String(Math.ceil(r.retryAfterMs/1e3))),d.step(a,"rate-limited",{}),d.finish(a,"gateway",429),(0,edge_1.sendError)(o,e,429,"rate_limited","Too Many Requests")}const t=(0,manifest_1.matchRoute)(n.table.routes,f,i+h.query);if(!t)return K(l,o,f,b,i),o;if(s&&(s.route=t),d.step(a,"route-match",{project:t.service,...t.prefix?{prefix:t.prefix}:{},...t.internal?{internal:!0}:{}}),e.method==="OPTIONS")return N.setCors(o,e),o.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(e.headers["access-control-request-headers"]??"*")).send(),o;const k=n.opts.tracing?.startSpan(e,`gateway ${t.service}`,{"http.request.method":e.method??"GET","url.path":b.split("?")[0],"server.address":f,"lensmcp.service":t.service,...t.internal?{"lensmcp.internal":!0}:{}});if(k&&D.set(l,k),s&&p.has("beforeAuth")&&(await p.run("beforeAuth",s),s.responded()))return o;const O=t.internal?void 0:n.opts.identify?.(e),S=O instanceof Promise?await O:O;let P;if(t.internal){const r=M.internal(e,t,a);if(!r.ok)return d.finish(a,t.service,r.status),(0,edge_1.sendError)(o,e,r.status,r.reason,r.detail);P=r.caller,s&&(s.caller=P)}else{const r=M.public(e,t,i,S,T,a);if(!r.ok)return d.finish(a,t.service,r.status),(0,edge_1.sendError)(o,e,r.status,r.reason,r.detail);if(s&&(s.claims=S),s&&p.has("afterAuth")&&(await p.run("afterAuth",s),s.responded()))return o}if(e.headers["x-forwarded-for"]=w.forwardedFor(e,I),e.headers["x-forwarded-proto"]=n.opts.tls?"https":"http",e.headers["x-forwarded-host"]=e.headers.host??f,n.opts.serviceKeys?.[t.service]&&(e.headers["x-internal-token"]=n.opts.serviceKeys[t.service]),t.prependPrefix){const r=e.url??"/",c=t.prependPrefix;r!==c&&!r.startsWith(c+"/")&&(e.url=c+r)}const x=(0,edge_1.hdr)(e.headers[n.versionHeader]);let R=x,A=!1,E;if(B&&!t.internal&&!x){const r=w.resolveStickyKey(e);r.mint&&(E=r.key);const c=C?n.opts.pods.resolveVersion(t.service,w.buildAttrs(e,T,S,r.key)):void 0;if(c)R=c.version,A=!0,d.step(a,"targeting",{rule:c.rule,version:c.version});else if(r.key){const g=n.opts.pods?.bucket?.(t.service,r.key);g&&(R=g.version,A=!0,d.step(a,"targeting",{mode:"sticky",version:g.version}))}}const u=await U.resolveUpstream(t,a,R,A);if(!u)return d.finish(a,t.service,503),(0,edge_1.sendError)(o,e,503,"unavailable",`Service ${t.service} has no available upstream.`);if(!("url"in u))return d.finish(a,t.service,502),(0,edge_1.sendError)(o,e,502,"unavailable",`Service ${t.service}: socket upstreams are not supported by the prod gateway.`);const J=new URL(u.url).origin,H=u.url,L={route:t,caller:P,startedAt:Date.now(),trace:a,version:u.version,upLabel:H};return $.set(l,L),s&&(s.version=u.version),d.step(a,u.version?"rollout":"upstream",{upstream:H,...u.version?{version:u.version,...x?{pinned:!0}:{}}:{}}),k&&n.opts.tracing.inject(k,e.headers),s&&p.has("beforeForward")&&(await p.run("beforeForward",s),s.responded())?o:o.from(void 0,{getUpstream:v(()=>J,"getUpstream"),retriesCount:F,rewriteHeaders:v(r=>{if(L.proxied=!0,a&&(r["x-lensmcp-upstream"]=H),r["x-request-id"]||(r["x-request-id"]=(0,gateway_errors_1.ensureRequestId)(e)),u.version&&(r["x-lensmcp-version"]=u.version),(x||u.version)&&(0,edge_1.addVary)(r,n.versionHeader),(E||j&&q)&&(0,edge_1.addVary)(r,"Cookie"),delete r["x-powered-by"],delete r.connection,delete r["keep-alive"],delete r["proxy-connection"],r.server="LensMCP",E){const c=r["set-cookie"],g=Array.isArray(c)?c:c?[c]:[];r["set-cookie"]=[...g,w.mintCookieHeader(E)]}return r},"rewriteHeaders"),onError:v((r,{error:c})=>{n.opts.pods?.drop?.(t.service,u),s&&p.has("onError")&&(s.error=c,p.run("onError",s)),(0,edge_1.sendError)(r,e,502,"unavailable",`Upstream ${t.service} unavailable.`)},"onError")})}}_(createHandler,"createHandler"),v(createHandler,"createHandler");
1
+ "use strict";var W=Object.defineProperty;var C=(y,a)=>W(y,"name",{value:a,configurable:!0});var z=Object.defineProperty,w=C((y,a)=>z(y,"name",{value:a,configurable:!0}),"w");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHandler=createHandler;const manifest_1=require("../manifest"),gateway_errors_1=require("../gateway-errors"),edge_1=require("./edge");function createHandler(y){const{rt:a,obs:c,cors:M,auth:L,upstream:D,rollout:b,hooks:h,reqState:T,spanByReq:$,ctxByReq:V}=y,j=!!a.opts.pods?.resolveVersion,O=!!a.opts.cookie?.secret,I=!!a.opts.pods?.bucket&&(O||!!a.opts.uidHeader),B=j||O||I,F=a.opts.upstreamRetries??1,G=w(l=>(0,edge_1.tokenMatches)(l.query?.token??(0,edge_1.hdr)(l.raw.headers["x-status-token"]),a.opts.statusToken),"statusTokenMatches"),K=w((l,t,e,v,m)=>{const n=l.raw;if(M.setCors(t,n),t.header("cache-control",edge_1.NO_STORE),n.method==="OPTIONS"){t.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(n.headers["access-control-request-headers"]??"*")).send();return}const o=a.table.routes.filter(i=>i.host&&(0,manifest_1.hostMatches)(i.host,e)&&i.prefix),f=m;if(o.length>0&&(f==="/health"||f==="/healthz")){t.code(200).type("application/json").send(JSON.stringify({status:"ok",host:e,services:o.map(i=>({service:i.service,prefix:i.prefix}))}));return}if(f==="/.well-known/lensmcp-routes"){if(!G(l)){(0,edge_1.sendError)(t,n,404,"not_found","No gateway route for this hostname.");return}t.code(200).type("application/json").send(JSON.stringify({host:e,routes:a.table.routes.filter(i=>!i.host||(0,manifest_1.hostMatches)(i.host,e)).map(i=>({host:i.host??"(default)",service:i.service,prefix:i.prefix,internal:!!i.internal,auth:i.auth}))}));return}(0,edge_1.sendError)(t,n,404,"not_found",o.length>0?`No service mounted at ${v} on ${e}.`:"No gateway route for this hostname.")},"handleNoRoute");return async(l,t)=>{const e=l.raw,v=(e.headers.host||"").split(":")[0],m=e.url??"/",n=c.beginTrace(e);(0,gateway_errors_1.ensureRequestId)(e),c.step(n,"received",{host:v,method:e.method,url:m});const o=h.any?h.httpContext(e,l,t,n):void 0;if(o&&V.set(l,o),o&&h.has("onRequest")&&(await h.run("onRequest",o),o.responded()))return t;if(e.httpVersionMajor<1)return c.finish(n,"gateway",400),(0,edge_1.sendError)(t,e,400,"bad_request","Bad Request");const f=(0,edge_1.canonicalize)(m);if(f===void 0)return c.step(n,"bad-path",{}),c.finish(n,"gateway",400),(0,edge_1.sendError)(t,e,400,"bad_request","Bad Request");const i=f.policy,k=f.path+f.query;k!==m&&(0,edge_1.rememberRawTarget)(e,m),e.url=k;const U=e.socket?.remoteAddress??l.ip,S=b.clientIpOf(e,U);if(a.opts.rateLimiter){const r=await a.opts.rateLimiter.check(S);if(!r.allowed)return t.header("retry-after",String(Math.ceil(r.retryAfterMs/1e3))),c.step(n,"rate-limited",{}),c.finish(n,"gateway",429),(0,edge_1.sendError)(t,e,429,"rate_limited","Too Many Requests")}const s=(0,manifest_1.matchRoute)(a.table.routes,v,i+f.query);if(!s)return K(l,t,v,k,i),t;if(o&&(o.route=s),c.step(n,"route-match",{project:s.service,...s.prefix?{prefix:s.prefix}:{},...s.internal?{internal:!0}:{}}),e.method==="OPTIONS")return M.setCors(t,e),t.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(e.headers["access-control-request-headers"]??"*")).send(),t;const E=a.opts.tracing?.startSpan(e,`gateway ${s.service}`,{"http.request.method":e.method??"GET","url.path":k.split("?")[0],"server.address":v,"lensmcp.service":s.service,...s.internal?{"lensmcp.internal":!0}:{}});if(E&&$.set(l,E),o&&h.has("beforeAuth")&&(await h.run("beforeAuth",o),o.responded()))return t;const P=s.internal?void 0:a.opts.identify?.(e),A=P instanceof Promise?await P:P;let R;if(s.internal){const r=L.internal(e,s,n);if(!r.ok)return c.finish(n,s.service,r.status),(0,edge_1.sendError)(t,e,r.status,r.reason,r.detail);R=r.caller,o&&(o.caller=R)}else{const r=L.public(e,s,i,A,S,n);if(!r.ok)return c.finish(n,s.service,r.status),(0,edge_1.sendError)(t,e,r.status,r.reason,r.detail);if(o&&(o.claims=A),o&&h.has("afterAuth")&&(await h.run("afterAuth",o),o.responded()))return t}if(e.headers["x-forwarded-for"]=b.forwardedFor(e,U),e.headers["x-forwarded-proto"]=a.opts.tls?"https":"http",e.headers["x-forwarded-host"]=e.headers.host??v,a.opts.serviceKeys?.[s.service]&&(e.headers["x-internal-token"]=a.opts.serviceKeys[s.service]),s.prependPrefix){const r=e.url??"/",d=s.prependPrefix;r!==d&&!r.startsWith(d+"/")&&(e.url=d+r)}const x=(0,edge_1.hdr)(e.headers[a.versionHeader]);let H=x,_=!1,q;if(B&&!s.internal&&!x){const r=b.resolveStickyKey(e);r.mint&&(q=r.key);const d=j?a.opts.pods.resolveVersion(s.service,b.buildAttrs(e,S,A,r.key)):void 0;if(d)H=d.version,_=!0,c.step(n,"targeting",{rule:d.rule,version:d.version});else if(r.key){const g=a.opts.pods?.bucket?.(s.service,r.key);g&&(H=g.version,_=!0,c.step(n,"targeting",{mode:"sticky",version:g.version}))}}const p=await D.resolveUpstream(s,n,H,_);if(!p)return c.finish(n,s.service,503),(0,edge_1.sendError)(t,e,503,"unavailable",`Service ${s.service} has no available upstream.`);if(!("url"in p))return c.finish(n,s.service,502),(0,edge_1.sendError)(t,e,502,"unavailable",`Service ${s.service}: socket upstreams are not supported by the prod gateway.`);const J=new URL(p.url).origin,N=p.url,u={route:s,caller:R,startedAt:Date.now(),trace:n,version:p.version,upLabel:N};return T.set(l,u),t.raw.once("close",()=>{if(t.raw.writableFinished||T.get(l)!==u)return;T.delete(l);const r=t.raw.headersSent,d=r?t.raw.statusCode:void 0;c.recordEdge(u.route,Date.now()-u.startedAt,d,u.caller,u.version),c.finish(u.trace,u.route.service,d,{aborted:r?"during-response":"before-response",error:r?"the caller went away before the response ended":"the caller went away before any response was sent",upstream:u.upLabel,...u.version?{version:u.version}:{},...u.caller?{caller:u.caller}:{}})}),o&&(o.version=p.version),c.step(n,p.version?"rollout":"upstream",{upstream:N,...p.version?{version:p.version,...x?{pinned:!0}:{}}:{}}),E&&a.opts.tracing.inject(E,e.headers),o&&h.has("beforeForward")&&(await h.run("beforeForward",o),o.responded())?t:t.from(void 0,{getUpstream:w(()=>J,"getUpstream"),retriesCount:F,rewriteHeaders:w(r=>{if(u.proxied=!0,n&&(r["x-lensmcp-upstream"]=N),r["x-request-id"]||(r["x-request-id"]=(0,gateway_errors_1.ensureRequestId)(e)),p.version&&(r["x-lensmcp-version"]=p.version),(x||p.version)&&(0,edge_1.addVary)(r,a.versionHeader),(q||I&&O)&&(0,edge_1.addVary)(r,"Cookie"),delete r["x-powered-by"],delete r.connection,delete r["keep-alive"],delete r["proxy-connection"],r.server="LensMCP",q){const d=r["set-cookie"],g=Array.isArray(d)?d:d?[d]:[];r["set-cookie"]=[...g,b.mintCookieHeader(q)]}return r},"rewriteHeaders"),onError:w((r,{error:d})=>{a.opts.pods?.drop?.(s.service,p),o&&h.has("onError")&&(o.error=d,h.run("onError",o)),(0,edge_1.sendError)(r,e,502,"unavailable",`Upstream ${s.service} unavailable.`)},"onError")})}}C(createHandler,"createHandler"),w(createHandler,"createHandler");
@@ -13,8 +13,10 @@ export interface Observability {
13
13
  emit(severity: string, title: string, fingerprint: string, raw?: Record<string, unknown>, ctx?: Record<string, unknown>): void;
14
14
  beginTrace(req: http.IncomingMessage): GwTrace | undefined;
15
15
  step(t: GwTrace | undefined, name: string, info?: Record<string, unknown>): void;
16
- finish(t: GwTrace | undefined, service: string, status: number, extra?: Record<string, unknown>): void;
17
- recordEdge(route: Route, ms: number, status: number, caller?: string, version?: string): void;
16
+ /** `status` `undefined`: the caller went away before any response head — the span reads «no response» (foodguard-118). */
17
+ finish(t: GwTrace | undefined, service: string, status: number | undefined, extra?: Record<string, unknown>): void;
18
+ /** `status` `undefined`: the caller went away before any response — timed, but not an upstream error. */
19
+ recordEdge(route: Route, ms: number, status: number | undefined, caller?: string, version?: string): void;
18
20
  buildStatusSnapshot(): ServiceStatus[];
19
21
  /** Start the periodic edge-flush + health-window roll; returns the interval (`.unref()`'d). */
20
22
  startFlusher(): NodeJS.Timeout;
@@ -1 +1 @@
1
- "use strict";var b=Object.defineProperty;var p=(o,d)=>b(o,"name",{value:d,configurable:!0});var y=Object.defineProperty,c=p((o,d)=>y(o,"name",{value:d,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createObservability=createObservability;const redact_1=require("@lensmcp/core/redact"),edge_1=require("./edge");function createObservability(o,d){const v=c((e,t,s,r,a)=>{if(o.emit)try{o.emit({id:(0,edge_1.eid)(),sessionId:"pending",timestamp:Date.now(),source:"gateway",category:"cluster",severity:e,context:(0,redact_1.redactDeep)({sessionId:"pending",...a}),fingerprint:s,title:(0,redact_1.safeTitle)(t),raw:r&&(0,redact_1.redactDeep)(r)})}catch{}},"emit"),h=new Map,q=c((e,t,s,r,a)=>{const n=e.host??"(default)",u=(r?r+"\u21D2":"")+n+"\u2192"+e.service+(a?"@"+a:""),i=h.get(u)??{host:n,service:e.service,caller:r,version:a,count:0,totalMs:0,errors:0};i.count+=1,i.totalMs+=t,(s>=500||s===0)&&(i.errors+=1),h.set(u,i)},"recordEdge"),m=Math.max(1,o.statusWindows??12),f=[],w=new Map,M=c(()=>[...new Set(d().map(e=>e.service))],"routeServices"),S=c(()=>{const e=new Map;for(const s of h.values()){const r=e.get(s.service)??{requests:0,errors:0,totalMs:0,versions:new Set};r.requests+=s.count,r.errors+=s.errors,r.totalMs+=s.totalMs,s.version&&r.versions.add(s.version),e.set(s.service,r)}const t=Date.now();for(const[s,r]of e)r.requests>0&&w.set(s,t);for(f.push(e);f.length>m;)f.shift()},"pushStatusWindow"),g=c(()=>{const e=new Map;for(const s of f)for(const[r,a]of s){const n=e.get(r)??{requests:0,errors:0,totalMs:0,versions:new Set};n.requests+=a.requests,n.errors+=a.errors,n.totalMs+=a.totalMs;for(const u of a.versions)n.versions.add(u);e.set(r,n)}const t=d();return M().map(s=>{const r=e.get(s)??{requests:0,errors:0,totalMs:0,versions:new Set},a=[...new Set(t.filter(i=>i.service===s).map(i=>i.host??"(default)"))],n=r.requests?r.errors/r.requests:0;let u=r.requests===0?"idle":n>=.5?"down":n>=.05?"degraded":"healthy";if(o.healthChecker){const i=t.filter(l=>l.service===s&&l.upstream).map(l=>l.upstream);i.length&&i.every(l=>!o.healthChecker.isHealthy(l))&&(u="down")}return{service:s,hosts:a,status:u,requests:r.requests,errorRate:+n.toFixed(4),avgMs:r.requests?Math.round(r.totalMs/r.requests):0,versions:[...r.versions],lastSeen:w.get(s)??null}})},"buildStatusSnapshot");return{emit:v,beginTrace:c(e=>{if(!o.emit)return;const t=(0,edge_1.hdr)(e.headers["x-lensmcp-flow-id"]);let s=(0,edge_1.hdr)(e.headers["x-request-id"]);if(!(!t&&!s&&!(0,edge_1.hdr)(e.headers.traceparent)))return s||(s=(0,edge_1.eid)(),e.headers["x-request-id"]=s),{startedAt:Date.now(),method:(e.method??"GET").toUpperCase(),host:(e.headers.host||"").split(":")[0],url:e.url??"/",steps:[],...t?{flowId:t}:{},...s?{requestId:s}:{}}},"beginTrace"),step:c((e,t,s)=>{e&&e.steps.push({step:t,atMs:Date.now()-e.startedAt,...s})},"step"),finish:c((e,t,s,r)=>{!e||e.done||(e.done=!0,v(s>=500?"error":"info",`gateway ${e.method} ${e.host}${e.url} \u2192 ${s} (${Date.now()-e.startedAt}ms)`,`gateway-request:${t}`,{kind:"gateway-request",project:t,host:e.host,method:e.method,url:e.url,status:s,durationMs:Date.now()-e.startedAt,startedAt:e.startedAt,steps:e.steps,...r},{...e.flowId?{flowId:e.flowId}:{},...e.requestId?{requestId:e.requestId}:{}}))},"finish"),recordEdge:q,buildStatusSnapshot:g,startFlusher:c(()=>{const e=setInterval(()=>{if(S(),o.onStatus)try{o.onStatus(g())}catch{}if(o.emit)for(const t of h.values())v(t.errors?"warning":"info",`traffic ${t.host} \u2192 ${t.service}${t.version?"@"+t.version:""}: ${t.count} req`,`gateway-traffic:${t.host}->${t.service}${t.version?"@"+t.version:""}`,{kind:"gateway-traffic",host:t.host,project:t.service,caller:t.caller,version:t.version,count:t.count,avgMs:Math.round(t.totalMs/t.count),errors:t.errors});h.clear()},o.trafficFlushMs??5e3);return e.unref?.(),e},"startFlusher")}}p(createObservability,"createObservability"),c(createObservability,"createObservability");
1
+ "use strict";var S=Object.defineProperty;var p=(a,d)=>S(a,"name",{value:d,configurable:!0});var y=Object.defineProperty,c=p((a,d)=>y(a,"name",{value:d,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createObservability=createObservability;const redact_1=require("@lensmcp/core/redact"),edge_1=require("./edge");function createObservability(a,d){const v=c((e,s,t,r,o)=>{if(a.emit)try{a.emit({id:(0,edge_1.eid)(),sessionId:"pending",timestamp:Date.now(),source:"gateway",category:"cluster",severity:e,context:(0,redact_1.redactDeep)({sessionId:"pending",...o}),fingerprint:t,title:(0,redact_1.safeTitle)(s),raw:r&&(0,redact_1.redactDeep)(r)})}catch{}},"emit"),h=new Map,m=c((e,s,t,r,o)=>{const n=e.host??"(default)",u=(r?r+"\u21D2":"")+n+"\u2192"+e.service+(o?"@"+o:""),i=h.get(u)??{host:n,service:e.service,caller:r,version:o,count:0,totalMs:0,errors:0};i.count+=1,i.totalMs+=s,t!==void 0&&(t>=500||t===0)&&(i.errors+=1),h.set(u,i)},"recordEdge"),q=Math.max(1,a.statusWindows??12),f=[],w=new Map,M=c(()=>[...new Set(d().map(e=>e.service))],"routeServices"),$=c(()=>{const e=new Map;for(const t of h.values()){const r=e.get(t.service)??{requests:0,errors:0,totalMs:0,versions:new Set};r.requests+=t.count,r.errors+=t.errors,r.totalMs+=t.totalMs,t.version&&r.versions.add(t.version),e.set(t.service,r)}const s=Date.now();for(const[t,r]of e)r.requests>0&&w.set(t,s);for(f.push(e);f.length>q;)f.shift()},"pushStatusWindow"),g=c(()=>{const e=new Map;for(const t of f)for(const[r,o]of t){const n=e.get(r)??{requests:0,errors:0,totalMs:0,versions:new Set};n.requests+=o.requests,n.errors+=o.errors,n.totalMs+=o.totalMs;for(const u of o.versions)n.versions.add(u);e.set(r,n)}const s=d();return M().map(t=>{const r=e.get(t)??{requests:0,errors:0,totalMs:0,versions:new Set},o=[...new Set(s.filter(i=>i.service===t).map(i=>i.host??"(default)"))],n=r.requests?r.errors/r.requests:0;let u=r.requests===0?"idle":n>=.5?"down":n>=.05?"degraded":"healthy";if(a.healthChecker){const i=s.filter(l=>l.service===t&&l.upstream).map(l=>l.upstream);i.length&&i.every(l=>!a.healthChecker.isHealthy(l))&&(u="down")}return{service:t,hosts:o,status:u,requests:r.requests,errorRate:+n.toFixed(4),avgMs:r.requests?Math.round(r.totalMs/r.requests):0,versions:[...r.versions],lastSeen:w.get(t)??null}})},"buildStatusSnapshot");return{emit:v,beginTrace:c(e=>{if(!a.emit)return;const s=(0,edge_1.hdr)(e.headers["x-lensmcp-flow-id"]);let t=(0,edge_1.hdr)(e.headers["x-request-id"]);if(!(!s&&!t&&!(0,edge_1.hdr)(e.headers.traceparent)))return t||(t=(0,edge_1.eid)(),e.headers["x-request-id"]=t),{startedAt:Date.now(),method:(e.method??"GET").toUpperCase(),host:(e.headers.host||"").split(":")[0],url:e.url??"/",steps:[],...s?{flowId:s}:{},...t?{requestId:t}:{}}},"beginTrace"),step:c((e,s,t)=>{e&&e.steps.push({step:s,atMs:Date.now()-e.startedAt,...t})},"step"),finish:c((e,s,t,r)=>{if(!e||e.done)return;e.done=!0;const o=Date.now()-e.startedAt;v(t===void 0?"warning":t>=500?"error":"info",t===void 0?`gateway ${e.method} ${e.host}${e.url} \u2192 no response: the caller went away after ${o}ms`:`gateway ${e.method} ${e.host}${e.url} \u2192 ${t} (${o}ms)`,`gateway-request:${s}`,{kind:"gateway-request",project:s,host:e.host,method:e.method,url:e.url,...t===void 0?{}:{status:t},durationMs:o,startedAt:e.startedAt,steps:e.steps,...r},{...e.flowId?{flowId:e.flowId}:{},...e.requestId?{requestId:e.requestId}:{}})},"finish"),recordEdge:m,buildStatusSnapshot:g,startFlusher:c(()=>{const e=setInterval(()=>{if($(),a.onStatus)try{a.onStatus(g())}catch{}if(a.emit)for(const s of h.values())v(s.errors?"warning":"info",`traffic ${s.host} \u2192 ${s.service}${s.version?"@"+s.version:""}: ${s.count} req`,`gateway-traffic:${s.host}->${s.service}${s.version?"@"+s.version:""}`,{kind:"gateway-traffic",host:s.host,project:s.service,caller:s.caller,version:s.version,count:s.count,avgMs:Math.round(s.totalMs/s.count),errors:s.errors});h.clear()},a.trafficFlushMs??5e3);return e.unref?.(),e},"startFlusher")}}p(createObservability,"createObservability"),c(createObservability,"createObservability");
@@ -0,0 +1,42 @@
1
+ import type { ServiceCtl, ServiceWake } from './types';
2
+ /** The longest hold: a forgotten one costs its memory for no longer than this, plus one idle window. */
3
+ export declare const MAX_AWAKE_SEC: number;
4
+ /**
5
+ * When the idle sweep would stop `svc` (ms): a whole window after the later of its last request and its hold's end —
6
+ * so never before the hold ends. `undefined` when it has no window (it never idle-stops).
7
+ */
8
+ export declare function idleStopAt(svc: Pick<ServiceCtl, 'lastUsed' | 'awakeUntil'>, idleSec: number | undefined): number | undefined;
9
+ /** What `/status` and `/awake` say of a service's idle clock. `idleStopAt` only for an up tree this gateway would stop. */
10
+ export declare function idleFields(svc: Pick<ServiceCtl, 'state' | 'proc' | 'lastUsed' | 'awakeUntil' | 'lastIdleStop' | 'wake'>, idleSec: number | undefined, now: number): {
11
+ idleSec?: number;
12
+ idleStopAt?: number;
13
+ awakeUntil?: number;
14
+ lastIdleStop?: {
15
+ at: number;
16
+ why: string;
17
+ };
18
+ wake?: ServiceWake;
19
+ };
20
+ export interface WakeDeps {
21
+ /** Start it and wait until it answers — control.ts's `POST /service` start: `true` once it does. */
22
+ start(svc: ServiceCtl): Promise<boolean>;
23
+ /** warm-start.ts `availableMemory`. */
24
+ available(): number | undefined;
25
+ /** warm-start.ts `warmMinFreeBytes`. */
26
+ minFreeBytes: number;
27
+ /** Is this service still its workspace's — not unregistered, not replaced, the gateway not stopping? */
28
+ current(svc: ServiceCtl): boolean;
29
+ /** Is a stop, start or restart of it under way (what `POST /service` refuses another for)? */
30
+ busy(svc: ServiceCtl): boolean;
31
+ /** Onto the service's own bus, as the lifecycle's events go. */
32
+ emit(svc: ServiceCtl, severity: 'info' | 'warning', title: string, raw: Record<string, unknown>): void;
33
+ log(line: string): void;
34
+ }
35
+ /**
36
+ * Start each queued service, ONE AT A TIME, each awaited, while the memory floor holds — writing how it went on its
37
+ * `wake` record. A service whose record a newer wake replaced is that wake's to start. Never throws.
38
+ */
39
+ export declare function wakeServices(queued: ReadonlyArray<{
40
+ svc: ServiceCtl;
41
+ wake: ServiceWake;
42
+ }>, deps: WakeDeps): Promise<void>;
@@ -0,0 +1 @@
1
+ "use strict";var k=Object.defineProperty;var d=(t,e)=>k(t,"name",{value:e,configurable:!0});var p=Object.defineProperty,l=d((t,e)=>p(t,"name",{value:e,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.MAX_AWAKE_SEC=void 0,exports.idleStopAt=idleStopAt,exports.idleFields=idleFields,exports.wakeServices=wakeServices;const warm_start_1=require("./warm-start");exports.MAX_AWAKE_SEC=14400;function idleStopAt(t,e){if(e)return Math.max(t.lastUsed||0,t.awakeUntil??0)+e*1e3}d(idleStopAt,"idleStopAt"),l(idleStopAt,"idleStopAt");function idleFields(t,e,n){const s=t.state==="up"&&t.proc?idleStopAt(t,e):void 0;return{...e?{idleSec:e}:{},...s!==void 0?{idleStopAt:s}:{},...(t.awakeUntil??0)>n?{awakeUntil:t.awakeUntil}:{},...t.lastIdleStop?{lastIdleStop:t.lastIdleStop}:{},...t.wake?{wake:t.wake}:{}}}d(idleFields,"idleFields"),l(idleFields,"idleFields");async function wakeServices(t,e){const n=l(a=>a.svc.wake===a.wake,"mine"),s=l((a,r,i)=>{a.wake={at:a.wake.at,outcome:r,...i?{reason:i}:{}},a.svc.wake=a.wake},"settle"),u=t.map(a=>({...a}));for(let a=0;a<u.length;a++){const r=u[a];if(!n(r))continue;const{svc:i}=r;if(!e.current(i)){for(const f of u.slice(a).filter(n))s(f,"failed","its workspace left the gateway before its turn");return}if(i.state==="up"){s(r,"up");continue}if(i.manualStopped){s(r,"skipped","stopped by hand while it waited its turn");continue}if(e.busy(i)){s(r,"skipped","a stop, start or restart of it was under way");continue}const c=(0,warm_start_1.belowFloor)(e.available(),e.minFreeBytes);if(c){const f=u.slice(a).filter(o=>n(o)&&o.svc.state!=="up");e.log(`[gateway] wake of ${i.wsKey} stopped: ${c} \u2014 ${f.map(o=>o.svc.project).join(", ")} start on their first request.`);for(const o of f)s(o,"deferred",c),e.emit(o.svc,"info",`wake deferred: ${o.svc.project} \u2014 ${c}`,{kind:"service-warm-deferred",project:o.svc.project,reason:c,minFreeBytes:e.minFreeBytes});return}s(r,"starting");const w=await e.start(i).catch(()=>!1);n(r)&&(w&&(i.lastUsed=Date.now()),s(r,w?"started":"failed",w?void 0:i.actionFailure??"it did not become ready"))}}d(wakeServices,"wakeServices"),l(wakeServices,"wakeServices");
@@ -1,6 +1,6 @@
1
1
  import type { ActStore } from './act';
2
2
  import type { ServiceLayer } from './lifecycle';
3
- import type { GatewayRuntime } from './types';
3
+ import type { GatewayRuntime, ServiceCtl } from './types';
4
4
  import type { WorkspaceFragment } from './route-registry';
5
5
  /** What `/list` and `/status` say about a workspace that is a worktree instance; nothing for any other. */
6
6
  export declare function instanceSummary(f: WorkspaceFragment | undefined): {
@@ -19,6 +19,16 @@ export interface ControlServerDeps {
19
19
  /** Restart one service (the svcLayer's `restartService`); `POST /restart` answers 501 without it. */
20
20
  restartService?: ServiceLayer['restartService'];
21
21
  ensureService?: ServiceLayer['ensureUp'];
22
+ /**
23
+ * The machine's one start at a time (warm-start.ts `createWorktreeWarmer().take`): a `POST /awake` start waits its
24
+ * turn behind the worktree warm starts. Without it, the starts this control plane asks for take turns among themselves.
25
+ */
26
+ startTurn?: <T>(job: () => Promise<T>) => Promise<T>;
27
+ /** Onto a service's own bus, as the lifecycle's events go (lifecycle.ts `emitSvc`) — `service-awake`; none without it. */
28
+ emitService?: (svc: ServiceCtl, severity: 'info' | 'warning', title: string, raw: Record<string, unknown>) => void;
29
+ /** The memory the system can still give out (warm-start.ts `availableMemory`) and the floor a start keeps. */
30
+ available?: () => number | undefined;
31
+ minFreeBytes?: number;
22
32
  /** Who signed in, and tickets to act as them (act.ts); `/identities` and `/act` answer 501 without it. */
23
33
  act?: ActStore;
24
34
  /** Hooks so the daemon can host/tear-down a workspace's dashboard around the route merge (P3d).
@@ -1,2 +1,2 @@
1
- "use strict";var O=Object.defineProperty;var w=(i,t)=>O(i,"name",{value:t,configurable:!0});var G=Object.defineProperty,f=w((i,t)=>G(i,"name",{value:t,configurable:!0}),"f");Object.defineProperty(exports,"__esModule",{value:!0}),exports.clusterVersion=void 0,exports.instanceSummary=instanceSummary,exports.controlSocketPath=controlSocketPath,exports.versionAtLoad=versionAtLoad,exports.readClusterVersion=readClusterVersion,exports.ensureOwnerOnlyDir=ensureOwnerOnlyDir,exports.startControlServer=startControlServer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),guest_graph_1=require("./guest-graph"),guest_replay_1=require("./guest-replay"),registration_guards_1=require("./registration-guards"),scope_1=require("./scope"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),workspace_middleware_1=require("./workspace-middleware");function instanceSummary(i){const t=i?.instance;return t?{instance:t.name,baseKey:t.baseKey,apex:t.apex,baseApex:t.baseApex,...t.frozen?{frozen:!0}:{}}:{}}w(instanceSummary,"instanceSummary"),f(instanceSummary,"instanceSummary");function controlSocketPath(){const i=process.env.LENSMCP_HOME||os.homedir();return path.join(i,".lensmcp","control.sock")}w(controlSocketPath,"controlSocketPath"),f(controlSocketPath,"controlSocketPath");const CONTROL_BODY_BYTES=1e6;class BodyTooLarge extends Error{static{w(this,"BodyTooLarge")}static{f(this,"BodyTooLarge")}constructor(t){super(`body too large (over ${t} bytes)`),this.limit=t}}exports.clusterVersion=versionAtLoad(()=>readClusterVersion(__dirname));function versionAtLoad(i){const t=i();return()=>t}w(versionAtLoad,"versionAtLoad"),f(versionAtLoad,"versionAtLoad");function readClusterVersion(i){let t=i;for(let l=0;l<6;l++){const h=path.join(t,"package.json");if(fs.existsSync(h))try{const g=JSON.parse(fs.readFileSync(h,"utf8"));if(g.name==="@lensmcp/cluster"&&typeof g.version=="string")return g.version}catch{}const a=path.dirname(t);if(a===t)break;t=a}}w(readClusterVersion,"readClusterVersion"),f(readClusterVersion,"readClusterVersion");function readJsonBody(i,t=CONTROL_BODY_BYTES){return new Promise((l,h)=>{const a=[];let g=0,S=!1;i.on("data",v=>{if(!S){if(g+=v.length,g>t){S=!0,a.length=0,h(new BodyTooLarge(t));return}a.push(v)}}),i.on("end",()=>{if(S)return;const v=Buffer.concat(a).toString("utf8");try{l({value:v?JSON.parse(v):{},bytes:g})}catch(k){h(k)}}),i.on("error",h)})}w(readJsonBody,"readJsonBody"),f(readJsonBody,"readJsonBody");function graphShare(i,t){return t-Buffer.byteLength(JSON.stringify({...i,projectGraph:null}))}w(graphShare,"graphShare"),f(graphShare,"graphShare");function graphVerdict(i){return i?i.refused===void 0?{forwarded:!0,accepted:!0}:{forwarded:!0,accepted:!1,reason:i.refused}:{forwarded:!1,accepted:!1}}w(graphVerdict,"graphVerdict"),f(graphVerdict,"graphVerdict");function ensureOwnerOnlyDir(i){try{fs.mkdirSync(i,{recursive:!0,mode:448})}catch{}try{const t=fs.lstatSync(i);return t.isDirectory()?((t.mode&63)!==0&&fs.chmodSync(i,448),!0):!1}catch{return!1}}w(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),f(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(i){const{rt:t}=i,l=controlSocketPath(),h=path.dirname(l);ensureOwnerOnlyDir(h)||console.warn(`[gateway] control home ${h} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(l)}catch{}const a=f((e,n,c)=>{e.writeHead(n,{"content-type":"application/json"}),e.end(JSON.stringify(c))},"send"),g=f(async(e,n,c=!1)=>{if(!e.wsKey||!e.root)return{status:400,json:{error:"wsKey and root are required"}};const r=e.projects??{},s=(0,discovery_1.projectSourceRoots)(e.root,r,e.projectDependencies),o=(0,scope_1.readLensScope)(e.root),u=o.key===e.wsKey,p=u&&o.sockNs!==e.wsKey?o.sockNs:void 0,{routes:y,services:d,instance:j}=(0,discovery_1.discoverRoutes)(e.root,r,void 0,e.wsKey,s,p,u?o.instance:void 0),b=(0,workspace_middleware_1.findGatewayMiddleware)(e.root,r),$={wsKey:e.wsKey,...p?{sockNs:p}:{},root:e.root,routes:y,services:d,...j?{instance:j}:{},...b?{middleware:b}:{}},_=(0,registration_guards_1.refuseRegistration)(t.registry,$);if(_)return console.warn(`[gateway] refused to register '${e.wsKey}' from ${e.root}: ${_.error}`),{status:409,json:_};if(c)return j?{status:200,json:{wsKey:e.wsKey,root:e.root,...instanceSummary($),env:j.env}}:{status:400,json:{error:`'${e.wsKey}' has no resolvable instance configuration`}};try{const m=(0,service_keys_1.adoptServiceKeys)(t,e.root,d,e.wsKey);m.added.length>0&&console.log(`[gateway] adopted east-west keys for ${e.wsKey}: ${m.added.join(", ")}`),m.regenerated.length>0&&console.warn(`[gateway] ${e.root} carried another workspace's east-west keys (a copied key file) \u2014 new keys for ${e.wsKey}: ${m.regenerated.join(", ")}`)}catch(m){console.error(`[gateway] could not adopt east-west keys for ${e.wsKey}: ${m.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(e.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${e.wsKey}') is served by the daemon at ${t.root} (pid ${process.pid}); its gateway-level log is ${path.join(t.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
2
- `)}catch{}let K;if(t.graphStores){const m={root:e.root,daemonRoot:t.root,otherRoots:t.registry.keys().filter(T=>T!==e.wsKey).flatMap(T=>t.registry.get(T)?.root??[])};K=e.projectGraph===void 0?void 0:(0,guest_graph_1.admitForwardedGraph)(e.projectGraph,{...m,bytes:graphShare(e,n)}),K?.refused!==void 0&&console.warn(`[gateway] refused the project graph '${e.wsKey}' forwarded from ${e.root}: ${K.refused} \u2014 its services start through \`nx run\` until the gateway computes it`),(0,registration_guards_1.sameRoot)(e.root,t.root)||t.graphStores.set((0,guest_graph_1.createGuestGraphStore)({wsKey:e.wsKey,root:e.root,...K?{admission:K}:{},context:m}))}return t.registry.register($),await i.onRegister?.($),t.rebuildRoutes(),t.root&&!(0,registration_guards_1.sameRoot)(e.root,t.root)&&(0,guest_replay_1.recordGuest)({wsKey:e.wsKey,root:e.root,projects:r,...e.projectDependencies?{projectDependencies:e.projectDependencies}:{},daemonRoot:t.root,registeredAt:new Date().toISOString()}),{status:200,json:{ok:!0,wsKey:e.wsKey,routes:y.length,services:d.length,workspaces:t.registry.keys(),...t.graphStores?{projectGraph:graphVerdict(K)}:{}}}},"register"),S=f((e,n)=>{const c=t.registry.get(e);if(!c)return{error:`no workspace '${e}' is registered`,workspaces:t.registry.keys()};const r=c.services.find(s=>s.project===n);return r?{svc:r}:{error:`'${e}' has no service '${n}'`,services:c.services.map(s=>s.project)}},"findService"),v=f(e=>({wsKey:e.wsKey,service:e.project,state:e.state,...e.generation!==void 0?{generation:e.generation}:{},...e.restart?{restart:e.restart}:{}}),"restartView"),k=http.createServer((e,n)=>{(async()=>{try{const c=(e.url??"").split("?")[0];if(e.method==="GET"&&c==="/list")return a(n,200,{workspaces:t.registry.keys().map(r=>{const s=t.registry.get(r);return{key:r,root:s?.root,routes:s?.routes.length??0,services:s?.services.length??0,...instanceSummary(s)}})});if(e.method==="GET"&&c==="/status"){const r=(0,exports.clusterVersion)();return a(n,200,{daemon:{pid:process.pid,wsKey:t.wsKey,...r?{version:r}:{}},workspaces:t.registry.keys().map(s=>{const o=t.registry.get(s);return{key:s,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0,isDaemon:s===t.wsKey,...instanceSummary(o)}}),services:t.registry.allServices().map(s=>({project:s.project,wsKey:s.wsKey,host:s.decl.host,state:s.state,...s.generation!==void 0?{generation:s.generation}:{},...s.restart?{restart:s.restart}:{}}))})}if(e.method==="GET"&&c==="/instance-env"){const r=new URL(e.url??"/","http://control").searchParams.get("wsKey")??"",s=t.registry.get(r);return s?s.instance?a(n,200,{wsKey:r,root:s.root,...instanceSummary(s),env:s.instance.env}):a(n,404,{error:`'${r}' is not an instance`}):a(n,404,{error:`no workspace '${r}' is registered`})}if(e.method==="POST"&&c==="/instance-env"){const{value:r,bytes:s}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),o=await g(r,s,!0);return a(n,o.status,o.json)}if(c==="/service"&&(e.method==="GET"||e.method==="POST")){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{};if(typeof r.wsKey!="string"||typeof r.service!="string")return a(n,400,{error:"wsKey and service are required"});const s=S(r.wsKey,r.service);if("error"in s)return a(n,404,s);const o=s.svc,u=f(()=>{let d=(o.stoppingUntil??0)>Date.now();if(o.stoppingPid)try{process.kill(o.stoppingPid,0),d=!0}catch{o.stoppingPid=void 0}return{service:o.project,state:o.state,stopped:o.manualStopped===!0&&!d,ready:!o.manualStopped&&o.state==="up"&&o.pool.socks.length>0,busy:d||o.actionStarting===!0||!!o.rolling||!!(o.restart&&!o.restart.done),failure:o.actionFailure??(!o.manualStopped&&o.restart?.done?.ok===!1?o.restart.done.error:void 0)}},"view");if(e.method==="GET")return a(n,200,u());const p=r.action;if(!["start","stop","restart"].includes(String(p)))return a(n,400,{error:"action must be start, stop or restart"});if(u().busy&&!(p==="stop"&&o.actionStarting&&!o.rolling))return a(n,409,{error:"A service lifecycle operation is already in progress"});if(p==="stop")return!o.proc&&o.pool.socks.length&&!o.manualStopped?a(n,409,{error:"This gateway does not own the adopted service process; stop it with its owner"}):(o.manualStopped=!0,o.stoppingPid=o.proc?.pid,o.stoppingUntil=Date.now()+(o.proc?1600:0),i.killService(o,"workspace action stop"),o.state="down",a(n,202,u()));if(p==="restart"){if(!i.restartService)return a(n,501,{error:"This gateway cannot restart services"});const d=i.restartService(o,{hard:r.hard===!0,reason:"workspace action restart"});return"refused"in d?a(n,409,{error:d.refused}):a(n,202,u())}if(!i.ensureService)return a(n,501,{error:"This gateway cannot start services"});const y=o.state!=="up"&&!o.proc;return o.manualStopped=!1,o.actionFailure=void 0,o.restart=void 0,o.actionStarting=!0,i.ensureService(o).then(d=>{!d&&!o.manualStopped&&(o.actionFailure="Service did not become ready")},d=>{o.actionFailure=String(d).slice(0,500)}).finally(()=>{o.actionStarting=!1}),a(n,202,{...u(),started:y})}if(c==="/restart"&&(e.method==="POST"||e.method==="GET")){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},s=typeof r.wsKey=="string"?r.wsKey:"",o=typeof r.service=="string"?r.service:"";if(!s||!o)return a(n,400,{error:"wsKey and service are required"});const u=S(s,o);if("error"in u)return a(n,404,u);const p=u.svc;if(e.method==="GET")return a(n,200,v(p));if(!i.restartService)return a(n,501,{error:"this gateway cannot restart a service"});const y=typeof r.reason=="string"&&r.reason.trim()?r.reason.trim().slice(0,200):"asked for",d=i.restartService(p,{hard:r.hard===!0,reason:y});return"refused"in d?a(n,409,{error:d.refused,...v(p)}):a(n,202,{ok:!0,already:d.already,...v(p)})}if(e.method==="GET"&&c==="/identities"||e.method==="POST"&&c==="/act"){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},s=typeof r.wsKey=="string"?r.wsKey:"";if(!s)return a(n,400,{error:"wsKey is required"});if(!t.registry.get(s)&&s!==t.wsKey)return a(n,404,{error:`no workspace '${s}' is registered`,workspaces:t.registry.keys()});if(!i.act)return a(n,501,{error:"acting as a signed-in user is turned off on this gateway (LENSMCP_GW_ACT=0)"});if(e.method==="GET")return a(n,200,{wsKey:s,identities:i.act.list(s)});const o=f(d=>typeof r[d]=="string"&&r[d].trim()?r[d].trim().slice(0,256):void 0,"pick"),u=o("user"),p=o("tenant"),y=i.act.mint(s,{...u?{user:u}:{},...p?{tenant:p}:{}});return"error"in y?a(n,404,{wsKey:s,...y}):a(n,200,{wsKey:s,...y})}if(e.method==="POST"&&c==="/register"){const{value:r,bytes:s}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),o=await g(r,s);return a(n,o.status,o.json)}if(e.method==="POST"&&c==="/unregister"){const r=(await readJsonBody(e)).value;if(!r.wsKey)return a(n,400,{error:"wsKey is required"});const s=(0,registration_guards_1.refuseUnregistration)(t.registry,{wsKey:r.wsKey,...r.root?{root:r.root}:{}},t.wsKey);if(s)return console.warn(`[gateway] refused to unregister '${r.wsKey}'${r.root?` for ${r.root}`:""}: ${s.error}`),a(n,409,s);const o=t.registry.unregister(r.wsKey);if(o){for(const u of o.services){const p=(0,service_keys_1.serviceKeyId)(u.wsKey??r.wsKey,u.project),y=t.serviceKeys[p];y&&(delete t.serviceKeys[p],t.keyToProject.delete(y),t.keyToWorkspace?.delete(y))}for(const u of o.services)i.killService(u,"workspace unregistered");t.graphStores?.delete(o.root),(0,guest_replay_1.forgetGuest)(r.wsKey),i.onUnregister?.(o)}return t.rebuildRoutes(),a(n,200,{ok:!0,removed:!!o,workspaces:t.registry.keys()})}a(n,404,{error:"not found"})}catch(c){if(c instanceof BodyTooLarge)return e.resume(),a(n,413,{error:c.message});a(n,500,{error:c.message})}})()});return k.listen(l,()=>{try{fs.chmodSync(l,384)}catch(e){console.warn(`[gateway] could not restrict ${l} to 0600:`,e.message)}}),{socketPath:l,async replay(){const{replay:e,gone:n}=(0,guest_replay_1.guestsToReplay)(t.root,(0,guest_replay_1.readGuests)()),c={replayed:[],refused:[],gone:n.map(r=>r.wsKey)};for(const r of n)(0,guest_replay_1.forgetGuest)(r.wsKey),console.log(`[gateway] not re-hosting '${r.wsKey}': ${r.root} is no longer a workspace \u2014 forgotten`);for(const r of e)if(!t.registry.has(r.wsKey))try{const s=await g({wsKey:r.wsKey,root:r.root,projects:r.projects,...r.projectDependencies?{projectDependencies:r.projectDependencies}:{}},0);s.status===200?(c.replayed.push(r.wsKey),console.log(`[gateway] re-hosted '${r.wsKey}' (${r.root}) \u2014 registered before this daemon restarted`)):(c.refused.push(r.wsKey),console.warn(`[gateway] could not re-host '${r.wsKey}' (${r.root}): ${JSON.stringify(s.json)} \u2014 \`lensmcp gateway start\` there`))}catch(s){c.refused.push(r.wsKey),console.warn(`[gateway] could not re-host '${r.wsKey}' (${r.root}): ${s.message}`)}return c},close:f(()=>{k.close();try{fs.unlinkSync(l)}catch{}},"close")}}w(startControlServer,"startControlServer"),f(startControlServer,"startControlServer");
1
+ "use strict";var q=Object.defineProperty;var h=(c,t)=>q(c,"name",{value:t,configurable:!0});var x=Object.defineProperty,l=h((c,t)=>x(c,"name",{value:t,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.clusterVersion=void 0,exports.instanceSummary=instanceSummary,exports.controlSocketPath=controlSocketPath,exports.versionAtLoad=versionAtLoad,exports.readClusterVersion=readClusterVersion,exports.ensureOwnerOnlyDir=ensureOwnerOnlyDir,exports.startControlServer=startControlServer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),guest_graph_1=require("./guest-graph"),guest_replay_1=require("./guest-replay"),registration_guards_1=require("./registration-guards"),scope_1=require("./scope"),awake_1=require("./awake"),warm_start_1=require("./warm-start"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),workspace_middleware_1=require("./workspace-middleware");function instanceSummary(c){const t=c?.instance;return t?{instance:t.name,baseKey:t.baseKey,apex:t.apex,baseApex:t.baseApex,...t.frozen?{frozen:!0}:{}}:{}}h(instanceSummary,"instanceSummary"),l(instanceSummary,"instanceSummary");function controlSocketPath(){const c=process.env.LENSMCP_HOME||os.homedir();return path.join(c,".lensmcp","control.sock")}h(controlSocketPath,"controlSocketPath"),l(controlSocketPath,"controlSocketPath");const CONTROL_BODY_BYTES=1e6;class BodyTooLarge extends Error{static{h(this,"BodyTooLarge")}static{l(this,"BodyTooLarge")}constructor(t){super(`body too large (over ${t} bytes)`),this.limit=t}}exports.clusterVersion=versionAtLoad(()=>readClusterVersion(__dirname));function versionAtLoad(c){const t=c();return()=>t}h(versionAtLoad,"versionAtLoad"),l(versionAtLoad,"versionAtLoad");function readClusterVersion(c){let t=c;for(let f=0;f<6;f++){const k=path.join(t,"package.json");if(fs.existsSync(k))try{const v=JSON.parse(fs.readFileSync(k,"utf8"));if(v.name==="@lensmcp/cluster"&&typeof v.version=="string")return v.version}catch{}const i=path.dirname(t);if(i===t)break;t=i}}h(readClusterVersion,"readClusterVersion"),l(readClusterVersion,"readClusterVersion");function readJsonBody(c,t=CONTROL_BODY_BYTES){return new Promise((f,k)=>{const i=[];let v=0,_=!1;c.on("data",S=>{if(!_){if(v+=S.length,v>t){_=!0,i.length=0,k(new BodyTooLarge(t));return}i.push(S)}}),c.on("end",()=>{if(_)return;const S=Buffer.concat(i).toString("utf8");try{f({value:S?JSON.parse(S):{},bytes:v})}catch(T){k(T)}}),c.on("error",k)})}h(readJsonBody,"readJsonBody"),l(readJsonBody,"readJsonBody");function graphShare(c,t){return t-Buffer.byteLength(JSON.stringify({...c,projectGraph:null}))}h(graphShare,"graphShare"),l(graphShare,"graphShare");function graphVerdict(c){return c?c.refused===void 0?{forwarded:!0,accepted:!0}:{forwarded:!0,accepted:!1,reason:c.refused}:{forwarded:!1,accepted:!1}}h(graphVerdict,"graphVerdict"),l(graphVerdict,"graphVerdict");function ensureOwnerOnlyDir(c){try{fs.mkdirSync(c,{recursive:!0,mode:448})}catch{}try{const t=fs.lstatSync(c);return t.isDirectory()?((t.mode&63)!==0&&fs.chmodSync(c,448),!0):!1}catch{return!1}}h(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),l(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(c){const{rt:t}=c,f=controlSocketPath(),k=path.dirname(f);ensureOwnerOnlyDir(k)||console.warn(`[gateway] control home ${k} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(f)}catch{}const i=l((e,s,u)=>{e.writeHead(s,{"content-type":"application/json"}),e.end(JSON.stringify(u))},"send"),v=l(async(e,s,u=!1)=>{if(!e.wsKey||!e.root)return{status:400,json:{error:"wsKey and root are required"}};const r=e.projects??{},o=(0,discovery_1.projectSourceRoots)(e.root,r,e.projectDependencies),a=(0,scope_1.readLensScope)(e.root),d=a.key===e.wsKey,y=d&&a.sockNs!==e.wsKey?a.sockNs:void 0,{routes:p,services:w,instance:m}=(0,discovery_1.discoverRoutes)(e.root,r,void 0,e.wsKey,o,y,d?a.instance:void 0),b=(0,workspace_middleware_1.findGatewayMiddleware)(e.root,r),$={wsKey:e.wsKey,...y?{sockNs:y}:{},root:e.root,routes:p,services:w,...m?{instance:m}:{},...b?{middleware:b}:{}},K=(0,registration_guards_1.refuseRegistration)(t.registry,$);if(K)return console.warn(`[gateway] refused to register '${e.wsKey}' from ${e.root}: ${K.error}`),{status:409,json:K};if(u)return m?{status:200,json:{wsKey:e.wsKey,root:e.root,...instanceSummary($),env:m.env}}:{status:400,json:{error:`'${e.wsKey}' has no resolvable instance configuration`}};try{const n=(0,service_keys_1.adoptServiceKeys)(t,e.root,w,e.wsKey);n.added.length>0&&console.log(`[gateway] adopted east-west keys for ${e.wsKey}: ${n.added.join(", ")}`),n.regenerated.length>0&&console.warn(`[gateway] ${e.root} carried another workspace's east-west keys (a copied key file) \u2014 new keys for ${e.wsKey}: ${n.regenerated.join(", ")}`)}catch(n){console.error(`[gateway] could not adopt east-west keys for ${e.wsKey}: ${n.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(e.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${e.wsKey}') is served by the daemon at ${t.root} (pid ${process.pid}); its gateway-level log is ${path.join(t.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
2
+ `)}catch{}let j;if(t.graphStores){const n={root:e.root,daemonRoot:t.root,otherRoots:t.registry.keys().filter(g=>g!==e.wsKey).flatMap(g=>t.registry.get(g)?.root??[])};j=e.projectGraph===void 0?void 0:(0,guest_graph_1.admitForwardedGraph)(e.projectGraph,{...n,bytes:graphShare(e,s)}),j?.refused!==void 0&&console.warn(`[gateway] refused the project graph '${e.wsKey}' forwarded from ${e.root}: ${j.refused} \u2014 its services start through \`nx run\` until the gateway computes it`),(0,registration_guards_1.sameRoot)(e.root,t.root)||t.graphStores.set((0,guest_graph_1.createGuestGraphStore)({wsKey:e.wsKey,root:e.root,...j?{admission:j}:{},context:n}))}return t.registry.register($),await c.onRegister?.($),t.rebuildRoutes(),t.root&&!(0,registration_guards_1.sameRoot)(e.root,t.root)&&(0,guest_replay_1.recordGuest)({wsKey:e.wsKey,root:e.root,projects:r,...e.projectDependencies?{projectDependencies:e.projectDependencies}:{},daemonRoot:t.root,registeredAt:new Date().toISOString()}),{status:200,json:{ok:!0,wsKey:e.wsKey,routes:p.length,services:w.length,workspaces:t.registry.keys(),...t.graphStores?{projectGraph:graphVerdict(j)}:{}}}},"register"),_=l((e,s)=>{const u=t.registry.get(e);if(!u)return{error:`no workspace '${e}' is registered`,workspaces:t.registry.keys()};const r=u.services.find(o=>o.project===s);return r?{svc:r}:{error:`'${e}' has no service '${s}'`,services:u.services.map(o=>o.project)}},"findService"),S=l(e=>{let s=(e.stoppingUntil??0)>Date.now();if(e.stoppingPid)try{process.kill(e.stoppingPid,0),s=!0}catch{e.stoppingPid=void 0}return{service:e.project,state:e.state,stopped:e.manualStopped===!0&&!s,ready:!e.manualStopped&&e.state==="up"&&e.pool.socks.length>0,busy:s||e.actionStarting===!0||!!e.rolling||!!(e.restart&&!e.restart.done),failure:e.actionFailure??(!e.manualStopped&&e.restart?.done?.ok===!1?e.restart.done.error:void 0)}},"serviceView"),T=l((e,s)=>(e.manualStopped=!1,e.actionFailure=void 0,e.restart=void 0,e.actionStarting=!0,s(e).then(u=>(!u&&!e.manualStopped&&(e.actionFailure="Service did not become ready"),u),u=>(e.actionFailure=String(u).slice(0,500),!1)).finally(()=>{e.actionStarting=!1})),"startService"),O=l((e,s)=>({project:e.project,wsKey:e.wsKey,host:e.decl.host,state:e.state,...e.generation!==void 0?{generation:e.generation}:{},...e.restart?{restart:e.restart}:{},...(0,awake_1.idleFields)(e,(0,warm_start_1.serviceIdleSec)(t,e),s)}),"statusRow");let P=Promise.resolve();const A=c.startTurn??(e=>{const s=P.then(e);return P=s.catch(()=>{}),s}),E=l(e=>({wsKey:e.wsKey,service:e.project,state:e.state,...e.generation!==void 0?{generation:e.generation}:{},...e.restart?{restart:e.restart}:{}}),"restartView"),G=http.createServer((e,s)=>{(async()=>{try{const u=(e.url??"").split("?")[0];if(e.method==="GET"&&u==="/list")return i(s,200,{workspaces:t.registry.keys().map(r=>{const o=t.registry.get(r);return{key:r,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0,...instanceSummary(o)}})});if(e.method==="GET"&&u==="/status"){const r=(0,exports.clusterVersion)();return i(s,200,{daemon:{pid:process.pid,wsKey:t.wsKey,...r?{version:r}:{}},workspaces:t.registry.keys().map(o=>{const a=t.registry.get(o);return{key:o,root:a?.root,routes:a?.routes.length??0,services:a?.services.length??0,isDaemon:o===t.wsKey,...instanceSummary(a)}}),services:t.registry.allServices().map(o=>O(o,Date.now()))})}if(e.method==="GET"&&u==="/instance-env"){const r=new URL(e.url??"/","http://control").searchParams.get("wsKey")??"",o=t.registry.get(r);return o?o.instance?i(s,200,{wsKey:r,root:o.root,...instanceSummary(o),env:o.instance.env}):i(s,404,{error:`'${r}' is not an instance`}):i(s,404,{error:`no workspace '${r}' is registered`})}if(e.method==="POST"&&u==="/instance-env"){const{value:r,bytes:o}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),a=await v(r,o,!0);return i(s,a.status,a.json)}if(u==="/service"&&(e.method==="GET"||e.method==="POST")){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{};if(typeof r.wsKey!="string"||typeof r.service!="string")return i(s,400,{error:"wsKey and service are required"});const o=_(r.wsKey,r.service);if("error"in o)return i(s,404,o);const a=o.svc,d=l(()=>S(a),"view");if(e.method==="GET")return i(s,200,d());const y=r.action;if(!["start","stop","restart"].includes(String(y)))return i(s,400,{error:"action must be start, stop or restart"});if(d().busy&&!(y==="stop"&&a.actionStarting&&!a.rolling))return i(s,409,{error:"A service lifecycle operation is already in progress"});if(y==="stop")return!a.proc&&a.pool.socks.length&&!a.manualStopped?i(s,409,{error:"This gateway does not own the adopted service process; stop it with its owner"}):(a.manualStopped=!0,a.stoppingPid=a.proc?.pid,a.stoppingUntil=Date.now()+(a.proc?1600:0),c.killService(a,"workspace action stop"),a.state="down",i(s,202,d()));if(y==="restart"){if(!c.restartService)return i(s,501,{error:"This gateway cannot restart services"});const w=c.restartService(a,{hard:r.hard===!0,reason:"workspace action restart"});return"refused"in w?i(s,409,{error:w.refused}):i(s,202,{...d(),restart:w.restart})}if(!c.ensureService)return i(s,501,{error:"This gateway cannot start services"});const p=a.state!=="up"&&!a.proc;return T(a,c.ensureService),i(s,202,{...d(),started:p})}if(e.method==="POST"&&u==="/awake"){const r=(await readJsonBody(e)).value??{},o=typeof r.wsKey=="string"?r.wsKey:"";if(!o)return i(s,400,{error:"wsKey is required"});const a=typeof r.forSec=="number"?r.forSec:NaN;if(!Number.isFinite(a)||a<0)return i(s,400,{error:"forSec is required: the seconds to hold them awake, 0 to release the hold"});if(a>awake_1.MAX_AWAKE_SEC)return i(s,400,{error:`forSec ${a} is over the ${awake_1.MAX_AWAKE_SEC} s (4 h) a hold may last \u2014 ask again before it ends to keep them longer`,maxSec:awake_1.MAX_AWAKE_SEC});const d=r.services;if(d!==void 0&&(!Array.isArray(d)||d.length===0||!d.every(n=>typeof n=="string"&&n.length>0)))return i(s,400,{error:"services, when given, names one service or more"});const y=t.registry.get(o);if(!y)return i(s,404,{error:`no workspace '${o}' is registered`,workspaces:t.registry.keys()});const p=[];for(const n of d??[]){const g=_(o,n);if("error"in g)return i(s,404,g);p.includes(g.svc)||p.push(g.svc)}const w=p.length>0;w||p.push(...y.services);const m=Date.now();if(a===0){for(const n of p)(n.awakeUntil??0)<=m||(n.awakeUntil=m,c.emitService?.(n,"info",`awake hold released: ${n.project}`,{kind:"service-awake",project:n.project,until:null,released:!0}));return i(s,202,{ok:!0,wsKey:o,forSec:0,released:!0,services:p.map(n=>O(n,m))})}const b=m+Math.round(a*1e3),$=[],K=l((n,g)=>({at:m,outcome:n,...g?{reason:g}:{}}),"asked");for(const n of p)n.awakeUntil=Math.max(n.awakeUntil??0,b),n.state==="up"?n.wake=K("up"):S(n).busy?n.wake=K("skipped","a stop, start or restart of it is under way \u2014 `lensmcp instance status` shows how it ends"):n.manualStopped&&!w?n.wake=K("skipped","stopped by hand \u2014 name it to start it"):c.ensureService?(n.manualStopped=!1,n.wake=K("queued"),$.push({svc:n,wake:n.wake})):n.wake=K("failed","this gateway cannot start services"),c.emitService?.(n,"info",`awake until ${new Date(n.awakeUntil).toISOString()}: ${n.project}`,{kind:"service-awake",project:n.project,until:n.awakeUntil,forSec:a,wake:n.wake.outcome});const j=c.ensureService;return $.length>0&&j&&A(()=>(0,awake_1.wakeServices)($,{start:l(n=>T(n,j),"start"),available:c.available??(()=>(0,warm_start_1.availableMemory)()),minFreeBytes:c.minFreeBytes??(0,warm_start_1.warmMinFreeBytes)(),current:l(n=>!t.stopped?.()&&!!t.registry.get(n.wsKey)?.services.includes(n),"current"),busy:l(n=>S(n).busy,"busy"),emit:l((n,g,D,R)=>c.emitService?.(n,g,D,R),"emit"),log:l(n=>console.log(n),"log")})).catch(n=>console.error(`[gateway] wake of ${o} failed:`,n)),i(s,202,{ok:!0,wsKey:o,forSec:a,until:b,services:p.map(n=>O(n,m))})}if(u==="/restart"&&(e.method==="POST"||e.method==="GET")){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},o=typeof r.wsKey=="string"?r.wsKey:"",a=typeof r.service=="string"?r.service:"";if(!o||!a)return i(s,400,{error:"wsKey and service are required"});const d=_(o,a);if("error"in d)return i(s,404,d);const y=d.svc;if(e.method==="GET")return i(s,200,E(y));if(!c.restartService)return i(s,501,{error:"this gateway cannot restart a service"});const p=typeof r.reason=="string"&&r.reason.trim()?r.reason.trim().slice(0,200):"asked for",w=c.restartService(y,{hard:r.hard===!0,reason:p});return"refused"in w?i(s,409,{error:w.refused,...E(y)}):i(s,202,{ok:!0,already:w.already,...E(y)})}if(e.method==="GET"&&u==="/identities"||e.method==="POST"&&u==="/act"){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},o=typeof r.wsKey=="string"?r.wsKey:"";if(!o)return i(s,400,{error:"wsKey is required"});if(!t.registry.get(o)&&o!==t.wsKey)return i(s,404,{error:`no workspace '${o}' is registered`,workspaces:t.registry.keys()});if(!c.act)return i(s,501,{error:"acting as a signed-in user is turned off on this gateway (LENSMCP_GW_ACT=0)"});if(e.method==="GET")return i(s,200,{wsKey:o,identities:c.act.list(o)});const a=l(w=>typeof r[w]=="string"&&r[w].trim()?r[w].trim().slice(0,256):void 0,"pick"),d=a("user"),y=a("tenant"),p=c.act.mint(o,{...d?{user:d}:{},...y?{tenant:y}:{}});return"error"in p?i(s,404,{wsKey:o,...p}):i(s,200,{wsKey:o,...p})}if(e.method==="POST"&&u==="/register"){const{value:r,bytes:o}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),a=await v(r,o);return i(s,a.status,a.json)}if(e.method==="POST"&&u==="/unregister"){const r=(await readJsonBody(e)).value;if(!r.wsKey)return i(s,400,{error:"wsKey is required"});const o=(0,registration_guards_1.refuseUnregistration)(t.registry,{wsKey:r.wsKey,...r.root?{root:r.root}:{}},t.wsKey);if(o)return console.warn(`[gateway] refused to unregister '${r.wsKey}'${r.root?` for ${r.root}`:""}: ${o.error}`),i(s,409,o);const a=t.registry.unregister(r.wsKey);if(a){for(const d of a.services){const y=(0,service_keys_1.serviceKeyId)(d.wsKey??r.wsKey,d.project),p=t.serviceKeys[y];p&&(delete t.serviceKeys[y],t.keyToProject.delete(p),t.keyToWorkspace?.delete(p))}for(const d of a.services)c.killService(d,"workspace unregistered");t.graphStores?.delete(a.root),(0,guest_replay_1.forgetGuest)(r.wsKey),c.onUnregister?.(a)}return t.rebuildRoutes(),i(s,200,{ok:!0,removed:!!a,workspaces:t.registry.keys()})}i(s,404,{error:"not found"})}catch(u){if(u instanceof BodyTooLarge)return e.resume(),i(s,413,{error:u.message});i(s,500,{error:u.message})}})()});return G.listen(f,()=>{try{fs.chmodSync(f,384)}catch(e){console.warn(`[gateway] could not restrict ${f} to 0600:`,e.message)}}),{socketPath:f,async replay(){const{replay:e,gone:s}=(0,guest_replay_1.guestsToReplay)(t.root,(0,guest_replay_1.readGuests)()),u={replayed:[],refused:[],gone:s.map(r=>r.wsKey)};for(const r of s)(0,guest_replay_1.forgetGuest)(r.wsKey),console.log(`[gateway] not re-hosting '${r.wsKey}': ${r.root} is no longer a workspace \u2014 forgotten`);for(const r of e)if(!t.registry.has(r.wsKey))try{const o=await v({wsKey:r.wsKey,root:r.root,projects:r.projects,...r.projectDependencies?{projectDependencies:r.projectDependencies}:{}},0);o.status===200?(u.replayed.push(r.wsKey),console.log(`[gateway] re-hosted '${r.wsKey}' (${r.root}) \u2014 registered before this daemon restarted`)):(u.refused.push(r.wsKey),console.warn(`[gateway] could not re-host '${r.wsKey}' (${r.root}): ${JSON.stringify(o.json)} \u2014 \`lensmcp gateway start\` there`))}catch(o){u.refused.push(r.wsKey),console.warn(`[gateway] could not re-host '${r.wsKey}' (${r.root}): ${o.message}`)}return u},close:l(()=>{G.close();try{fs.unlinkSync(f)}catch{}},"close")}}h(startControlServer,"startControlServer"),l(startControlServer,"startControlServer");
@@ -1 +1 @@
1
- "use strict";var A=Object.defineProperty;var h=(e,s)=>A(e,"name",{value:s,configurable:!0});var I=Object.defineProperty,p=h((e,s)=>I(e,"name",{value:s,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.RETIRING_SUFFIX=void 0,exports.stopRoutingTo=stopRoutingTo,exports.pickSock=pickSock,exports.lensPortFromUpstream=lensPortFromUpstream,exports.rewriteInstanceDecls=rewriteInstanceDecls,exports.discoverRoutes=discoverRoutes,exports.projectSourceRoots=projectSourceRoots;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),manifest_1=require("../manifest"),pod_sock_1=require("../../../pod-sock"),instance_1=require("./instance"),scope_1=require("./scope");exports.RETIRING_SUFFIX=".retiring";function sockIdentity(e){try{const s=fs.statSync(e);return`${s.dev}:${s.ino}:${s.ctimeMs}`}catch{return}}h(sockIdentity,"sockIdentity"),p(sockIdentity,"sockIdentity");function stopRoutingTo(e,s=()=>!0){let a;try{a=fs.readdirSync(e.dir)}catch{a=[]}const r=a.filter(t=>(0,pod_sock_1.isPodSockName)(t)&&s(t)).map(t=>path.join(e.dir,t));if(r.length===0)return r;const l=e.retired??=new Map;for(const t of r){const d=sockIdentity(t);d!==void 0&&l.set(t,d)}const n=new Set(r);return e.socks=e.socks.filter(t=>!n.has(t)),e.idx=-1,r}h(stopRoutingTo,"stopRoutingTo"),p(stopRoutingTo,"stopRoutingTo");function stillRetired(e,s){const a=e.retired?.get(s);return a===void 0?!1:sockIdentity(s)===a?!0:(e.retired?.delete(s),!1)}h(stillRetired,"stillRetired"),p(stillRetired,"stillRetired");function pickSock(e,s=1500){const a=Date.now();if(a-e.scannedAt>s){try{const r=fs.readdirSync(e.dir),l=r.filter(t=>(0,pod_sock_1.isPodSockName)(t)&&!stillRetired(e,path.join(e.dir,t)));if(e.retired)for(const t of[...e.retired.keys()])r.includes(path.basename(t))||e.retired.delete(t);const n=l.filter(t=>!r.includes(t+exports.RETIRING_SUFFIX));e.socks=(n.length>0?n:l).map(t=>path.join(e.dir,t))}catch{e.socks=[]}e.scannedAt=a}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}h(pickSock,"pickSock"),p(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const s=/:(\d+)/.exec(e);return s?Number(s[1]):void 0}h(lensPortFromUpstream,"lensPortFromUpstream"),p(lensPortFromUpstream,"lensPortFromUpstream");function rewriteInstanceDecls(e,s){const a=e.flatMap(([,n])=>[...n.host?[n.host]:[],...typeof n.internalHost=="string"?[n.internalHost]:[]]),r=s.apex??(0,instance_1.commonApex)(a);if(!r)return console.warn(`[gateway] instance '${s.name}': the declared hosts share no apex \u2014 set \`instance.apex\` in .lensmcp/config.json; no host is served for it.`),{decls:e.map(([n,t])=>[n,{...t,host:void 0,internalHost:!1,default:!1}])};const l=[];for(const[n,t]of e){const d={...t};if(t.host){const c=(0,instance_1.rewriteHost)(t.host,r,s.name);c||console.warn(`[gateway] instance '${s.name}': ${n}'s host ${t.host} is outside ${r} \u2014 not served for the instance.`),d.host=c}if(typeof t.internalHost=="string"){const c=(0,instance_1.rewriteHost)(t.internalHost,r,s.name);c||console.warn(`[gateway] instance '${s.name}': ${n}'s internal host ${t.internalHost} is outside ${r} \u2014 not served for the instance.`),d.internalHost=c??!1}l.push([n,d])}return{decls:l,baseApex:r,apex:`${s.name}.${r}`}}h(rewriteInstanceDecls,"rewriteInstanceDecls"),p(rewriteInstanceDecls,"rewriteInstanceDecls");function discoverRoutes(e,s,a=os.tmpdir(),r="",l={},n=r,t){const d=n||r,c=p(i=>d?path.join(a,d,`${i}-devserver`):path.join(a,`${i}-devserver`),"sockDirFor"),u=[],g=[],_=[],H=[],S={};let y,w=[];const F=new Map;for(const[i,o]of Object.entries(s))try{const f=JSON.parse(fs.readFileSync(path.join(e,o.root,"project.json"),"utf8")),m=f.cluster??f.davnx;m&&(w.push([i,m]),F.set(i,o.root))}catch{continue}let v={};t&&({decls:w,...v}=rewriteInstanceDecls(w,t));for(const[i,o]of w){const f={root:F.get(i)};let m,j,k,R;if(!o.service&&o.sources!==void 0&&console.warn(`[gateway] ${i}: cluster.sources is read from pod (service) decls only \u2014 ignored.`),o.service)o.lens&&console.warn(`[gateway] ${i}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),m={dir:c(o.service),socks:[],idx:-1,scannedAt:0},j={project:i,decl:o,root:e,wsKey:r,...d!==r?{sockNs:d}:{},pool:m,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},g.push(j);else if(o.lens){const x=o.port??lensPortFromUpstream(o.upstream)??5173;k=`http://localhost:${x}`,R={project:i,...r?{workspaceKey:r}:{},workspaceRoot:e,projectRoot:path.join(e,f.root),port:x,...l[i]?.length?{sourceRoots:l[i]}:{}},_.push(R)}else if(k=o.upstream??(o.port?`http://localhost:${o.port}`:void 0),!k){console.warn(`[gateway] ${i}: no service/upstream/port \u2014 skipped.`);continue}const b={...r?{wsKey:r}:{},project:i,prependPrefix:o.prependPrefix,target:k,pool:m,svc:j,prefix:o.path,auth:o.auth,lens:R};if(o.host&&(u.push({...b,host:o.host}),H.push(o.host),S[i]={host:o.host},m&&o.internalHost!==!1)){const x=typeof o.internalHost=="string"?o.internalHost:`internal.${o.host}`;u.push({...b,host:x,internal:!0}),S[i]={host:o.host,internalHost:x}}o.default&&(y={...b,prefix:void 0})}y&&(y.apex=v.apex??(0,scope_1.baseDomainOf)(H),u.push(y));let $;if(t&&v.apex&&v.baseApex){const i={name:t.name,apex:v.apex,baseApex:v.baseApex},o=(0,instance_1.instanceEnv)(e,(0,instance_1.instanceBuiltinEnv)(i,S));$={...i,baseKey:t.baseKey,env:o,...t.frozen?{frozen:!0}:{}};for(const f of g)f.instanceEnv=o,t.frozen&&(f.frozen=!0);for(const f of _)f.env=o,t.frozen&&(f.frozen=!0)}return(0,manifest_1.sortBySpecificity)(u),{routes:u,services:g,...$?{instance:$}:{}}}h(discoverRoutes,"discoverRoutes"),p(discoverRoutes,"discoverRoutes");function projectSourceRoots(e,s,a={}){const r={};for(const l of Object.keys(s)){const n=new Set,t=p(c=>{if(!n.has(c)){n.add(c);for(const u of a[c]??[])t(u)}},"visit");t(l);const d=[...n].map(c=>s[c]?.root).filter(c=>typeof c=="string").map(c=>path.resolve(e,c)).filter(c=>{const u=path.relative(e,c);return u===""||u!==".."&&!u.startsWith(`..${path.sep}`)&&!path.isAbsolute(u)});r[l]=[...new Set(d)]}return r}h(projectSourceRoots,"projectSourceRoots"),p(projectSourceRoots,"projectSourceRoots");
1
+ "use strict";var A=Object.defineProperty;var h=(e,o)=>A(e,"name",{value:o,configurable:!0});var I=Object.defineProperty,l=h((e,o)=>I(e,"name",{value:o,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.RETIRING_SUFFIX=void 0,exports.stopRoutingTo=stopRoutingTo,exports.pickSock=pickSock,exports.lensPortFromUpstream=lensPortFromUpstream,exports.rewriteInstanceDecls=rewriteInstanceDecls,exports.discoverRoutes=discoverRoutes,exports.projectSourceRoots=projectSourceRoots;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),manifest_1=require("../manifest"),pod_sock_1=require("../../../pod-sock"),instance_1=require("./instance"),scope_1=require("./scope");exports.RETIRING_SUFFIX=".retiring";function sockIdentity(e){try{const o=fs.statSync(e);return`${o.dev}:${o.ino}:${o.ctimeMs}`}catch{return}}h(sockIdentity,"sockIdentity"),l(sockIdentity,"sockIdentity");function stopRoutingTo(e,o=()=>!0){let a;try{a=fs.readdirSync(e.dir)}catch{a=[]}const r=a.filter(t=>(0,pod_sock_1.isPodSockName)(t)&&o(t)).map(t=>path.join(e.dir,t));if(r.length===0)return r;const d=e.retired??=new Map;for(const t of r){const u=sockIdentity(t);u!==void 0&&d.set(t,u)}const n=new Set(r);return e.socks=e.socks.filter(t=>!n.has(t)),e.idx=-1,r}h(stopRoutingTo,"stopRoutingTo"),l(stopRoutingTo,"stopRoutingTo");function stillRetired(e,o){const a=e.retired?.get(o);return a===void 0?!1:sockIdentity(o)===a?!0:(e.retired?.delete(o),!1)}h(stillRetired,"stillRetired"),l(stillRetired,"stillRetired");function pickSock(e,o=1500){const a=Date.now();if(a-e.scannedAt>o){try{const r=fs.readdirSync(e.dir),d=r.filter(t=>(0,pod_sock_1.isPodSockName)(t)&&!stillRetired(e,path.join(e.dir,t)));if(e.retired)for(const t of[...e.retired.keys()])r.includes(path.basename(t))||e.retired.delete(t);const n=d.filter(t=>!r.includes(t+exports.RETIRING_SUFFIX));e.socks=(n.length>0?n:d).map(t=>path.join(e.dir,t))}catch{e.socks=[]}e.scannedAt=a}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}h(pickSock,"pickSock"),l(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const o=/:(\d+)/.exec(e);return o?Number(o[1]):void 0}h(lensPortFromUpstream,"lensPortFromUpstream"),l(lensPortFromUpstream,"lensPortFromUpstream");function rewriteInstanceDecls(e,o){const a=e.flatMap(([,n])=>[...n.host?[n.host]:[],...typeof n.internalHost=="string"?[n.internalHost]:[]]),r=o.apex??(0,instance_1.commonApex)(a);if(!r)return console.warn(`[gateway] instance '${o.name}': the declared hosts share no apex \u2014 set \`instance.apex\` in .lensmcp/config.json; no host is served for it.`),{decls:e.map(([n,t])=>[n,{...t,host:void 0,internalHost:!1,default:!1}])};const d=[];for(const[n,t]of e){const u={...t};if(t.host){const c=(0,instance_1.rewriteHost)(t.host,r,o.name);c||console.warn(`[gateway] instance '${o.name}': ${n}'s host ${t.host} is outside ${r} \u2014 not served for the instance.`),u.host=c}if(typeof t.internalHost=="string"){const c=(0,instance_1.rewriteHost)(t.internalHost,r,o.name);c||console.warn(`[gateway] instance '${o.name}': ${n}'s internal host ${t.internalHost} is outside ${r} \u2014 not served for the instance.`),u.internalHost=c??!1}d.push([n,u])}return{decls:d,baseApex:r,apex:`${o.name}.${r}`}}h(rewriteInstanceDecls,"rewriteInstanceDecls"),l(rewriteInstanceDecls,"rewriteInstanceDecls");function discoverRoutes(e,o,a=os.tmpdir(),r="",d={},n=r,t){const u=n||r,c=l(i=>u?path.join(a,u,`${i}-devserver`):path.join(a,`${i}-devserver`),"sockDirFor"),f=[],g=[],_=[],H=[],S={};let y,w=[];const F=new Map;for(const[i,s]of Object.entries(o))try{const m=JSON.parse(fs.readFileSync(path.join(e,s.root,"project.json"),"utf8")),p=m.cluster??m.davnx;p&&(w.push([i,p]),F.set(i,s.root))}catch{continue}let v={};t&&({decls:w,...v}=rewriteInstanceDecls(w,t));for(const[i,s]of w){const m={root:F.get(i)};let p,j,k,R;if(!s.service&&s.sources!==void 0&&console.warn(`[gateway] ${i}: cluster.sources is read from pod (service) decls only \u2014 ignored.`),s.service)s.lens&&console.warn(`[gateway] ${i}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),p={dir:c(s.service),socks:[],idx:-1,scannedAt:0},j={project:i,decl:s,root:e,wsKey:r,...u!==r?{sockNs:u}:{},pool:p,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},g.push(j);else if(s.lens){const x=s.port??lensPortFromUpstream(s.upstream)??5173;k=`http://localhost:${x}`,R={project:i,...r?{workspaceKey:r}:{},workspaceRoot:e,projectRoot:path.join(e,m.root),port:x,...d[i]?.length?{sourceRoots:d[i]}:{}},_.push(R)}else if(k=s.upstream??(s.port?`http://localhost:${s.port}`:void 0),!k){console.warn(`[gateway] ${i}: no service/upstream/port \u2014 skipped.`);continue}const b={...r?{wsKey:r}:{},project:i,prependPrefix:s.prependPrefix,target:k,pool:p,svc:j,prefix:s.path,auth:s.auth,lens:R};if(s.host&&(f.push({...b,host:s.host}),H.push(s.host),S[i]={host:s.host},p&&s.internalHost!==!1)){const x=typeof s.internalHost=="string"?s.internalHost:`internal.${s.host}`;f.push({...b,host:x,internal:!0}),S[i]={host:s.host,internalHost:x}}s.default&&(y={...b,prefix:void 0})}y&&(y.apex=v.apex??(0,scope_1.baseDomainOf)(H),f.push(y));let $;if(t&&v.apex&&v.baseApex){const i={name:t.name,apex:v.apex,baseApex:v.baseApex},s=(0,instance_1.instanceBuiltinEnv)(i,S),m=(0,instance_1.instanceEnv)(e,s);$={...i,baseKey:t.baseKey,env:m,...t.frozen?{frozen:!0}:{}};for(const p of g)p.instanceEnv=m,p.instanceBuiltins=s,t.frozen&&(p.frozen=!0);for(const p of _)p.env=m,t.frozen&&(p.frozen=!0)}return(0,manifest_1.sortBySpecificity)(f),{routes:f,services:g,...$?{instance:$}:{}}}h(discoverRoutes,"discoverRoutes"),l(discoverRoutes,"discoverRoutes");function projectSourceRoots(e,o,a={}){const r={};for(const d of Object.keys(o)){const n=new Set,t=l(c=>{if(!n.has(c)){n.add(c);for(const f of a[c]??[])t(f)}},"visit");t(d);const u=[...n].map(c=>o[c]?.root).filter(c=>typeof c=="string").map(c=>path.resolve(e,c)).filter(c=>{const f=path.relative(e,c);return f===""||f!==".."&&!f.startsWith(`..${path.sep}`)&&!path.isAbsolute(f)});r[d]=[...new Set(u)]}return r}h(projectSourceRoots,"projectSourceRoots"),l(projectSourceRoots,"projectSourceRoots");
@@ -97,8 +97,24 @@ export declare function parseInstanceEnv(text: string, scope: Readonly<Record<st
97
97
  env: Record<string, string>;
98
98
  skipped: number[];
99
99
  };
100
+ /** The instance's variables as registration reads them ({@link layerInstanceEnv}, lenient): never throws. */
101
+ export declare function instanceEnv(root: string, builtins: Readonly<Record<string, string>>): Record<string, string>;
100
102
  /**
101
- * The built-ins, then `instance.resources.env` (the lens's, from the declared resources), then `instance.env` (the
102
- * user's) over them — each file expanding over everything below it. A missing file is the common case.
103
+ * The same variables {@link instanceEnv} gives, read again for a restart of one service — or why they cannot be: a file
104
+ * that is there and unreadable, or a line that is not `KEY=value`. A missing file is still the common case.
103
105
  */
104
- export declare function instanceEnv(root: string, builtins: Readonly<Record<string, string>>): Record<string, string>;
106
+ export declare function readInstanceEnvStrict(root: string, builtins: Readonly<Record<string, string>>): {
107
+ ok: true;
108
+ env: Record<string, string>;
109
+ } | {
110
+ ok: false;
111
+ reason: string;
112
+ };
113
+ /**
114
+ * Twelve hex digits of a SHA-256 over every `KEY=value` line, sorted by key: the same variables give the same print,
115
+ * whatever order a file names them in. It answers «did the change take» without saying any value: the answer of a
116
+ * restart and its event carry it, and never the variables themselves.
117
+ */
118
+ export declare function instanceEnvFingerprint(env: Readonly<Record<string, string>>): string;
119
+ /** The NAMES of the variables that were added, removed or given another value between two snapshots — sorted, no value. */
120
+ export declare function changedInstanceEnvNames(before: Readonly<Record<string, string>>, after: Readonly<Record<string, string>>): string[];
@@ -1,2 +1,3 @@
1
- "use strict";var x=Object.defineProperty;var a=(t,e)=>x(t,"name",{value:e,configurable:!0});var h=Object.defineProperty,c=a((t,e)=>h(t,"name",{value:e,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.INSTANCE_NAME_PATTERN=void 0,exports.parseInstanceDecl=parseInstanceDecl,exports.instanceKey=instanceKey,exports.instanceSockNs=instanceSockNs,exports.commonApex=commonApex,exports.rewriteHost=rewriteHost,exports.envSuffix=envSuffix,exports.instanceBuiltinEnv=instanceBuiltinEnv,exports.instanceEnvFile=instanceEnvFile,exports.instanceResourcesEnvFile=instanceResourcesEnvFile,exports.parseInstanceEnv=parseInstanceEnv,exports.instanceEnv=instanceEnv;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));exports.INSTANCE_NAME_PATTERN=/^[a-z0-9](?:[a-z0-9-]{0,18}[a-z0-9])?$/;const HOST_PATTERN=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/;function parseInstanceDecl(t){if(!t||typeof t!="object")return;const e=t;if(typeof e.name!="string"||!exports.INSTANCE_NAME_PATTERN.test(e.name)||typeof e.baseKey!="string"||e.baseKey.length===0)return;const n=typeof e.apex=="string"&&HOST_PATTERN.test(e.apex.toLowerCase())?e.apex.toLowerCase():void 0;return{name:e.name,baseKey:e.baseKey,...n?{apex:n}:{},...typeof e.mainRoot=="string"&&e.mainRoot.length>0?{mainRoot:e.mainRoot}:{},...e.frozen===!0?{frozen:!0}:{}}}a(parseInstanceDecl,"parseInstanceDecl"),c(parseInstanceDecl,"parseInstanceDecl");function instanceKey(t,e){return`${t}-${e}`}a(instanceKey,"instanceKey"),c(instanceKey,"instanceKey");function instanceSockNs(t){let e=2166136261;for(let n=0;n<t.length;n++)e^=t.charCodeAt(n),e=Math.imul(e,16777619);return`i-${(e>>>0&16777215).toString(16).padStart(6,"0")}`}a(instanceSockNs,"instanceSockNs"),c(instanceSockNs,"instanceSockNs");function commonApex(t){const e=t.map(s=>s.toLowerCase().replace(/^\*\./,"")).filter(s=>s.length>0).map(s=>s.split(".").reverse());if(e.length===0)return;const n=[];for(let s=0;;s++){const i=e[0]?.[s];if(i===void 0||!e.every(r=>r[s]===i))break;n.push(i)}return n.length>=2?n.reverse().join("."):void 0}a(commonApex,"commonApex"),c(commonApex,"commonApex");function rewriteHost(t,e,n){const s=t.toLowerCase();if(s.startsWith("*.")){const i=rewriteHost(s.slice(2),e,n);return i===void 0?void 0:`*.${i}`}if(s===e)return`${n}.${e}`;if(s.endsWith(`.${e}`))return`${s.slice(0,-e.length)}${n}.${e}`}a(rewriteHost,"rewriteHost"),c(rewriteHost,"rewriteHost");function envSuffix(t){return t.toUpperCase().replace(/[^A-Z0-9]+/g,"_").replace(/^_+|_+$/g,"")}a(envSuffix,"envSuffix"),c(envSuffix,"envSuffix");function instanceBuiltinEnv(t,e){const n={LENSMCP_INSTANCE:t.name,LENSMCP_APEX:t.apex,LENSMCP_BASE_APEX:t.baseApex},s={};for(const[i,r]of Object.entries(e).sort(([o],[l])=>o.localeCompare(l))){const o=envSuffix(i);o&&(r.host&&(n[`LENSMCP_HOST_${o}`]=r.host),r.internalHost&&(n[`LENSMCP_INTERNAL_HOST_${o}`]=r.internalHost),(r.host||r.internalHost)&&(s[i]={...r.host?{host:r.host}:{},...r.internalHost?{internalHost:r.internalHost}:{}}))}return n.LENSMCP_HOSTS_JSON=JSON.stringify(s),n}a(instanceBuiltinEnv,"instanceBuiltinEnv"),c(instanceBuiltinEnv,"instanceBuiltinEnv");function instanceEnvFile(t){return path.join(t,".lensmcp","instance.env")}a(instanceEnvFile,"instanceEnvFile"),c(instanceEnvFile,"instanceEnvFile");function instanceResourcesEnvFile(t){return path.join(t,".lensmcp","instance.resources.env")}a(instanceResourcesEnvFile,"instanceResourcesEnvFile"),c(instanceResourcesEnvFile,"instanceResourcesEnvFile");function parseInstanceEnv(t,e){const n={},s=[],i=c(o=>n[o]??e[o]??"","lookup"),r=c(o=>o.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,(l,u)=>i(u)),"expand");return t.split(/\r?\n/).forEach((o,l)=>{const u=o.trim();if(u===""||u.startsWith("#"))return;const v=/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(u);if(!v){s.push(l+1);return}const[,_,p]=v;let E;if(p.startsWith("'")){const f=p.indexOf("'",1);if(f<0){s.push(l+1);return}E=p.slice(1,f)}else if(p.startsWith('"')){const f=p.indexOf('"',1);if(f<0){s.push(l+1);return}E=r(p.slice(1,f).replace(/\\n/g,`
2
- `))}else E=r(p.replace(/\s+#.*$/,"").trim());n[_]=E}),{env:n,skipped:s}}a(parseInstanceEnv,"parseInstanceEnv"),c(parseInstanceEnv,"parseInstanceEnv");function instanceEnv(t,e){let n={...e};for(const s of[instanceResourcesEnvFile(t),instanceEnvFile(t)]){let i;try{i=fs.readFileSync(s,"utf8")}catch{continue}const{env:r,skipped:o}=parseInstanceEnv(i,n);o.length>0&&console.warn(`[gateway] ${s}: skipped malformed line(s) ${o.join(", ")}`),n={...n,...r}}return{...n,...e}}a(instanceEnv,"instanceEnv"),c(instanceEnv,"instanceEnv");
1
+ "use strict";var m=Object.defineProperty;var i=(n,e)=>m(n,"name",{value:e,configurable:!0});var h=Object.defineProperty,o=i((n,e)=>h(n,"name",{value:e,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.INSTANCE_NAME_PATTERN=void 0,exports.parseInstanceDecl=parseInstanceDecl,exports.instanceKey=instanceKey,exports.instanceSockNs=instanceSockNs,exports.commonApex=commonApex,exports.rewriteHost=rewriteHost,exports.envSuffix=envSuffix,exports.instanceBuiltinEnv=instanceBuiltinEnv,exports.instanceEnvFile=instanceEnvFile,exports.instanceResourcesEnvFile=instanceResourcesEnvFile,exports.parseInstanceEnv=parseInstanceEnv,exports.instanceEnv=instanceEnv,exports.readInstanceEnvStrict=readInstanceEnvStrict,exports.instanceEnvFingerprint=instanceEnvFingerprint,exports.changedInstanceEnvNames=changedInstanceEnvNames;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"));exports.INSTANCE_NAME_PATTERN=/^[a-z0-9](?:[a-z0-9-]{0,18}[a-z0-9])?$/;const HOST_PATTERN=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/;function parseInstanceDecl(n){if(!n||typeof n!="object")return;const e=n;if(typeof e.name!="string"||!exports.INSTANCE_NAME_PATTERN.test(e.name)||typeof e.baseKey!="string"||e.baseKey.length===0)return;const t=typeof e.apex=="string"&&HOST_PATTERN.test(e.apex.toLowerCase())?e.apex.toLowerCase():void 0;return{name:e.name,baseKey:e.baseKey,...t?{apex:t}:{},...typeof e.mainRoot=="string"&&e.mainRoot.length>0?{mainRoot:e.mainRoot}:{},...e.frozen===!0?{frozen:!0}:{}}}i(parseInstanceDecl,"parseInstanceDecl"),o(parseInstanceDecl,"parseInstanceDecl");function instanceKey(n,e){return`${n}-${e}`}i(instanceKey,"instanceKey"),o(instanceKey,"instanceKey");function instanceSockNs(n){let e=2166136261;for(let t=0;t<n.length;t++)e^=n.charCodeAt(t),e=Math.imul(e,16777619);return`i-${(e>>>0&16777215).toString(16).padStart(6,"0")}`}i(instanceSockNs,"instanceSockNs"),o(instanceSockNs,"instanceSockNs");function commonApex(n){const e=n.map(r=>r.toLowerCase().replace(/^\*\./,"")).filter(r=>r.length>0).map(r=>r.split(".").reverse());if(e.length===0)return;const t=[];for(let r=0;;r++){const a=e[0]?.[r];if(a===void 0||!e.every(s=>s[r]===a))break;t.push(a)}return t.length>=2?t.reverse().join("."):void 0}i(commonApex,"commonApex"),o(commonApex,"commonApex");function rewriteHost(n,e,t){const r=n.toLowerCase();if(r.startsWith("*.")){const a=rewriteHost(r.slice(2),e,t);return a===void 0?void 0:`*.${a}`}if(r===e)return`${t}.${e}`;if(r.endsWith(`.${e}`))return`${r.slice(0,-e.length)}${t}.${e}`}i(rewriteHost,"rewriteHost"),o(rewriteHost,"rewriteHost");function envSuffix(n){return n.toUpperCase().replace(/[^A-Z0-9]+/g,"_").replace(/^_+|_+$/g,"")}i(envSuffix,"envSuffix"),o(envSuffix,"envSuffix");function instanceBuiltinEnv(n,e){const t={LENSMCP_INSTANCE:n.name,LENSMCP_APEX:n.apex,LENSMCP_BASE_APEX:n.baseApex},r={};for(const[a,s]of Object.entries(e).sort(([c],[p])=>c.localeCompare(p))){const c=envSuffix(a);c&&(s.host&&(t[`LENSMCP_HOST_${c}`]=s.host),s.internalHost&&(t[`LENSMCP_INTERNAL_HOST_${c}`]=s.internalHost),(s.host||s.internalHost)&&(r[a]={...s.host?{host:s.host}:{},...s.internalHost?{internalHost:s.internalHost}:{}}))}return t.LENSMCP_HOSTS_JSON=JSON.stringify(r),t}i(instanceBuiltinEnv,"instanceBuiltinEnv"),o(instanceBuiltinEnv,"instanceBuiltinEnv");function instanceEnvFile(n){return path.join(n,".lensmcp","instance.env")}i(instanceEnvFile,"instanceEnvFile"),o(instanceEnvFile,"instanceEnvFile");function instanceResourcesEnvFile(n){return path.join(n,".lensmcp","instance.resources.env")}i(instanceResourcesEnvFile,"instanceResourcesEnvFile"),o(instanceResourcesEnvFile,"instanceResourcesEnvFile");function parseInstanceEnv(n,e){const t={},r=[],a=o(c=>t[c]??e[c]??"","lookup"),s=o(c=>c.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,(p,l)=>a(l)),"expand");return n.split(/\r?\n/).forEach((c,p)=>{const l=c.trim();if(l===""||l.startsWith("#"))return;const f=/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(l);if(!f){r.push(p+1);return}const[,d,u]=f;let E;if(u.startsWith("'")){const v=u.indexOf("'",1);if(v<0){r.push(p+1);return}E=u.slice(1,v)}else if(u.startsWith('"')){const v=u.indexOf('"',1);if(v<0){r.push(p+1);return}E=s(u.slice(1,v).replace(/\\n/g,`
2
+ `))}else E=s(u.replace(/\s+#.*$/,"").trim());t[d]=E}),{env:t,skipped:r}}i(parseInstanceEnv,"parseInstanceEnv"),o(parseInstanceEnv,"parseInstanceEnv");function layerInstanceEnv(n,e,t){let r={...e};for(const a of[instanceResourcesEnvFile(n),instanceEnvFile(n)]){let s;try{s=fs.readFileSync(a,"utf8")}catch(l){const f=l.code;if(!t||f==="ENOENT")continue;return{problem:`${a} could not be read (${f??l.message})`}}const{env:c,skipped:p}=parseInstanceEnv(s,r);if(p.length>0){if(t)return{problem:`${a}: line(s) ${p.join(", ")} are not KEY=value`};console.warn(`[gateway] ${a}: skipped malformed line(s) ${p.join(", ")}`)}r={...r,...c}}return{env:{...r,...e}}}i(layerInstanceEnv,"layerInstanceEnv"),o(layerInstanceEnv,"layerInstanceEnv");function instanceEnv(n,e){const t=layerInstanceEnv(n,e,!1);return"env"in t?t.env:{...e}}i(instanceEnv,"instanceEnv"),o(instanceEnv,"instanceEnv");function readInstanceEnvStrict(n,e){const t=layerInstanceEnv(n,e,!0);return"env"in t?{ok:!0,env:t.env}:{ok:!1,reason:t.problem}}i(readInstanceEnvStrict,"readInstanceEnvStrict"),o(readInstanceEnvStrict,"readInstanceEnvStrict");function instanceEnvFingerprint(n){const e=Object.keys(n).sort().map(t=>`${t}=${n[t]}`);return(0,node_crypto_1.createHash)("sha256").update(e.join(`
3
+ `)).digest("hex").slice(0,12)}i(instanceEnvFingerprint,"instanceEnvFingerprint"),o(instanceEnvFingerprint,"instanceEnvFingerprint");function changedInstanceEnvNames(n,e){return[...new Set([...Object.keys(n),...Object.keys(e)])].filter(t=>n[t]!==e[t]).sort()}i(changedInstanceEnvNames,"changedInstanceEnvNames"),o(changedInstanceEnvNames,"changedInstanceEnvNames");
@@ -8,7 +8,7 @@ import * as net from 'node:net';
8
8
  import { probePodAlive } from './pod-probe';
9
9
  import { type SharedBuildRelay } from './shared-build';
10
10
  import type { GatewayRuntime, LensWatcherHealth, PodWatcherTestimony, PodWatcherVerdict, ServiceCtl, ServiceRestart, SockPool, SourceSetChange } from './types';
11
- import type { Observability } from './observability';
11
+ import type { Observability, Severity } from './observability';
12
12
  import { type PsRow } from './orphan-reaper';
13
13
  /** Bounded, async, non-blocking walk of the workspace source tree → a signature of the file SET
14
14
  * (`<count>:<xor-of-path-hashes>`). Skips node_modules/dist/build/dotdirs + `*-devserver` sock dirs + every
@@ -277,6 +277,9 @@ export interface ServiceLayer {
277
277
  * Idempotent (a no-op once `state === 'up'`), so the hot path can call it per
278
278
  * request. Returns true only on the transition. See the implementation. */
279
279
  markUp(svc: ServiceCtl, how: 'adopted' | 'observed'): boolean;
280
+ /** Onto the service's OWN workspace's bus — the lifecycle's events, and those the control plane raises for a service
281
+ * (control.ts `POST /awake`). `fingerprint` is the flood bucket: identity goes in `raw`. */
282
+ emitSvc(svc: ServiceCtl, severity: Severity, title: string, fingerprint: string, raw: Record<string, unknown>): void;
280
283
  /** Start the idle sweeper; returns the interval (already `.unref()`'d). */
281
284
  startSweeper(): NodeJS.Timeout;
282
285
  /** Rolling recycle: standby generation first, retire the old once the new answers (issues/closed/dev-gateway-09). */