@lensmcp/cluster 1.23.12 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/build-host.d.ts +1 -0
  2. package/build-host.js +1 -0
  3. package/create-webpack-dev.d.ts +75 -39
  4. package/create-webpack-dev.js +1 -1
  5. package/dev-devtool.d.ts +45 -0
  6. package/dev-devtool.js +1 -0
  7. package/devserver-notifier.d.ts +57 -0
  8. package/devserver-notifier.js +1 -0
  9. package/executors/gateway/body-stall.d.ts +13 -1
  10. package/executors/gateway/body-stall.js +1 -1
  11. package/executors/gateway/canonical-path.d.ts +1 -1
  12. package/executors/gateway/client-ip.d.ts +1 -1
  13. package/executors/gateway/gateway.impl.js +1 -1
  14. package/executors/gateway/jwt-claims.d.ts +1 -1
  15. package/executors/gateway/prod-config.d.ts +1 -1
  16. package/executors/gateway/prod-runtime/body-stall.d.ts +1 -1
  17. package/executors/gateway/rate-limit.d.ts +1 -1
  18. package/executors/gateway/runtime/auth.d.ts +1 -1
  19. package/executors/gateway/runtime/chooser.d.ts +2 -2
  20. package/executors/gateway/runtime/control.d.ts +3 -3
  21. package/executors/gateway/runtime/control.js +2 -2
  22. package/executors/gateway/runtime/dev-auth.d.ts +1 -1
  23. package/executors/gateway/runtime/edge.d.ts +3 -3
  24. package/executors/gateway/runtime/guest-graph.d.ts +113 -0
  25. package/executors/gateway/runtime/guest-graph.js +2 -0
  26. package/executors/gateway/runtime/lens-children.d.ts +11 -1
  27. package/executors/gateway/runtime/lens-children.js +1 -1
  28. package/executors/gateway/runtime/lifecycle.d.ts +74 -3
  29. package/executors/gateway/runtime/lifecycle.js +3 -3
  30. package/executors/gateway/runtime/process-names.d.ts +18 -0
  31. package/executors/gateway/runtime/process-names.js +1 -0
  32. package/executors/gateway/runtime/project-graph-store.d.ts +74 -0
  33. package/executors/gateway/runtime/project-graph-store.js +1 -0
  34. package/executors/gateway/runtime/proxy.d.ts +2 -2
  35. package/executors/gateway/runtime/proxy.js +1 -1
  36. package/executors/gateway/runtime/registration-guards.d.ts +32 -0
  37. package/executors/gateway/runtime/registration-guards.js +1 -0
  38. package/executors/gateway/runtime/scope.d.ts +1 -1
  39. package/executors/gateway/runtime/serve-launch.d.ts +118 -0
  40. package/executors/gateway/runtime/serve-launch.js +3 -0
  41. package/executors/gateway/runtime/server.d.ts +3 -3
  42. package/executors/gateway/runtime/server.js +2 -2
  43. package/executors/gateway/runtime/shared-build.d.ts +65 -0
  44. package/executors/gateway/runtime/shared-build.js +4 -0
  45. package/executors/gateway/runtime/types.d.ts +35 -9
  46. package/executors/gateway/runtime/types.js +1 -1
  47. package/executors/gateway/runtime/workspace-log.js +1 -1
  48. package/executors/gateway/schema.d.ts +3 -0
  49. package/executors/gateway/schema.json +4 -0
  50. package/executors/serve/schema.d.ts +2 -0
  51. package/executors/serve/schema.json +4 -0
  52. package/executors/serve/serve.impl.d.ts +12 -6
  53. package/executors/serve/serve.impl.js +1 -1
  54. package/executors/trust/hostname.d.ts +1 -1
  55. package/exit-reason.d.ts +1 -1
  56. package/fork-ts-check-reporter.d.ts +12 -0
  57. package/fork-ts-check-reporter.js +1 -1
  58. package/graph-snapshot.d.ts +1 -0
  59. package/graph-snapshot.js +1 -0
  60. package/heap-snapshot.d.ts +66 -0
  61. package/heap-snapshot.js +1 -0
  62. package/hot-swap-registries.d.ts +1 -1
  63. package/inspector-secret.d.ts +39 -0
  64. package/inspector-secret.js +1 -0
  65. package/main.devserver.d.ts +50 -4
  66. package/main.devserver.js +11 -9
  67. package/package.json +4 -4
  68. package/perf/heavy-barrel-import.d.ts +135 -0
  69. package/perf/heavy-barrel-import.js +1 -0
  70. package/perf/perf-config.d.ts +29 -0
  71. package/perf/perf-config.js +1 -0
  72. package/perf/perf-rule-events.d.ts +71 -0
  73. package/perf/perf-rule-events.js +2 -0
  74. package/perf/perf-rules-analysis.d.ts +28 -0
  75. package/perf/perf-rules-analysis.js +1 -0
  76. package/perf/perf-rules-plugin.d.ts +71 -0
  77. package/perf/perf-rules-plugin.js +1 -0
  78. package/perf/perf-rules-worker.d.ts +1 -0
  79. package/perf/perf-rules-worker.js +1 -0
  80. package/perf/source-scan.d.ts +59 -0
  81. package/perf/source-scan.js +5 -0
  82. package/perf/workspace-libraries.d.ts +52 -0
  83. package/perf/workspace-libraries.js +2 -0
  84. package/pod-memory-events.d.ts +71 -0
  85. package/pod-memory-events.js +2 -0
  86. package/pod-memory.d.ts +258 -0
  87. package/pod-memory.js +1 -0
  88. package/process-name.d.ts +45 -0
  89. package/process-name.js +1 -1
  90. package/serve-runner.d.ts +2 -0
  91. package/serve-runner.js +1 -0
  92. package/shared-build/attach.d.ts +56 -0
  93. package/shared-build/attach.js +3 -0
  94. package/shared-build/host.d.ts +52 -0
  95. package/shared-build/host.js +4 -0
  96. package/shared-build/protocol.d.ts +118 -0
  97. package/shared-build/protocol.js +1 -0
  98. package/shared-build/shared-compilation.d.ts +91 -0
  99. package/shared-build/shared-compilation.js +3 -0
  100. package/shared-build/signature.d.ts +52 -0
  101. package/shared-build/signature.js +1 -0
  102. package/stdio-guard.d.ts +1 -1
  103. package/swap-recycle.d.ts +21 -0
  104. package/swap-recycle.js +1 -1
  105. package/tsgo-check-plugin.d.ts +1 -1
  106. package/workspace-paths.d.ts +76 -0
  107. package/workspace-paths.js +1 -0
@@ -0,0 +1 @@
1
+ export {};
package/build-host.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var u=Object.defineProperty;var p=(s,t)=>u(s,"name",{value:t,configurable:!0});var d=Object.defineProperty,o=p((s,t)=>d(s,"name",{value:t,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0});const host_1=require("./shared-build/host"),protocol_1=require("./shared-build/protocol"),STOP_SIGNALS=["SIGTERM","SIGINT","SIGHUP"],GATEWAY_POLL_MS=5e3;function main(){const s=process.argv.find(e=>e.startsWith(protocol_1.BUILD_HOST_ROOT_FLAG))?.slice(protocol_1.BUILD_HOST_ROOT_FLAG.length);(!s||typeof process.send!="function")&&(console.error("[build-host] started without a workspace root or a gateway channel \u2014 only the dev gateway starts this process"),process.exit(1)),process.env.NODE_ENV="development";const t=o(e=>{if(process.connected)try{process.send(e,void 0,void 0,()=>{})}catch{}},"send"),n=Number(process.env.LENSMCP_BUILD_HOST_PAUSE_MS),c=new host_1.BuildHost({root:s,send:t,log:o(e=>console.log(e),"log"),...Number.isFinite(n)&&n>=0&&process.env.LENSMCP_BUILD_HOST_PAUSE_MS?{pauseMs:n}:{}});let i=!1;const a=o((e,l=0)=>{i||(i=!0,console.log(`stopping (${e})`),c.stop().finally(()=>process.exit(l)),setTimeout(()=>process.exit(l),8e3).unref())},"stop");for(const e of STOP_SIGNALS)process.on(e,()=>a(e));process.on("disconnect",()=>a("the gateway is gone"));const r=Number(process.env.LENSMCP_GATEWAY_PID??0);Number.isInteger(r)&&r>0&&setInterval(()=>{try{process.kill(r,0)}catch(e){e.code!=="EPERM"&&a(`gateway pid ${r} is gone`)}},GATEWAY_POLL_MS).unref(),process.on("message",e=>{i||!e||typeof e!="object"||(e.type==="join"?c.join(e.member,e.join):e.type==="leave"&&c.leave(e.member))}),t({type:"ready",pid:process.pid})}p(main,"main"),o(main,"main"),require.main===module&&main();
@@ -1,4 +1,7 @@
1
1
  import { type NodeExternalsConfig } from './build-scope-patterns';
2
+ import { type DevDevtool } from './dev-devtool';
3
+ import { type PodWatcherTestimonyWire } from './devserver-notifier';
4
+ export { DevserverNotifier, devserverControlSock, type PodWatcherTestimonyWire } from './devserver-notifier';
2
5
  export type { NodeExternalsConfig };
3
6
  export interface DevWebpackOptions {
4
7
  appName: string;
@@ -22,50 +25,83 @@ export interface DevWebpackOptions {
22
25
  webpackConfigPath?: string;
23
26
  /** Parent terminates TLS (gateway.https) — reload notifications go over https. */
24
27
  httpsReload?: boolean;
28
+ /** webpack `devtool`. Default `LENSMCP_DEV_DEVTOOL`, else `eval` (dev-devtool.ts). */
29
+ devtool?: DevDevtool;
25
30
  }
26
- /**
27
- * Notifies the devserver parent on every successful rebuild (`POST /webpack/reload` → rolling hot-swap of the
28
- * pod children) AND stamps the pod's WATCHER TESTIMONY (`POST /webpack/watching`): every `watchRun`, `done`,
29
- * `failed` and `watchClose`, plus any watchpack error. The parent relays the latest testimony to its children,
30
- * which serve it in `/__lensmcp/alive`; the gateway's pod sweeper reads it before a `stale-source-set` recycle
31
- * (`lifecycle.ts` `podWatcherVerdict`). Without it the sweeper had NO evidence and scaled every pod to zero on
32
- * every source-set change in its scope (issues/dev-gateway/07). Both posts are best-effort: a parent that
33
- * predates `/webpack/watching` answers 404 and the plugin stays quiet.
34
- */
31
+ /** {@link DevserverNotifier} on a service's own compiler: every `watchRun`, `done`, `failed` and `watchClose`. */
35
32
  export declare class DevServerReloadPlugin {
36
- private base;
37
- private secure;
38
- /** The devserver parent's CONTROL socket — preferred over the TCP port (see `post`). */
39
- private controlSock;
40
- /** Bounded, newest-last paths of the last few `watchRun`s — diagnostics for the register, not policy. */
41
- private static readonly RECENT_PATH_CAP;
42
- private testimony;
43
- constructor(port: number, secure?: boolean, controlSock?: string);
44
- /**
45
- * Deliver to the parent's CONTROL SOCKET first (`$TMPDIR[/<wsKey>]/<service>-devserver/parent.sock`), then
46
- * the TCP port. The socket path is bind-STOLEN by whichever parent generation started last, so during a
47
- * rolling recycle (lensmcp issues/dev-gateway/09) this webpack's rebuilds reach ITS OWN parent even while the
48
- * retiring generation still holds the TCP port; the port is the fallback for a parent without the socket.
49
- */
50
- private post;
33
+ private readonly notifier;
34
+ constructor(port: number, secure?: boolean, controlSock?: string, managed?: boolean);
51
35
  /** The current testimony snapshot (exported for the spec — the wire shape the parent receives). */
52
36
  snapshot(): PodWatcherTestimonyWire;
53
- private stampWatchRun;
54
- private degrade;
55
- private tellWatching;
56
37
  apply(compiler: any): void;
57
38
  }
58
- /** The devserver parent's control socket for `serviceName` — the SAME derivation as `main.devserver.ts`'s
59
- * `SOCK_DIR` (`$TMPDIR[/<LENSMCP_WS_KEY>]/<service>-devserver/parent.sock`). */
60
- export declare function devserverControlSock(serviceName: string, env?: NodeJS.ProcessEnv): string;
61
- /** Wire shape of the testimony (mirrors `PodWatcherTestimony` in the gateway runtime; kept structural so the
62
- * webpack side does not import the gateway's types). */
63
- export interface PodWatcherTestimonyWire {
64
- schemaVersion: 1;
65
- lastWatchRunAt: number;
66
- lastBuildAt: number;
67
- buildsSinceSpawn: number;
68
- degraded: false | string;
69
- recentPaths: string[];
39
+ /** The dev build's persistent webpack cache (`cache` with `type: 'filesystem'`). */
40
+ export interface DevFilesystemCache {
41
+ type: 'filesystem';
42
+ cacheDirectory: string;
43
+ /** The project's directory under `cacheDirectory`. */
44
+ name: string;
45
+ /** A cache written under any other version is discarded, not reused. */
46
+ version: string;
47
+ /** Files whose change discards the whole cache: the code that builds this config, the user's overrides,
48
+ * the lockfile, the tsconfig and every file it extends. */
49
+ buildDependencies: Record<string, string[]>;
50
+ /** Builds an entry may go unused before the in-memory layer drops it (the disk copy stays). */
51
+ maxMemoryGenerations: number;
52
+ /** How long an entry may go unused before it is dropped from disk. */
53
+ maxAge: number;
54
+ }
55
+ /**
56
+ * The persistent cache for one project's dev build (issues/closed/pod-memory-04): under the workspace's
57
+ * `node_modules/.cache/lensmcp-webpack/<project>`, so every restart, standby generation and rolling
58
+ * recycle of that service starts warm instead of compiling everything again.
59
+ *
60
+ * Its `version` covers what changes how a module is built or resolved without being a file webpack can
61
+ * snapshot — the devtool (a module built for one devtool carries the wrong source for another), the
62
+ * resolution and externals inputs, `@lensmcp/cluster`'s version — and `buildDependencies` the files that
63
+ * do. Entry points and the output path are not in it: webpack handles a change to those on its own, and
64
+ * a standby generation that differed in one would have to start cold.
65
+ */
66
+ export declare function devCacheOptions(options: DevWebpackOptions, devtool: DevDevtool): DevFilesystemCache;
67
+ /** The part of webpack's module-filename info `devModuleFilename` reads. */
68
+ export interface ModuleFilenameInfo {
69
+ absoluteResourcePath: string;
70
+ }
71
+ /** `output.devtoolModuleFilenameTemplate` for dev builds: a module's absolute path as a `file://` URL.
72
+ * Under `eval` it is the `sourceURL` every stack frame names, and webpack strips one leading slash from
73
+ * that — a bare absolute path (the previous `[absolute-resource-path]`) came out as `Users/me/…`. Under
74
+ * a map-carrying devtool it is each map's `sources` entry, which Node prints back as the path. */
75
+ export declare function devModuleFilename(info: ModuleFilenameInfo): string;
76
+ export interface DevBuildFinalizeOptions {
77
+ cache: DevFilesystemCache;
78
+ /** The devtool carries source maps: make ts-loader emit one per module, so they map to TypeScript lines. */
79
+ tsSourceMaps: boolean;
80
+ }
81
+ /**
82
+ * Corrects what `NxAppWebpackPlugin` writes into `compiler.options` when it applies — so it must come
83
+ * AFTER that plugin in `plugins`:
84
+ *
85
+ * - `cache`: the plugin overwrites it — with `{ type: 'memory' }` only when the target is exactly `'node'`,
86
+ * `undefined` otherwise (ours is `node22`), which webpack turns into an unbounded memory cache in
87
+ * development mode, and every restart, standby generation and rolling recycle compiled the whole
88
+ * service cold (issues/closed/pod-memory-04). This puts the persistent cache back.
89
+ * - `context`: the plugin moves `options.context` to the project root after the Compiler has already
90
+ * captured the cwd as `compiler.context`. Module sources are named against the one and source maps
91
+ * resolved against the other, so every map's `sources` named a file that does not exist
92
+ * (`<workspace>/src/main.ts` for `<workspace>/apps/api/src/main.ts`).
93
+ * - ts-loader emits a map only when the tsconfig asks for one; without it a "module" source map maps the
94
+ * transpiled JS onto itself. When the devtool carries maps, they are asked for here.
95
+ * - `resolve.plugins`: when the config resolves the tsconfig's `paths` itself ({@link WorkspacePathsResolverPlugin}),
96
+ * the plugin's own tsconfig-paths resolver is taken out (issues/closed/dev-gateway-22). It runs before
97
+ * `resolve.alias` and resolves differently from `tsc` — against the project's folder when a tsconfig has a
98
+ * comment, and with any matching wildcard ahead of an exact key — so left in, it would answer first. The plugin
99
+ * pushes it only after every configured plugin has applied, so it is taken out on `afterPlugins`; one the
100
+ * user's override added itself is kept.
101
+ */
102
+ export declare class DevBuildFinalizePlugin {
103
+ private readonly options;
104
+ constructor(options: DevBuildFinalizeOptions);
105
+ apply(compiler: any): void;
70
106
  }
71
107
  export declare function createDevWebpackConfig(options: DevWebpackOptions): Record<string, any>;
@@ -1 +1 @@
1
- "use strict";var W=Object.defineProperty;var d=(r,t)=>W(r,"name",{value:t,configurable:!0});var T=Object.defineProperty,u=d((r,t)=>T(r,"name",{value:t,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DevServerReloadPlugin=void 0,exports.devserverControlSock=devserverControlSock,exports.createDevWebpackConfig=createDevWebpackConfig;const tslib_1=require("tslib"),app_plugin_1=require("@nx/webpack/app-plugin"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),webpack_node_externals_1=tslib_1.__importDefault(require("webpack-node-externals")),build_scope_patterns_1=require("./build-scope-patterns"),tsgo_check_plugin_1=require("./tsgo-check-plugin"),fork_ts_check_reporter_1=require("./fork-ts-check-reporter"),webpack=require("webpack");class DevServerReloadPlugin{static{d(this,"DevServerReloadPlugin")}static{u(this,"DevServerReloadPlugin")}static{this.RECENT_PATH_CAP=32}constructor(t,o=!1,i){this.testimony={schemaVersion:1,lastWatchRunAt:0,lastBuildAt:0,buildsSinceSpawn:0,degraded:!1,recentPaths:[]},this.secure=o,this.base=`${o?"https":"http"}://localhost:${t}`,this.controlSock=i}post(t,o){const i=JSON.stringify(o),s=u(()=>new Promise((a,c)=>{const p=(this.secure?require("node:https"):require("node:http")).request(`${this.base}${t}`,{method:"POST",headers:{"content-type":"application/json"},rejectUnauthorized:!1},l=>{l.resume(),l.on("end",a)});p.on("error",c),p.end(i)}),"viaTcp"),n=this.controlSock;return!n||!fs.existsSync(n)?s():new Promise((a,c)=>{const p=require("node:http").request({socketPath:n,path:t,method:"POST",headers:{"content-type":"application/json"}},l=>{l.resume(),l.on("end",a)});p.on("error",c),p.end(i)}).catch(s)}snapshot(){return{...this.testimony,recentPaths:[...this.testimony.recentPaths]}}stampWatchRun(t){if(this.testimony.lastWatchRunAt=Date.now(),t.length){const o=[...this.testimony.recentPaths,...t];this.testimony.recentPaths=o.slice(-DevServerReloadPlugin.RECENT_PATH_CAP)}}degrade(t){this.testimony.degraded=t}tellWatching(){this.post("/webpack/watching",this.testimony).catch(()=>{})}apply(t){const o="DevServerReloadPlugin";let i=!1;t.hooks.watchRun?.tap(o,s=>{const n=[...s?.modifiedFiles??[],...s?.removedFiles??[]].filter(a=>typeof a=="string");if(this.stampWatchRun(n),!i){const a=s?.watchFileSystem?.watcher;a&&typeof a.on=="function"&&(i=!0,a.on("error",c=>{this.degrade(`watcher error: ${c?.message??String(c)}`),this.tellWatching()}))}this.tellWatching()}),t.hooks.watchClose?.tap(o,()=>{this.degrade("watch closed"),this.tellWatching()}),t.hooks.failed?.tap(o,s=>{this.degrade(`compiler failed: ${s?.message??String(s)}`),this.tellWatching()}),t.hooks.done.tap(o,async s=>{try{if(s.hasErrors()){this.tellWatching();return}this.testimony.lastBuildAt=Date.now(),this.testimony.buildsSinceSpawn+=1,await this.post("/webpack/reload",{watcher:this.testimony}),console.log("[DevServerReloadPlugin] notified devserver to reload")}catch{console.log("[DevServerReloadPlugin] devserver not running yet")}})}}exports.DevServerReloadPlugin=DevServerReloadPlugin;function devserverControlSock(r,t=process.env){const o=t.LENSMCP_WS_KEY||"",i=o?path.join(os.tmpdir(),o,`${r}-devserver`):path.join(os.tmpdir(),`${r}-devserver`);return path.join(i,"parent.sock")}d(devserverControlSock,"devserverControlSock"),u(devserverControlSock,"devserverControlSock");function createDevWebpackConfig(r){const{appRoot:t,outputDir:o,main:i,tsConfig:s,workspaceRoot:n,assets:a=[],port:c,memoryLimit:p=8192,buildLibsFromSource:l=!0,orgScopes:w=[],bundlePackages:b=[],additionalEntryPoints:y=[],nodeExternalsConfig:m,webpackConfigPath:v}=r,{allowlistPatterns:C,scopePrefixes:E,scopePatterns:N}=(0,build_scope_patterns_1.buildScopePatterns)(w),P=process.env.LENSMCP_TYPECHECK,f=P==="tsgo"?(0,tsgo_check_plugin_1.resolveTsgoBin)(n,process.env.LENSMCP_TSGO_BIN):null;if(P==="tsgo"&&!f){const e=`${tsgo_check_plugin_1.TSGO_MISSING_CAUSE}. Fell back to fork-ts-checker, which reports to the console only \u2014 so typecheck:// has no producer for this project until it is fixed`;console.warn(`[serve] ${r.appName}: LENSMCP_TYPECHECK=tsgo but ${e}.`),(0,tsgo_check_plugin_1.reportTsgoUnavailable)({appName:r.appName,tsConfig:s,workspaceRoot:n,detail:e})}const _=!!f,S=b.map(e=>new RegExp(`^${e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}(/|$)`)),R=[/webpack\/hot\/poll\?100/,...C,...S,...m?.allowlist||[]],k={output:{path:o,...process.env.NODE_ENV!=="production"&&{devtoolModuleFilenameTemplate:"[absolute-resource-path]"},clean:!0},externals:[(0,webpack_node_externals_1.default)({allowlist:R,additionalModuleDirs:m?.additionalModuleDirs||[],...m?.importType&&{importType:m.importType}}),({request:e},g)=>{if(e&&path.isAbsolute(e)&&e.startsWith(n)&&!e.includes("node_modules")||e&&(e.startsWith(path.join(n,"apps"))||e.startsWith(path.join(n,"libs")))||e&&E.some(h=>e.startsWith(h))||e&&N.some(h=>h.test(e))||e&&S.some(h=>h.test(e)))return g();if(e&&!(e.startsWith("./")||e.startsWith(".."))){const h=e.startsWith("@")?e.split("/").slice(0,2).join("/"):e.split("/")[0];return fs.existsSync(path.join(n,"node_modules",h))?g(null,`commonjs ${e}`):g()}return g()}],mode:"development",devtool:"eval-cheap-module-source-map",plugins:[new webpack.HotModuleReplacementPlugin,new app_plugin_1.NxAppWebpackPlugin({target:"node22",compiler:"tsc",main:i,additionalEntryPoints:y,verbose:!0,sourceMap:"eval-cheap-module-source-map",mergeExternals:!0,externalDependencies:[],memoryLimit:p,tsConfig:s,assets:a,optimization:!1,progress:!1,outputHashing:"none",generatePackageJson:!1,watchDependencies:!0,typeCheckOptions:process.env.LENSMCP_TYPECHECK==="off"||_?!1:{async:!0},buildLibsFromSource:l}),new DevServerReloadPlugin(c,r.httpsReload===!0,devserverControlSock(r.serviceName)),..._?[new tsgo_check_plugin_1.TsgoCheckPlugin({appName:r.appName,tsConfig:s,workspaceRoot:n,...process.env.LENSMCP_TSGO_BIN?{binPath:process.env.LENSMCP_TSGO_BIN}:{}})]:process.env.LENSMCP_TYPECHECK==="off"?[]:[new fork_ts_check_reporter_1.ForkTsCheckReporterPlugin({appName:r.appName,tsConfig:s,workspaceRoot:n})]],snapshot:{managedPaths:[/^(.+?[\\/])?node_modules[\\/]/]},watch:!0,watchOptions:{ignored:["**/*.env.template","**/config.template.json","**/*.md","**/dist/**","**/migrations/**"]},cache:{type:"filesystem"}};if(v){const e=require(path.resolve(t,v));return(e.default||e)(k)}return k}d(createDevWebpackConfig,"createDevWebpackConfig"),u(createDevWebpackConfig,"createDevWebpackConfig");
1
+ "use strict";var j=Object.defineProperty;var p=(r,e)=>j(r,"name",{value:e,configurable:!0});var O=Object.defineProperty,c=p((r,e)=>O(r,"name",{value:e,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DevBuildFinalizePlugin=exports.DevServerReloadPlugin=exports.devserverControlSock=exports.DevserverNotifier=void 0,exports.devCacheOptions=devCacheOptions,exports.devModuleFilename=devModuleFilename,exports.createDevWebpackConfig=createDevWebpackConfig;const tslib_1=require("tslib"),app_plugin_1=require("@nx/webpack/app-plugin"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),webpack_node_externals_1=tslib_1.__importDefault(require("webpack-node-externals")),build_scope_patterns_1=require("./build-scope-patterns"),dev_devtool_1=require("./dev-devtool"),tsgo_check_plugin_1=require("./tsgo-check-plugin"),fork_ts_check_reporter_1=require("./fork-ts-check-reporter"),perf_rules_plugin_1=require("./perf/perf-rules-plugin"),devserver_notifier_1=require("./devserver-notifier"),workspace_paths_1=require("./workspace-paths");var devserver_notifier_2=require("./devserver-notifier");Object.defineProperty(exports,"DevserverNotifier",{enumerable:!0,get:c(function(){return devserver_notifier_2.DevserverNotifier},"get")}),Object.defineProperty(exports,"devserverControlSock",{enumerable:!0,get:c(function(){return devserver_notifier_2.devserverControlSock},"get")});const webpack=require("webpack");class DevServerReloadPlugin{static{p(this,"DevServerReloadPlugin")}static{c(this,"DevServerReloadPlugin")}constructor(e,n=!1,a,o=Number(process.env.LENSMCP_GATEWAY_PID??0)>0){this.notifier=new devserver_notifier_1.DevserverNotifier(e,n,a,o)}snapshot(){return this.notifier.snapshot()}apply(e){const n="DevServerReloadPlugin";let a=!1;e.hooks.watchRun?.tap(n,o=>{const s=[...o?.modifiedFiles??[],...o?.removedFiles??[]].filter(i=>typeof i=="string");if(!a){const i=o?.watchFileSystem?.watcher;i&&typeof i.on=="function"&&(a=!0,i.on("error",l=>this.notifier.degrade(`watcher error: ${l?.message??String(l)}`)))}this.notifier.watchRun(s)}),e.hooks.watchClose?.tap(n,()=>this.notifier.degrade("watch closed")),e.hooks.failed?.tap(n,o=>this.notifier.degrade(`compiler failed: ${o?.message??String(o)}`)),e.hooks.done.tap(n,async o=>{if(o.hasErrors()){this.notifier.tellWatching();return}await this.notifier.built()})}}exports.DevServerReloadPlugin=DevServerReloadPlugin;const DEV_CACHE_MEMORY_GENERATIONS=5,DEV_CACHE_MAX_AGE_MS=10080*60*1e3,LOCKFILES=["yarn.lock","package-lock.json","pnpm-lock.yaml","bun.lock"];function clusterVersion(){for(const r of[path.join(__dirname,"package.json"),path.join(__dirname,"..","package.json")])try{const e=JSON.parse(fs.readFileSync(r,"utf8"));if(e.name==="@lensmcp/cluster"&&e.version)return e.version}catch{}return"unknown"}p(clusterVersion,"clusterVersion"),c(clusterVersion,"clusterVersion");function devCacheOptions(r,e){const{appName:n,appRoot:a,workspaceRoot:o,tsConfig:s,webpackConfigPath:i}=r,l=clusterVersion(),_=JSON.stringify({lensmcp:l,devtool:e,buildLibsFromSource:r.buildLibsFromSource??!0,orgScopes:r.orgScopes??[],bundlePackages:r.bundlePackages??[],nodeExternalsConfig:r.nodeExternalsConfig??null}),g=c(f=>f.filter(d=>fs.existsSync(d)),"existing"),k=(0,workspace_paths_1.readWorkspacePaths)(s,o)?.configFiles??[path.resolve(o,s)],v={lensmcp:[__filename],lockfile:g(LOCKFILES.map(f=>path.join(o,f))),tsconfig:g([...new Set([...k,path.join(o,"tsconfig.base.json")])])};return i&&(v.overrides=[path.resolve(a,i)]),{type:"filesystem",cacheDirectory:path.join(o,"node_modules",".cache","lensmcp-webpack"),name:n.replace(/[^\w.-]+/g,"_"),version:`${l}-${(0,node_crypto_1.createHash)("sha256").update(_).digest("hex").slice(0,16)}`,buildDependencies:v,maxMemoryGenerations:DEV_CACHE_MEMORY_GENERATIONS,maxAge:DEV_CACHE_MAX_AGE_MS}}p(devCacheOptions,"devCacheOptions"),c(devCacheOptions,"devCacheOptions");function devModuleFilename(r){const e=r.absoluteResourcePath;return path.isAbsolute(e)?`file://${e}`:e}p(devModuleFilename,"devModuleFilename"),c(devModuleFilename,"devModuleFilename");class DevBuildFinalizePlugin{static{p(this,"DevBuildFinalizePlugin")}static{c(this,"DevBuildFinalizePlugin")}constructor(e){this.options=e}apply(e){const{cache:n}=this.options;e.options.cache={...n,buildDependencies:{...n.buildDependencies}},e.options.context&&(e.context=e.options.context);const a=e.options.resolve?.plugins??[];if(a.some(o=>o instanceof workspace_paths_1.WorkspacePathsResolverPlugin)){const o=new Set(a);e.hooks.afterPlugins.tap("DevBuildFinalizePlugin",()=>{const s=e.options.resolve;s.plugins=s.plugins.filter(i=>o.has(i)||i?.constructor?.name!=="TsconfigPathsPlugin")})}if(this.options.tsSourceMaps)for(const o of e.options.module?.rules??[])typeof o?.loader!="string"||!/[\\/]ts-loader[\\/]/.test(o.loader)||(o.options={...o.options,compilerOptions:{...o.options?.compilerOptions,sourceMap:!0,inlineSourceMap:!1}})}}exports.DevBuildFinalizePlugin=DevBuildFinalizePlugin;function createDevWebpackConfig(r){const{appRoot:e,outputDir:n,main:a,tsConfig:o,workspaceRoot:s,assets:i=[],port:l,memoryLimit:_=8192,buildLibsFromSource:g=!0,orgScopes:k=[],bundlePackages:v=[],additionalEntryPoints:f=[],nodeExternalsConfig:d,webpackConfigPath:b}=r,{allowlistPatterns:y,scopePrefixes:x,scopePatterns:M}=(0,build_scope_patterns_1.buildScopePatterns)(k),S=process.env.LENSMCP_TYPECHECK,C=S==="tsgo"?(0,tsgo_check_plugin_1.resolveTsgoBin)(s,process.env.LENSMCP_TSGO_BIN):null;if(S==="tsgo"&&!C){const t=`${tsgo_check_plugin_1.TSGO_MISSING_CAUSE}. Fell back to fork-ts-checker, which reports to the console only \u2014 so typecheck:// has no producer for this project until it is fixed`;console.warn(`[serve] ${r.appName}: LENSMCP_TYPECHECK=tsgo but ${t}.`),(0,tsgo_check_plugin_1.reportTsgoUnavailable)({appName:r.appName,tsConfig:o,workspaceRoot:s,detail:t})}const w=!!C,P=(0,dev_devtool_1.resolveDevDevtool)(r.devtool);P.rejected&&console.warn(`[serve] ${r.appName}: '${P.rejected}' (devtool / ${dev_devtool_1.DEV_DEVTOOL_ENV}) is not a webpack devtool \u2014 building with '${dev_devtool_1.DEFAULT_DEV_DEVTOOL}'.`);const h=P.devtool,E=g?(0,workspace_paths_1.readWorkspacePaths)(o,s):null,D=v.map(t=>new RegExp(`^${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}(/|$)`)),R=[/webpack\/hot\/poll\?100/,...y,...D,...d?.allowlist||[]],N={output:{path:n,...process.env.NODE_ENV!=="production"&&{devtoolModuleFilenameTemplate:devModuleFilename},clean:!0},externals:[(0,webpack_node_externals_1.default)({allowlist:R,additionalModuleDirs:d?.additionalModuleDirs||[],...d?.importType&&{importType:d.importType}}),({request:t},m)=>{if(t&&path.isAbsolute(t)&&t.startsWith(s)&&!t.includes("node_modules")||t&&(t.startsWith(path.join(s,"apps"))||t.startsWith(path.join(s,"libs")))||t&&x.some(u=>t.startsWith(u))||t&&M.some(u=>u.test(t))||t&&D.some(u=>u.test(t)))return m();if(t&&!(t.startsWith("./")||t.startsWith(".."))){const u=t.startsWith("@")?t.split("/").slice(0,2).join("/"):t.split("/")[0];return fs.existsSync(path.join(s,"node_modules",u))?m(null,`commonjs ${t}`):m()}return m()}],mode:"development",devtool:h,...(0,workspace_paths_1.hasPathMappings)(E)&&{resolve:{plugins:[new workspace_paths_1.WorkspacePathsResolverPlugin(E)]}},plugins:[new webpack.HotModuleReplacementPlugin,new app_plugin_1.NxAppWebpackPlugin({target:"node22",compiler:"tsc",main:a,additionalEntryPoints:f,verbose:!0,sourceMap:h,mergeExternals:!0,externalDependencies:[],memoryLimit:_,tsConfig:o,assets:i,optimization:!1,progress:!1,outputHashing:"none",generatePackageJson:!1,watchDependencies:!0,typeCheckOptions:process.env.LENSMCP_TYPECHECK==="off"||w?!1:{async:!0},buildLibsFromSource:g}),new DevBuildFinalizePlugin({cache:devCacheOptions(r,h),tsSourceMaps:(0,dev_devtool_1.bundleCarriesSourceMaps)(h)}),new DevServerReloadPlugin(l,r.httpsReload===!0,(0,devserver_notifier_1.devserverControlSock)(r.serviceName)),new perf_rules_plugin_1.PerfRulesPlugin({project:process.env.LENSMCP_PROJECT||r.appName,workspaceRoot:s,tsConfig:o}),...w?[new tsgo_check_plugin_1.TsgoCheckPlugin({appName:r.appName,tsConfig:o,workspaceRoot:s,...process.env.LENSMCP_TSGO_BIN?{binPath:process.env.LENSMCP_TSGO_BIN}:{}})]:process.env.LENSMCP_TYPECHECK==="off"?[]:[new fork_ts_check_reporter_1.ForkTsCheckReporterPlugin({appName:r.appName,tsConfig:o,workspaceRoot:s})]],snapshot:{managedPaths:[/^(.+?[\\/])?node_modules[\\/]/]},watch:!0,watchOptions:{ignored:["**/*.env.template","**/config.template.json","**/*.md","**/dist/**","**/migrations/**"]}};if(b){const t=require(path.resolve(e,b));return(t.default||t)(N)}return N}p(createDevWebpackConfig,"createDevWebpackConfig"),c(createDevWebpackConfig,"createDevWebpackConfig");
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What a DEV build emits for each module's source, and whether the processes that run it need Node's
3
+ * source-map support (issues/closed/pod-memory-03).
4
+ *
5
+ * The default used to be `eval-cheap-module-source-map`, with every pod and worker started under
6
+ * `--enable-source-maps`. That kept each bundle in a pod as text several times over — `main.js` itself,
7
+ * every module's eval string, an inline map per module and Node's parsed copy of each map: 130 MB of a
8
+ * 510 MB live heap on foodguard's audit pod, maps 59% of the bytes webpack wrote. And they bought little:
9
+ * a tsconfig without `sourceMap` makes ts-loader emit no map, so the "module" maps re-mapped transpiled
10
+ * JS onto itself, and their `sources` named files that do not exist.
11
+ *
12
+ * `eval` keeps what a developer reads off a stack — each frame names the module it is in, by its absolute
13
+ * path (the `sourceURL` webpack appends to each module's eval) — and keeps rebuilds as cheap as before
14
+ * (webpack caches each module's eval string). The line is the transpiled one. Maps stay one switch away:
15
+ * `devtool` on the serve target, or `LENSMCP_DEV_DEVTOOL`.
16
+ */
17
+ export declare const DEFAULT_DEV_DEVTOOL = "eval";
18
+ /** Picks the dev build's devtool for every service a process starts (`devtool` on the target wins). */
19
+ export declare const DEV_DEVTOOL_ENV = "LENSMCP_DEV_DEVTOOL";
20
+ /** Set by the serve executor on the devserver it forks: `1` when the bundle carries source maps a pod can
21
+ * read, so pods and workers run under `--enable-source-maps`; anything else, and they do not. */
22
+ export declare const DEV_SOURCE_MAPS_ENV = "LENSMCP_DEV_SOURCE_MAPS";
23
+ /** A webpack `devtool` value: `false` for none. */
24
+ export type DevDevtool = string | false;
25
+ export interface DevDevtoolChoice {
26
+ devtool: DevDevtool;
27
+ /** Set when the requested value was not a devtool webpack knows — the default was used instead. */
28
+ rejected?: string;
29
+ }
30
+ /**
31
+ * The devtool for a dev build: the `devtool` option when given, else `LENSMCP_DEV_DEVTOOL`, else `eval`.
32
+ * `false`/`off`/`none`/`0` mean no devtool at all. A value webpack would not recognise is refused rather
33
+ * than passed on — webpack silently treats one as "no devtool", and the stack frames would lose their
34
+ * module names with nothing saying why.
35
+ */
36
+ export declare function resolveDevDevtool(option: DevDevtool | undefined, env?: NodeJS.ProcessEnv): DevDevtoolChoice;
37
+ /**
38
+ * Whether a bundle built with `devtool` carries source maps a running process can find: an inline map,
39
+ * or a `.map` file the bundle links to. `eval` and `false` emit none, and a `hidden-` map is written but
40
+ * never referenced from the bundle, so Node could not load it anyway.
41
+ */
42
+ export declare function bundleCarriesSourceMaps(devtool: unknown): boolean;
43
+ /** The `execArgv` a pod or worker starts with for the bundle it runs: `--enable-source-maps` only when the
44
+ * bundle has maps — Node keeps a parsed copy of every map it loads, for the life of the module. */
45
+ export declare function sourceMapExecArgv(env?: NodeJS.ProcessEnv): string[];
package/dev-devtool.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var D=Object.defineProperty;var E=(e,o)=>D(e,"name",{value:o,configurable:!0});var t=Object.defineProperty,s=E((e,o)=>t(e,"name",{value:o,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DEV_SOURCE_MAPS_ENV=exports.DEV_DEVTOOL_ENV=exports.DEFAULT_DEV_DEVTOOL=void 0,exports.resolveDevDevtool=resolveDevDevtool,exports.bundleCarriesSourceMaps=bundleCarriesSourceMaps,exports.sourceMapExecArgv=sourceMapExecArgv,exports.DEFAULT_DEV_DEVTOOL="eval",exports.DEV_DEVTOOL_ENV="LENSMCP_DEV_DEVTOOL",exports.DEV_SOURCE_MAPS_ENV="LENSMCP_DEV_SOURCE_MAPS";const DEVTOOL_PATTERN=/^(?:eval|(?:inline-|hidden-|eval-)?(?:nosources-)?(?:cheap-(?:module-)?)?source-map(?:-debugids)?)$/,NO_DEVTOOL=/^(?:false|off|none|0)$/i;function resolveDevDevtool(e,o=process.env){const r=e===!1?"false":(e??o[exports.DEV_DEVTOOL_ENV]??"").trim();return r?NO_DEVTOOL.test(r)?{devtool:!1}:DEVTOOL_PATTERN.test(r)?{devtool:r}:{devtool:exports.DEFAULT_DEV_DEVTOOL,rejected:r}:{devtool:exports.DEFAULT_DEV_DEVTOOL}}E(resolveDevDevtool,"resolveDevDevtool"),s(resolveDevDevtool,"resolveDevDevtool");function bundleCarriesSourceMaps(e){return typeof e=="string"&&e.includes("source-map")&&!e.startsWith("hidden-")}E(bundleCarriesSourceMaps,"bundleCarriesSourceMaps"),s(bundleCarriesSourceMaps,"bundleCarriesSourceMaps");function sourceMapExecArgv(e=process.env){return e[exports.DEV_SOURCE_MAPS_ENV]==="1"?["--enable-source-maps"]:[]}E(sourceMapExecArgv,"sourceMapExecArgv"),s(sourceMapExecArgv,"sourceMapExecArgv");
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What a dev build tells its devserver parent: "reload" (`POST /webpack/reload` → rolling hot-swap of the pod
3
+ * children) and the WATCHER TESTIMONY (`POST /webpack/watching`): every `watchRun`, build, failure and close, plus
4
+ * any watchpack error. The parent relays the latest testimony to its children, which serve it in
5
+ * `/__lensmcp/alive`; the gateway's pod sweeper reads it before a `stale-source-set` recycle (`lifecycle.ts`
6
+ * `podWatcherVerdict`). Without it the sweeper had NO evidence and scaled every pod to zero on every source-set
7
+ * change in its scope (issues/closed/dev-gateway-07). Every post is best-effort: a parent that predates
8
+ * `/webpack/watching` answers 404 and nothing is said.
9
+ *
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).
12
+ */
13
+ export declare class DevserverNotifier {
14
+ private base;
15
+ private secure;
16
+ /** The devserver parent's CONTROL socket — preferred over the TCP port (see `post`). */
17
+ private controlSock;
18
+ /** Run by a gateway: the control socket is the ONLY way to the parent (see `post`). */
19
+ private readonly managed;
20
+ /** Bounded, newest-last paths of the last few `watchRun`s — diagnostics for the register, not policy. */
21
+ private static readonly RECENT_PATH_CAP;
22
+ private testimony;
23
+ constructor(port: number, secure?: boolean, controlSock?: string, managed?: boolean);
24
+ /**
25
+ * Deliver to the parent's CONTROL SOCKET first (`$TMPDIR[/<wsKey>]/<service>-devserver/parent.sock`), then
26
+ * the TCP port. The socket path is bind-STOLEN by whichever parent generation started last, so during a
27
+ * rolling recycle (lensmcp issues/closed/dev-gateway-09) this webpack's rebuilds reach ITS OWN parent even while the
28
+ * retiring generation still holds the TCP port; the port is the fallback for a parent without the socket.
29
+ */
30
+ private post;
31
+ /** The current testimony snapshot (the wire shape the parent receives). */
32
+ snapshot(): PodWatcherTestimonyWire;
33
+ /** The watcher started a run for these changed paths. */
34
+ watchRun(paths: string[]): void;
35
+ /** The watcher can no longer be trusted to see every change. */
36
+ degrade(reason: string): void;
37
+ /** Send the testimony as it stands. */
38
+ tellWatching(): void;
39
+ /**
40
+ * A build without errors finished. `reload: false` — the bundle this devserver loads did not change (a shared
41
+ * compilation rebuilt for another service): the testimony is updated, the pods are left alone.
42
+ */
43
+ built(reload?: boolean): Promise<void>;
44
+ }
45
+ /** The devserver parent's control socket for `serviceName` — the SAME derivation as `main.devserver.ts`'s
46
+ * `SOCK_DIR` (`$TMPDIR[/<LENSMCP_WS_KEY>]/<service>-devserver/parent.sock`). */
47
+ export declare function devserverControlSock(serviceName: string, env?: NodeJS.ProcessEnv): string;
48
+ /** Wire shape of the testimony (mirrors `PodWatcherTestimony` in the gateway runtime; kept structural so the
49
+ * webpack side does not import the gateway's types). */
50
+ export interface PodWatcherTestimonyWire {
51
+ schemaVersion: 1;
52
+ lastWatchRunAt: number;
53
+ lastBuildAt: number;
54
+ buildsSinceSpawn: number;
55
+ degraded: false | string;
56
+ recentPaths: string[];
57
+ }
@@ -0,0 +1 @@
1
+ "use strict";var u=Object.defineProperty;var h=(r,e)=>u(r,"name",{value:e,configurable:!0});var p=Object.defineProperty,a=h((r,e)=>p(r,"name",{value:e,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DevserverNotifier=void 0,exports.devserverControlSock=devserverControlSock;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"));class DevserverNotifier{static{h(this,"DevserverNotifier")}static{a(this,"DevserverNotifier")}static{this.RECENT_PATH_CAP=32}constructor(e,t=!1,s,c=Number(process.env.LENSMCP_GATEWAY_PID??0)>0){this.testimony={schemaVersion:1,lastWatchRunAt:0,lastBuildAt:0,buildsSinceSpawn:0,degraded:!1,recentPaths:[]},this.managed=c,this.secure=t,this.base=`${t?"https":"http"}://localhost:${e}`,this.controlSock=s}post(e,t){const s=JSON.stringify(t),c=a(()=>new Promise((o,l)=>{const i=(this.secure?require("node:https"):require("node:http")).request(`${this.base}${e}`,{method:"POST",headers:{"content-type":"application/json"},rejectUnauthorized:!1},n=>{n.resume(),n.on("end",o)});i.on("error",l),i.end(s)}),"viaTcp"),d=this.controlSock;return!d||!fs.existsSync(d)?this.managed?Promise.reject(new Error("the devserver control socket is not up yet")):c():new Promise((o,l)=>{const i=require("node:http").request({socketPath:d,path:e,method:"POST",headers:{"content-type":"application/json"}},n=>{n.resume(),n.on("end",o)});i.on("error",l),i.end(s)}).catch(o=>{if(this.managed)throw o;return c()})}snapshot(){return{...this.testimony,recentPaths:[...this.testimony.recentPaths]}}watchRun(e){if(this.testimony.lastWatchRunAt=Date.now(),e.length){const t=[...this.testimony.recentPaths,...e];this.testimony.recentPaths=t.slice(-DevserverNotifier.RECENT_PATH_CAP)}this.tellWatching()}degrade(e){this.testimony.degraded=e,this.tellWatching()}tellWatching(){this.post("/webpack/watching",this.testimony).catch(()=>{})}async built(e=!0){if(this.testimony.lastBuildAt=Date.now(),this.testimony.buildsSinceSpawn+=1,!e){this.tellWatching();return}try{await this.post("/webpack/reload",{watcher:this.testimony}),console.log("[DevServerReloadPlugin] notified devserver to reload")}catch{console.log("[DevServerReloadPlugin] devserver not running yet")}}}exports.DevserverNotifier=DevserverNotifier;function devserverControlSock(r,e=process.env){const t=e.LENSMCP_WS_KEY||"",s=t?path.join(os.tmpdir(),t,`${r}-devserver`):path.join(os.tmpdir(),`${r}-devserver`);return path.join(s,"parent.sock")}h(devserverControlSock,"devserverControlSock"),a(devserverControlSock,"devserverControlSock");
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The bidirectional progress bound — SHARED by both gateways (lifted from `prod-runtime/body-stall.ts`;
3
- * the dev proxy arms the same guard on its h1 and h2 paths, issues/dev-gateway/12).
3
+ * the dev proxy arms the same guard on its h1 and h2 paths, issues/closed/dev-gateway-12).
4
4
  *
5
5
  * `requestTimeout` is deliberately 0 on this proxy — a hard request deadline would cut the
6
6
  * long-lived streaming (SSE, streamable-HTTP MCP) it exists to carry. But `headersTimeout`
@@ -34,4 +34,16 @@ export interface BodyStallGuard {
34
34
  armResponse(res: http.ServerResponse): void;
35
35
  stop(): void;
36
36
  }
37
+ /** Does this request carry a body at all? Only one that does can stall mid-body — or still be arriving. */
38
+ export declare function requestCarriesBody(req: http.IncomingMessage): boolean;
39
+ /**
40
+ * Is the client still SENDING this request's body? The inbound half of what the stall bound watches: a
41
+ * request that carries a body and has not completed.
42
+ *
43
+ * The dev proxy's first-byte guard reads it too (issues/closed/codex-security-27). An upstream body parser
44
+ * produces no headers until the last byte is in, so a pod "silent" while the body is still arriving is
45
+ * waiting on the CLIENT — that time is not the pod's, and must never count toward evicting it. Read from
46
+ * the request's own state rather than this guard's sampler, so it holds when the stall bound is disabled.
47
+ */
48
+ export declare function requestBodyArriving(req: http.IncomingMessage): boolean;
37
49
  export declare function createBodyStallGuard(stallMs: number): BodyStallGuard;
@@ -1 +1 @@
1
- "use strict";var u=Object.defineProperty;var a=(r,s)=>u(r,"name",{value:s,configurable:!0});var y=Object.defineProperty,c=a((r,s)=>y(r,"name",{value:s,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createBodyStallGuard=createBodyStallGuard;const OFF={arm:c(()=>{},"arm"),armResponse:c(()=>{},"armResponse"),stop:c(()=>{},"stop")};function createBodyStallGuard(r){if(!Number.isFinite(r)||r<=0)return OFF;const s=new Map,i=new Map,l=Math.max(500,Math.min(r,5e3)),d=setInterval(c(()=>{if(s.size===0&&i.size===0)return;const e=Date.now();for(const[t,n]of s){if(t.complete||t.destroyed||!t.socket||t.socket.destroyed){s.delete(t);continue}const o=t.socket.bytesRead;if(o!==n.bytes){n.bytes=o,n.since=e;continue}e-n.since>=r&&(s.delete(t),t.socket.destroy())}for(const[t,n]of i){const o=t.socket;if(t.writableEnded||t.destroyed||!o||o.destroyed){i.delete(t);continue}if(o.writableLength===0){n.bytes=o.bytesWritten,n.since=e;continue}if(o.bytesWritten!==n.bytes){n.bytes=o.bytesWritten,n.since=e;continue}e-n.since>=r&&(i.delete(t),o.destroy())}},"tick"),l);return d.unref?.(),{arm(e){const t=e.headers["content-length"];if(!(String(e.headers["transfer-encoding"]??"").toLowerCase().includes("chunked")||typeof t=="string"&&Number(t)>0)||e.complete||!e.socket)return;s.set(e,{bytes:e.socket.bytesRead,since:Date.now()});const n=c(()=>{s.delete(e)},"done");e.on("end",n),e.on("close",n),e.on("aborted",n)},armResponse(e){const t=e.socket;if(!t||e.writableEnded||e.destroyed)return;i.set(e,{bytes:t.bytesWritten,since:Date.now()});const n=c(()=>{i.delete(e)},"done");e.on("finish",n),e.on("close",n)},stop(){clearInterval(d),s.clear(),i.clear()}}}a(createBodyStallGuard,"createBodyStallGuard"),c(createBodyStallGuard,"createBodyStallGuard");
1
+ "use strict";var l=Object.defineProperty;var d=(s,o)=>l(s,"name",{value:o,configurable:!0});var u=Object.defineProperty,r=d((s,o)=>u(s,"name",{value:o,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.requestCarriesBody=requestCarriesBody,exports.requestBodyArriving=requestBodyArriving,exports.createBodyStallGuard=createBodyStallGuard;const OFF={arm:r(()=>{},"arm"),armResponse:r(()=>{},"armResponse"),stop:r(()=>{},"stop")};function requestCarriesBody(s){const o=s.headers["content-length"];return String(s.headers["transfer-encoding"]??"").toLowerCase().includes("chunked")||typeof o=="string"&&Number(o)>0}d(requestCarriesBody,"requestCarriesBody"),r(requestCarriesBody,"requestCarriesBody");function requestBodyArriving(s){return requestCarriesBody(s)&&!s.complete&&!s.destroyed}d(requestBodyArriving,"requestBodyArriving"),r(requestBodyArriving,"requestBodyArriving");function createBodyStallGuard(s){if(!Number.isFinite(s)||s<=0)return OFF;const o=new Map,c=new Map,y=Math.max(500,Math.min(s,5e3)),a=setInterval(r(()=>{if(o.size===0&&c.size===0)return;const e=Date.now();for(const[t,n]of o){if(t.complete||t.destroyed||!t.socket||t.socket.destroyed){o.delete(t);continue}const i=t.socket.bytesRead;if(i!==n.bytes){n.bytes=i,n.since=e;continue}e-n.since>=s&&(o.delete(t),t.socket.destroy())}for(const[t,n]of c){const i=t.socket;if(t.writableEnded||t.destroyed||!i||i.destroyed){c.delete(t);continue}if(i.writableLength===0){n.bytes=i.bytesWritten,n.since=e;continue}if(i.bytesWritten!==n.bytes){n.bytes=i.bytesWritten,n.since=e;continue}e-n.since>=s&&(c.delete(t),i.destroy())}},"tick"),y);return a.unref?.(),{arm(e){if(!requestCarriesBody(e)||e.complete||!e.socket)return;o.set(e,{bytes:e.socket.bytesRead,since:Date.now()});const t=r(()=>{o.delete(e)},"done");e.on("end",t),e.on("close",t),e.on("aborted",t)},armResponse(e){const t=e.socket;if(!t||e.writableEnded||e.destroyed)return;c.set(e,{bytes:t.bytesWritten,since:Date.now()});const n=r(()=>{c.delete(e)},"done");e.on("finish",n),e.on("close",n)},stop(){clearInterval(a),o.clear(),c.clear()}}}d(createBodyStallGuard,"createBodyStallGuard"),r(createBodyStallGuard,"createBodyStallGuard");
@@ -2,7 +2,7 @@
2
2
  * The ONE request-path canonicalizer — shared by BOTH gateways.
3
3
  *
4
4
  * Lifted out of `prod-runtime/edge.ts` (prod-gateway-2 #01/#03/#04/#12) so the dev gateway can stop
5
- * routing, authorising and forwarding the RAW `req.url` (issues/dev-gateway/11: `/./admin`, an
5
+ * routing, authorising and forwarding the RAW `req.url` (issues/closed/dev-gateway-11: `/./admin`, an
6
6
  * absolute-form target and `/..;/admin` all skipped dev's path policy while prod had been fixed).
7
7
  * `prod-runtime/edge.ts` and `runtime/edge.ts` re-export everything here, so each pipeline keeps its
8
8
  * import surface and neither can drift from the other.
@@ -10,7 +10,7 @@
10
10
  * Dev used the raw socket peer for the ABAC `ip` attribute and passed the client's own `X-Forwarded-For`
11
11
  * through (h2 path appended to it; h1 path forwarded it verbatim), so behind a front proxy (the Caddy the
12
12
  * workspace once ran on :443) an `ip` rule saw the proxy and a client could prepend any chain it liked
13
- * (issues/dev-gateway/11, the resolver half).
13
+ * (issues/closed/dev-gateway-11, the resolver half).
14
14
  */
15
15
  import type * as http from 'node:http';
16
16
  export interface ClientIpOptions {
@@ -1 +1 @@
1
- "use strict";var u=Object.defineProperty;var i=(t,r)=>u(t,"name",{value:r,configurable:!0});var p=Object.defineProperty,n=i((t,r)=>p(t,"name",{value:r,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=gatewayExecutor;const devkit_1=require("@nx/devkit"),gateway_lib_1=require("./gateway.lib"),server_1=require("./runtime/server");async function gatewayExecutor(t,r){process.on("uncaughtException",e=>{if((e.syscall==="read"||e.syscall==="write")&&server_1.BENIGN_SOCKET_ERRNOS.has(e.code??"")){console.warn(`[gateway] survived a stray socket ${e.syscall} error (${e.code}) \u2014 one connection dropped, the daemon lives`);return}console.error(e),process.exit(1)});let l;try{let e;try{const s=await(0,devkit_1.createProjectGraphAsync)();e=Object.fromEntries(Object.entries(s.dependencies).map(([o,c])=>[o,c.map(a=>a.target)]))}catch{}l=await(0,gateway_lib_1.startGateway)(t,{...r,...e?{projectDependencies:e}:{}})}catch(e){return console.error(e.message),{success:!1}}return new Promise(e=>{let s=!1;const o=n(c=>{if(s)return;s=!0;let a=!0;try{process.kill(process.ppid,0)}catch{a=!1}console.log(`[gateway] received ${c} (pid ${process.pid}, parent ${process.ppid} ${a?"alive":"gone \u2014 orphaned"}) \u2014 shutting down`),l.stop().then(()=>e({success:!0}))},"stop");process.on("SIGINT",()=>o("SIGINT")),process.on("SIGTERM",()=>o("SIGTERM"))})}i(gatewayExecutor,"gatewayExecutor"),n(gatewayExecutor,"gatewayExecutor"),module.exports=gatewayExecutor,module.exports.default=gatewayExecutor,module.exports.startGateway=gateway_lib_1.startGateway;
1
+ "use strict";var d=Object.defineProperty;var p=(t,r)=>d(t,"name",{value:r,configurable:!0});var l=Object.defineProperty,n=p((t,r)=>l(t,"name",{value:r,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=gatewayExecutor;const gateway_lib_1=require("./gateway.lib"),project_graph_store_1=require("./runtime/project-graph-store"),server_1=require("./runtime/server");async function gatewayExecutor(t,r){process.on("uncaughtException",e=>{if((e.syscall==="read"||e.syscall==="write")&&server_1.BENIGN_SOCKET_ERRNOS.has(e.code??"")){console.warn(`[gateway] survived a stray socket ${e.syscall} error (${e.code}) \u2014 one connection dropped, the daemon lives`);return}console.error(e),process.exit(1)});let i;try{const e=(0,project_graph_store_1.createProjectGraphStore)(r.root,()=>(0,project_graph_store_1.computeProjectGraphInProcess)(r.root)),o=(await e.refresh())?.projectGraph,s=o?Object.fromEntries(Object.entries(o.dependencies).map(([c,a])=>[c,a.map(u=>u.target)])):void 0;i=await(0,gateway_lib_1.startGateway)(t,{...r,...s?{projectDependencies:s}:{},projectGraphStore:e})}catch(e){return console.error(e.message),{success:!1}}return new Promise(e=>{let o=!1;const s=n(c=>{if(o)return;o=!0;let a=!0;try{process.kill(process.ppid,0)}catch{a=!1}console.log(`[gateway] received ${c} (pid ${process.pid}, parent ${process.ppid} ${a?"alive":"gone \u2014 orphaned"}) \u2014 shutting down`),i.stop().then(()=>e({success:!0}))},"stop");process.on("SIGINT",()=>s("SIGINT")),process.on("SIGTERM",()=>s("SIGTERM"))})}p(gatewayExecutor,"gatewayExecutor"),n(gatewayExecutor,"gatewayExecutor"),module.exports=gatewayExecutor,module.exports.default=gatewayExecutor,module.exports.startGateway=gateway_lib_1.startGateway;
@@ -7,7 +7,7 @@
7
7
  * `LENSMCP_GW_JWT_AUD` as the required restrictions for the fallback it also documents. An operator
8
8
  * following that table configured an identity control that did not exist, and a token signed with
9
9
  * the same shared key by another issuer or for another audience was accepted, satisfied edge ABAC,
10
- * and was stamped into gateway-owned `x-user-*` headers (issues/codex-security/01).
10
+ * and was stamped into gateway-owned `x-user-*` headers (issues/closed/codex-security-01).
11
11
  *
12
12
  * Two verifiers that must agree about identity should not each carry their own copy of the rules.
13
13
  */
@@ -60,7 +60,7 @@ export declare function parseGatewayEnv(env: NodeJS.ProcessEnv): GatewayEnvConfi
60
60
  /**
61
61
  * Will the Redis-backed rate limiter actually be used — as opposed to merely asked for?
62
62
  *
63
- * issues/codex-security/31: the primary's multi-worker warning tested the DECLARED INTENT
63
+ * issues/closed/codex-security-31: the primary's multi-worker warning tested the DECLARED INTENT
64
64
  * (`LENSMCP_GW_RATE_LIMIT_REDIS=1` or a dedicated URL) and suppressed itself whenever either was
65
65
  * present. Whether Redis is reachable at all is decided later, per worker, where the limiter falls
66
66
  * back to the per-process in-memory one and logs a line that says nothing about multiplication. So
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The stall guard lives in `../body-stall` — SHARED with the dev gateway (issues/dev-gateway/12) so the
2
+ * The stall guard lives in `../body-stall` — SHARED with the dev gateway (issues/closed/dev-gateway-12) so the
3
3
  * two proxies bound a stalled transfer the same way. Re-exported so every prod import keeps its address.
4
4
  */
5
5
  export { createBodyStallGuard, type BodyStallGuard } from '../body-stall';
@@ -69,7 +69,7 @@ export interface RedisRateLimiterOptions {
69
69
  *
70
70
  * A limiter that fails open answers "allowed" to everything and says nothing, while startup has
71
71
  * already logged that GLOBAL rate limiting is on — so a namespace/ACL mismatch looks exactly like
72
- * a working limiter (issues/codex-security/34). Once, not per request: the point is to be noticed,
72
+ * a working limiter (issues/closed/codex-security-34). Once, not per request: the point is to be noticed,
73
73
  * not to become the flood.
74
74
  */
75
75
  onFailure?: (reason: string) => void;
@@ -5,7 +5,7 @@ export interface EdgeAuth {
5
5
  internal(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
6
6
  /**
7
7
  * `cpath` is the DECODED canonical path the caller already derived (`canonicalize(...).policy`) — the
8
- * one string routing, policy and forwarding share (issues/dev-gateway/11). A caller that has none
8
+ * one string routing, policy and forwarding share (issues/closed/dev-gateway-11). A caller that has none
9
9
  * gets it re-derived from `req.url`, so the override is never matched against a raw target again.
10
10
  */
11
11
  public(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined, cpath?: string): AuthVerdict;
@@ -15,7 +15,7 @@ export declare function isLoopbackRequest(req: Pick<http.IncomingMessage, 'socke
15
15
  * The registry stores absolute paths because the gateway needs them; the LANDING PAGE needs only
16
16
  * enough to tell two workspaces apart. An absolute root carries the developer's username and the
17
17
  * machine's directory layout, and this page used to render every registered one — including
18
- * workspaces whose gateways are not even running (issues/codex-security/26).
18
+ * workspaces whose gateways are not even running (issues/closed/codex-security-26).
19
19
  *
20
20
  * Home-relative, last two segments: `/Users/dana/work/acme/web` → `~/…/acme/web`.
21
21
  */
@@ -28,7 +28,7 @@ export declare function renderChooserPage(entries: WorkspaceEntry[], currentKey:
28
28
  * LOOPBACK ONLY. This is a listing of every workspace the developer has ever registered — names,
29
29
  * roots and whether each gateway is running — and it used to be served as an *unrouted* request,
30
30
  * which means before route authentication and before the configured after-auth middleware
31
- * (issues/codex-security/26). A caller from elsewhere gets the same 404 any other unrouted path
31
+ * (issues/closed/codex-security-26). A caller from elsewhere gets the same 404 any other unrouted path
32
32
  * gets: a landing page that announces itself to the network is not a 404 page.
33
33
  */
34
34
  export declare function serveChooser(res: http.ServerResponse, currentKey: string, req?: Pick<http.IncomingMessage, 'socket'>): void;
@@ -26,7 +26,7 @@ export interface ControlServer {
26
26
  * request, and an install rewrites that manifest in place: so the daemon it was meant to expose
27
27
  * answered with the NEW version the moment the files landed, and `lensmcp upgrade` concluded
28
28
  * "nothing to do" beside a process still running the old bundle (foodguard, 2026-10-06 —
29
- * issues/foodguard/33). What is on disk now is not what was loaded then.
29
+ * issues/closed/foodguard-33). What is on disk now is not what was loaded then.
30
30
  */
31
31
  export declare const clusterVersion: () => string | undefined;
32
32
  /** Take the answer now; give that same answer for the life of the process. */
@@ -46,7 +46,7 @@ export declare function readClusterVersion(from: string): string | undefined;
46
46
  * Nothing enforced owner-only access: `mkdirSync` supplied no mode, an existing directory was
47
47
  * accepted whatever its permissions, and `LENSMCP_HOME` can point the whole thing somewhere
48
48
  * permissive. Under a cooperative `0002` umask that left a group-writable control plane
49
- * (issues/codex-security/25).
49
+ * (issues/closed/codex-security-25).
50
50
  *
51
51
  * Returns false when the path exists and is not a directory — the caller decides what to do rather
52
52
  * than proceeding on a bad assumption.
@@ -54,5 +54,5 @@ export declare function readClusterVersion(from: string): string | undefined;
54
54
  export declare function ensureOwnerOnlyDir(dir: string): boolean;
55
55
  /** Start the control listener. A filesystem socket is unreachable off-host, but NOT unreachable by
56
56
  * other users on this machine — so the home is owner-only and the socket is 0600
57
- * (issues/codex-security/25). */
57
+ * (issues/closed/codex-security-25). */
58
58
  export declare function startControlServer(deps: ControlServerDeps): ControlServer;
@@ -1,2 +1,2 @@
1
- "use strict";var k=Object.defineProperty;var u=(t,e)=>k(t,"name",{value:e,configurable:!0});var h=Object.defineProperty,d=u((t,e)=>h(t,"name",{value:e,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.clusterVersion=void 0,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"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log");function controlSocketPath(){const t=process.env.LENSMCP_HOME||os.homedir();return path.join(t,".lensmcp","control.sock")}u(controlSocketPath,"controlSocketPath"),d(controlSocketPath,"controlSocketPath"),exports.clusterVersion=versionAtLoad(()=>readClusterVersion(__dirname));function versionAtLoad(t){const e=t();return()=>e}u(versionAtLoad,"versionAtLoad"),d(versionAtLoad,"versionAtLoad");function readClusterVersion(t){let e=t;for(let n=0;n<6;n++){const c=path.join(e,"package.json");if(fs.existsSync(c))try{const w=JSON.parse(fs.readFileSync(c,"utf8"));if(w.name==="@lensmcp/cluster"&&typeof w.version=="string")return w.version}catch{}const s=path.dirname(e);if(s===e)break;e=s}}u(readClusterVersion,"readClusterVersion"),d(readClusterVersion,"readClusterVersion");function readJsonBody(t){return new Promise((e,n)=>{let c="";t.on("data",s=>{c+=s,c.length>1e6&&n(new Error("body too large"))}),t.on("end",()=>{try{e(c?JSON.parse(c):{})}catch(s){n(s)}}),t.on("error",n)})}u(readJsonBody,"readJsonBody"),d(readJsonBody,"readJsonBody");function ensureOwnerOnlyDir(t){try{fs.mkdirSync(t,{recursive:!0,mode:448})}catch{}try{const e=fs.lstatSync(t);return e.isDirectory()?((e.mode&63)!==0&&fs.chmodSync(t,448),!0):!1}catch{return!1}}u(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),d(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(t){const{rt:e}=t,n=controlSocketPath(),c=path.dirname(n);ensureOwnerOnlyDir(c)||console.warn(`[gateway] control home ${c} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(n)}catch{}const s=d((i,a,l)=>{i.writeHead(a,{"content-type":"application/json"}),i.end(JSON.stringify(l))},"send"),w=http.createServer((i,a)=>{(async()=>{try{const l=(i.url??"").split("?")[0];if(i.method==="GET"&&l==="/list")return s(a,200,{workspaces:e.registry.keys().map(r=>{const o=e.registry.get(r);return{key:r,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0}})});if(i.method==="GET"&&l==="/status"){const r=(0,exports.clusterVersion)();return s(a,200,{daemon:{pid:process.pid,wsKey:e.wsKey,...r?{version:r}:{}},workspaces:e.registry.keys().map(o=>{const y=e.registry.get(o);return{key:o,root:y?.root,routes:y?.routes.length??0,services:y?.services.length??0,isDaemon:o===e.wsKey}}),services:e.registry.allServices().map(o=>({project:o.project,wsKey:o.wsKey,host:o.decl.host,state:o.state}))})}if(i.method==="POST"&&l==="/register"){const r=await readJsonBody(i);if(!r.wsKey||!r.root)return s(a,400,{error:"wsKey and root are required"});const o=r.projects??{},y=(0,discovery_1.projectSourceRoots)(r.root,o,r.projectDependencies),{routes:g,services:p}=(0,discovery_1.discoverRoutes)(r.root,o,void 0,r.wsKey,y),f={wsKey:r.wsKey,root:r.root,routes:g,services:p};try{const v=(0,service_keys_1.adoptServiceKeys)(e,r.root,p,r.wsKey);v.added.length>0&&console.log(`[gateway] adopted east-west keys for ${r.wsKey}: ${v.added.join(", ")}`)}catch(v){console.error(`[gateway] could not adopt east-west keys for ${r.wsKey}: ${v.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(r.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${r.wsKey}') is served by the daemon at ${e.root} (pid ${process.pid}); its gateway-level log is ${path.join(e.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
2
- `)}catch{}return e.registry.register(f),await t.onRegister?.(f),e.rebuildRoutes(),s(a,200,{ok:!0,wsKey:r.wsKey,routes:g.length,services:p.length,workspaces:e.registry.keys()})}if(i.method==="POST"&&l==="/unregister"){const r=await readJsonBody(i);if(!r.wsKey)return s(a,400,{error:"wsKey is required"});const o=e.registry.unregister(r.wsKey);if(o){for(const y of o.services){const g=(0,service_keys_1.serviceKeyId)(y.wsKey??r.wsKey,y.project),p=e.serviceKeys[g];p&&(delete e.serviceKeys[g],e.keyToProject.delete(p),e.keyToWorkspace?.delete(p))}for(const y of o.services)t.killService(y,"workspace unregistered");t.onUnregister?.(o)}return e.rebuildRoutes(),s(a,200,{ok:!0,removed:!!o,workspaces:e.registry.keys()})}s(a,404,{error:"not found"})}catch(l){s(a,500,{error:l.message})}})()});return w.listen(n,()=>{try{fs.chmodSync(n,384)}catch(i){console.warn(`[gateway] could not restrict ${n} to 0600:`,i.message)}}),{socketPath:n,close:d(()=>{w.close();try{fs.unlinkSync(n)}catch{}},"close")}}u(startControlServer,"startControlServer"),d(startControlServer,"startControlServer");
1
+ "use strict";var $=Object.defineProperty;var y=(t,e)=>$(t,"name",{value:e,configurable:!0});var K=Object.defineProperty,d=y((t,e)=>K(t,"name",{value:e,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.clusterVersion=void 0,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"),registration_guards_1=require("./registration-guards"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log");function controlSocketPath(){const t=process.env.LENSMCP_HOME||os.homedir();return path.join(t,".lensmcp","control.sock")}y(controlSocketPath,"controlSocketPath"),d(controlSocketPath,"controlSocketPath");const CONTROL_BODY_BYTES=1e6;class BodyTooLarge extends Error{static{y(this,"BodyTooLarge")}static{d(this,"BodyTooLarge")}constructor(e){super(`body too large (over ${e} bytes)`),this.limit=e}}exports.clusterVersion=versionAtLoad(()=>readClusterVersion(__dirname));function versionAtLoad(t){const e=t();return()=>e}y(versionAtLoad,"versionAtLoad"),d(versionAtLoad,"versionAtLoad");function readClusterVersion(t){let e=t;for(let c=0;c<6;c++){const p=path.join(e,"package.json");if(fs.existsSync(p))try{const l=JSON.parse(fs.readFileSync(p,"utf8"));if(l.name==="@lensmcp/cluster"&&typeof l.version=="string")return l.version}catch{}const s=path.dirname(e);if(s===e)break;e=s}}y(readClusterVersion,"readClusterVersion"),d(readClusterVersion,"readClusterVersion");function readJsonBody(t,e=CONTROL_BODY_BYTES){return new Promise((c,p)=>{const s=[];let l=0,i=!1;t.on("data",o=>{if(!i){if(l+=o.length,l>e){i=!0,s.length=0,p(new BodyTooLarge(e));return}s.push(o)}}),t.on("end",()=>{if(i)return;const o=Buffer.concat(s).toString("utf8");try{c({value:o?JSON.parse(o):{},bytes:l})}catch(u){p(u)}}),t.on("error",p)})}y(readJsonBody,"readJsonBody"),d(readJsonBody,"readJsonBody");function graphShare(t,e){return e-Buffer.byteLength(JSON.stringify({...t,projectGraph:null}))}y(graphShare,"graphShare"),d(graphShare,"graphShare");function graphVerdict(t){return t?t.refused===void 0?{forwarded:!0,accepted:!0}:{forwarded:!0,accepted:!1,reason:t.refused}:{forwarded:!1,accepted:!1}}y(graphVerdict,"graphVerdict"),d(graphVerdict,"graphVerdict");function ensureOwnerOnlyDir(t){try{fs.mkdirSync(t,{recursive:!0,mode:448})}catch{}try{const e=fs.lstatSync(t);return e.isDirectory()?((e.mode&63)!==0&&fs.chmodSync(t,448),!0):!1}catch{return!1}}y(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),d(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(t){const{rt:e}=t,c=controlSocketPath(),p=path.dirname(c);ensureOwnerOnlyDir(p)||console.warn(`[gateway] control home ${p} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(c)}catch{}const s=d((i,o,u)=>{i.writeHead(o,{"content-type":"application/json"}),i.end(JSON.stringify(u))},"send"),l=http.createServer((i,o)=>{(async()=>{try{const u=(i.url??"").split("?")[0];if(i.method==="GET"&&u==="/list")return s(o,200,{workspaces:e.registry.keys().map(a=>{const n=e.registry.get(a);return{key:a,root:n?.root,routes:n?.routes.length??0,services:n?.services.length??0}})});if(i.method==="GET"&&u==="/status"){const a=(0,exports.clusterVersion)();return s(o,200,{daemon:{pid:process.pid,wsKey:e.wsKey,...a?{version:a}:{}},workspaces:e.registry.keys().map(n=>{const r=e.registry.get(n);return{key:n,root:r?.root,routes:r?.routes.length??0,services:r?.services.length??0,isDaemon:n===e.wsKey}}),services:e.registry.allServices().map(n=>({project:n.project,wsKey:n.wsKey,host:n.decl.host,state:n.state}))})}if(i.method==="POST"&&u==="/register"){const{value:a,bytes:n}=await readJsonBody(i,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),r=a;if(!r.wsKey||!r.root)return s(o,400,{error:"wsKey and root are required"});const g=r.projects??{},v=(0,discovery_1.projectSourceRoots)(r.root,g,r.projectDependencies),{routes:h,services:m}=(0,discovery_1.discoverRoutes)(r.root,g,void 0,r.wsKey,v),S={wsKey:r.wsKey,root:r.root,routes:h,services:m},k=(0,registration_guards_1.refuseRegistration)(e.registry,S);if(k)return console.warn(`[gateway] refused to register '${r.wsKey}' from ${r.root}: ${k.error}`),s(o,409,k);try{const w=(0,service_keys_1.adoptServiceKeys)(e,r.root,m,r.wsKey);w.added.length>0&&console.log(`[gateway] adopted east-west keys for ${r.wsKey}: ${w.added.join(", ")}`)}catch(w){console.error(`[gateway] could not adopt east-west keys for ${r.wsKey}: ${w.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(r.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${r.wsKey}') is served by the daemon at ${e.root} (pid ${process.pid}); its gateway-level log is ${path.join(e.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
2
+ `)}catch{}let f;if(e.graphStores){const w={root:r.root,daemonRoot:e.root,otherRoots:e.registry.keys().filter(_=>_!==r.wsKey).flatMap(_=>e.registry.get(_)?.root??[])};f=r.projectGraph===void 0?void 0:(0,guest_graph_1.admitForwardedGraph)(r.projectGraph,{...w,bytes:graphShare(r,n)}),f?.refused!==void 0&&console.warn(`[gateway] refused the project graph '${r.wsKey}' forwarded from ${r.root}: ${f.refused} \u2014 its services start through \`nx run\` until the gateway computes it`),(0,registration_guards_1.sameRoot)(r.root,e.root)||e.graphStores.set((0,guest_graph_1.createGuestGraphStore)({wsKey:r.wsKey,root:r.root,...f?{admission:f}:{},context:w}))}return e.registry.register(S),await t.onRegister?.(S),e.rebuildRoutes(),s(o,200,{ok:!0,wsKey:r.wsKey,routes:h.length,services:m.length,workspaces:e.registry.keys(),...e.graphStores?{projectGraph:graphVerdict(f)}:{}})}if(i.method==="POST"&&u==="/unregister"){const a=(await readJsonBody(i)).value;if(!a.wsKey)return s(o,400,{error:"wsKey is required"});const n=(0,registration_guards_1.refuseUnregistration)(e.registry,{wsKey:a.wsKey,...a.root?{root:a.root}:{}},e.wsKey);if(n)return console.warn(`[gateway] refused to unregister '${a.wsKey}'${a.root?` for ${a.root}`:""}: ${n.error}`),s(o,409,n);const r=e.registry.unregister(a.wsKey);if(r){for(const g of r.services){const v=(0,service_keys_1.serviceKeyId)(g.wsKey??a.wsKey,g.project),h=e.serviceKeys[v];h&&(delete e.serviceKeys[v],e.keyToProject.delete(h),e.keyToWorkspace?.delete(h))}for(const g of r.services)t.killService(g,"workspace unregistered");e.graphStores?.delete(r.root),t.onUnregister?.(r)}return e.rebuildRoutes(),s(o,200,{ok:!0,removed:!!r,workspaces:e.registry.keys()})}s(o,404,{error:"not found"})}catch(u){if(u instanceof BodyTooLarge)return i.resume(),s(o,413,{error:u.message});s(o,500,{error:u.message})}})()});return l.listen(c,()=>{try{fs.chmodSync(c,384)}catch(i){console.warn(`[gateway] could not restrict ${c} to 0600:`,i.message)}}),{socketPath:c,close:d(()=>{l.close();try{fs.unlinkSync(c)}catch{}},"close")}}y(startControlServer,"startControlServer"),d(startControlServer,"startControlServer");
@@ -14,6 +14,6 @@ export declare function stampIdentity(req: http.IncomingMessage, claims: Record<
14
14
  /**
15
15
  * Build the ABAC attribute bag from the verified claims + request envelope. `ip` is the address the edge
16
16
  * RESOLVED (the socket peer, or a trusted proxy's assertion — `../client-ip`), and `path` the DECODED
17
- * canonical path, so a rule judges the same values routing and forwarding used (issues/dev-gateway/11).
17
+ * canonical path, so a rule judges the same values routing and forwarding used (issues/closed/dev-gateway-11).
18
18
  */
19
19
  export declare const devAttrs: (req: http.IncomingMessage, claims: Record<string, unknown>, cpath?: string) => Attrs;
@@ -21,7 +21,7 @@ export declare const clientIpOf: (req: http.IncomingMessage) => string;
21
21
  * Stamp the `X-Forwarded-*` the UPSTREAM should see (idempotent per request): `X-Forwarded-For` is the
22
22
  * trusted chain + peer, or JUST the peer when no front proxy is trusted — a client can no longer hand the
23
23
  * service an arbitrary chain (h1 forwarded it verbatim; h2 appended to it). `X-Forwarded-Proto` is the
24
- * edge's own scheme, `X-Forwarded-Host` the routed host. Prod's #11 twin (issues/dev-gateway/11).
24
+ * edge's own scheme, `X-Forwarded-Host` the routed host. Prod's #11 twin (issues/closed/dev-gateway-11).
25
25
  */
26
26
  export declare const stampForwardingHeaders: (rt: Pick<GatewayRuntime, "clientIp" | "https">, req: http.IncomingMessage) => void;
27
27
  /**
@@ -72,7 +72,7 @@ export declare const callerContextOf: (req: http.IncomingMessage) => CallerConte
72
72
  * The allowlist needs no configuration, because the gateway already knows the answer: the case the
73
73
  * CORS helper exists for is `app.<domain>` calling `api.<domain>`, and both are in its own route
74
74
  * table. Anything else — including a look-alike like `foodguard.local.evil.example` — is a site the
75
- * gateway has no relationship with (issues/codex-security/55).
75
+ * gateway has no relationship with (issues/closed/codex-security-55).
76
76
  */
77
77
  export declare const isRoutedOrigin: (origin: string, routes: readonly {
78
78
  host?: string;
@@ -83,7 +83,7 @@ export declare const isRoutedOrigin: (origin: string, routes: readonly {
83
83
  * This used to reflect any Origin with `credentials: true`. Before it existed a browser could still
84
84
  * send these requests; the same-origin policy stopped an unrelated site READING the answers, and
85
85
  * reflecting the origin removed that — so a page the developer visited could read `/health`,
86
- * `/.well-known/services` and the informative 404s (issues/codex-security/55). The prod gateway
86
+ * `/.well-known/services` and the informative 404s (issues/closed/codex-security-55). The prod gateway
87
87
  * already restricts credentials to an allowlist (`prod-runtime/cors.ts`); this is the dev twin.
88
88
  *
89
89
  * An untrusted origin gets NO `access-control-allow-origin`, so the browser refuses the read. `Vary`