@lensmcp/cluster 1.24.1 → 1.24.3

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 (55) hide show
  1. package/README.md +13 -1
  2. package/checkout-cache.d.ts +5 -0
  3. package/checkout-cache.js +1 -0
  4. package/create-webpack-dev.d.ts +1 -1
  5. package/create-webpack-dev.js +1 -1
  6. package/devserver-notifier.d.ts +1 -1
  7. package/devserver-notifier.js +1 -1
  8. package/devserver-transport.d.ts +41 -0
  9. package/devserver-transport.js +1 -0
  10. package/executors/gateway/manifest.js +1 -1
  11. package/executors/gateway/runtime/auth.d.ts +7 -0
  12. package/executors/gateway/runtime/auth.js +1 -1
  13. package/executors/gateway/runtime/chooser.d.ts +2 -1
  14. package/executors/gateway/runtime/chooser.js +23 -10
  15. package/executors/gateway/runtime/control.d.ts +14 -0
  16. package/executors/gateway/runtime/control.js +2 -2
  17. package/executors/gateway/runtime/discovery.d.ts +15 -2
  18. package/executors/gateway/runtime/discovery.js +1 -1
  19. package/executors/gateway/runtime/edge.d.ts +7 -0
  20. package/executors/gateway/runtime/edge.js +1 -1
  21. package/executors/gateway/runtime/guest-replay.d.ts +22 -0
  22. package/executors/gateway/runtime/guest-replay.js +2 -0
  23. package/executors/gateway/runtime/instance.d.ts +104 -0
  24. package/executors/gateway/runtime/instance.js +2 -0
  25. package/executors/gateway/runtime/lens-children.js +1 -1
  26. package/executors/gateway/runtime/lifecycle.d.ts +2 -0
  27. package/executors/gateway/runtime/lifecycle.js +3 -3
  28. package/executors/gateway/runtime/pod-probe.d.ts +17 -0
  29. package/executors/gateway/runtime/pod-probe.js +1 -1
  30. package/executors/gateway/runtime/proxy.d.ts +14 -0
  31. package/executors/gateway/runtime/proxy.js +1 -1
  32. package/executors/gateway/runtime/registration-guards.js +1 -1
  33. package/executors/gateway/runtime/route-registry.d.ts +7 -0
  34. package/executors/gateway/runtime/scope.d.ts +18 -0
  35. package/executors/gateway/runtime/scope.js +1 -1
  36. package/executors/gateway/runtime/server.js +2 -2
  37. package/executors/gateway/runtime/service-keys.d.ts +12 -3
  38. package/executors/gateway/runtime/service-keys.js +1 -1
  39. package/executors/gateway/runtime/shared-build.d.ts +1 -1
  40. package/executors/gateway/runtime/shared-build.js +3 -3
  41. package/executors/gateway/runtime/types.d.ts +47 -0
  42. package/executors/gateway/runtime/types.js +1 -1
  43. package/executors/gateway/runtime/upgrade.d.ts +13 -8
  44. package/executors/gateway/runtime/upgrade.js +1 -1
  45. package/executors/gateway/runtime/workspace-middleware.d.ts +38 -0
  46. package/executors/gateway/runtime/workspace-middleware.js +1 -0
  47. package/executors/gateway/runtime/workspace-registry.d.ts +7 -0
  48. package/executors/serve/serve.impl.js +1 -1
  49. package/executors/trust/trust.impl.js +3 -3
  50. package/inspector-secret.d.ts +6 -0
  51. package/inspector-secret.js +1 -1
  52. package/main.devserver.d.ts +4 -38
  53. package/main.devserver.js +10 -10
  54. package/package.json +4 -4
  55. package/shared-build/host.js +2 -2
package/README.md CHANGED
@@ -76,6 +76,7 @@ A second backend **path-mounted** under a shared API domain:
76
76
  | `prependPrefix` | Prepend to forwarded URLs — serve a prefix-mounted service at the root of its own dev domain (prod parity). |
77
77
  | `default` | This project is the catch-all for unmatched hostnames. |
78
78
  | `internalHost` | Override the auto `internal.<host>` name, or `false` to disable the internal route. |
79
+ | `callerIds` | Identities this service may name as its `x-api-key-id` on an `internal.<host>` call besides its project name — for one service that acts as several (e.g. `["console", "platform"]`). Kept only when the call carries this service's own validated key; any other name it claims becomes its project name, and a name that is another service's project in the workspace is never honoured. |
79
80
  | `auth` | Edge auth policy: `{ default, overrides }` (JWT verify + identity injection + per-path ABAC enforcement). Absent ⇒ unguarded at the edge. Typically generated from controller decorators rather than hand-written. |
80
81
  | `eager` | `true` = start at gateway boot; `false` (default) = start on first request (scale from zero). |
81
82
  | `minPods` / `maxPods` | Pod range: spawn `minPods`, autoscale up to `maxPods` while requests overlap. |
@@ -117,9 +118,20 @@ One front door, two transports sharing one routing brain:
117
118
  - **Routing** — every `cluster` declaration becomes a route; requests match by hostname (exact beats `*.suffix` wildcard) then by most-specific path prefix. A *claimed* host's unmatched root is served by the gateway itself (`/health` aggregate, `/.well-known/...`, CORS + preflights); an *unclaimed* host can only reach the `default` route or 404.
118
119
  - **Dev transport** — `node:http` + `http-proxy`. It owns the unix-socket pods: **scale-from-zero** on first request, autoscale while requests overlap, idle-kill, supervised reforks, and ghost-socket sweeping.
119
120
  - **Prod transport** — the same `matchRoute` composed over **Fastify + @fastify/reply-from (undici)** (connection-reusing, HTTP/1.1 pipelined; benchmarks materially lower CPU/req than http-proxy), shipped as a **Node 26 distroless** image. No process management — prod pods are always-on (k8s keeps them up).
120
- - **Edge auth + identity** — for `jwt` routes the gateway verifies the end-user token (dev: HS256 shared secret; prod: RS256 via JWKS) and stamps trusted identity headers (`x-user-id`, `x-tenant-id`, roles, perms) onto the forwarded request; `internal.<host>` routes require the CALLER's own `x-api-key`, and once it is validated the `x-tenant-id` and `x-user-id` the caller named are forwarded to the callee — production's service-to-service path is direct, so the callee reads them there too; roles, permissions and every other owned header stay the gateway's. Per-path **ABAC** rules (a `permission` or full predicate over verified claims + IP/headers, scoped by HTTP method) are enforced at the edge → 403.
121
+ - **Edge auth + identity** — for `jwt` routes the gateway verifies the end-user token (dev: HS256 shared secret; prod: RS256 via JWKS) and stamps trusted identity headers (`x-user-id`, `x-tenant-id`, roles, perms) onto the forwarded request; `internal.<host>` routes require the CALLER's own `x-api-key`, stamp `x-api-key-id` with the caller's project (or an identity it declares in `callerIds` and named itself), and once the key is validated the `x-tenant-id` and `x-user-id` the caller named are forwarded to the callee — production's service-to-service path is direct, so the callee reads them there too; roles, permissions and every other owned header stay the gateway's. Per-path **ABAC** rules (a `permission` or full predicate over verified claims + IP/headers, scoped by HTTP method) are enforced at the edge → 403.
121
122
  - **Rollout** — per-request upstream selection supports **weighted** cohorts (smooth nginx-style split for canary / blue-green) and **attribute-based targeting** (route by user / tenant / IP / header / sticky device key, consistent-hashed so a ramp moves a key across the boundary at most once). The serving version rides on `x-lensmcp-version` and the trace.
122
123
 
124
+ ## Worktree instances
125
+
126
+ A second checkout of a workspace — a git worktree an agent works in — can be served by the same gateway beside the main one: `lensmcp instance start` in it writes `instance: { name, baseKey }` into its git-ignored `.lensmcp/config.json` and registers it. The gateway reads that block from the root (`runtime/instance.ts`) and moves every declared host under the name — `app.foodguard.localhost` → `app.gate1.foodguard.localhost`, `internal.*` and the catch-all's apex included — so certificates, CORS and the edge's sign-in verifier follow. Its pods live in a short socket folder (`i-` + 6 hex), bind no TCP port (`LENSMCP_SOCKET_ONLY=1`), and get `LENSMCP_INSTANCE`, `LENSMCP_APEX`, `LENSMCP_HOST_<PROJECT>`, `LENSMCP_INTERNAL_HOST_<PROJECT>`, `LENSMCP_HOSTS_JSON` plus `.lensmcp/instance.resources.env` (its own database and queue URLs, written by `lensmcp instance start` from `nx.json` → `lensmcp.instances`) and the user's `.lensmcp/instance.env` over it; its Vite gets the same. Its east-west keys are its own, so a call between an instance and main is refused as cross-workspace. A `--frozen` instance (`instance.frozen`) compiles each service once with no watcher and no type checker (`LENSMCP_FREEZE=1`), its Vite watches nothing, and the lens sweeper never refreshes its apps. `/list` and `/status` name it; `GET /instance-env?wsKey=<key>` on the control socket returns its variables.
127
+
128
+ ## A daemon shared by several workspaces
129
+
130
+ - **Each workspace's routes run its own edge middleware** — the `middleware` of the `@lensmcp/cluster:gateway` target in its own tree, loaded from its own root when it registers; one that declares none runs none. The owner's routes run the owner's, as before. `LENSMCP_GUEST_MIDDLEWARE=owner` restores the old behaviour (the owner's middleware on every route). A middleware is called `(req, ctx)`: `ctx` is `{ wsKey, root, env }`, where `env` is a worktree instance's variables (empty otherwise) — a middleware runs inside the daemon, so its `process.env` is the owner's, and an instance's addresses (its `AUTH_API_RESOURCE`, …) reach it this way. A one-argument middleware ignores it.
131
+ - **A restarted daemon hosts its guests again.** Every registration is recorded in `~/.lensmcp/guests.json` (owner-only) and replayed on boot, one workspace at a time, through the same guards; an unregister forgets it, a folder that is gone is dropped. Only the daemon of the workspace it registered with replays it. `LENSMCP_GUEST_REPLAY=0` turns it off.
132
+ - **Cold starts queue daemon-wide** (`LENSMCP_MAX_COLD_STARTS`, default half the cores): a page that touches a dozen lazy services, or two dozen worktrees waking at once, no longer starts every `serve-hmr` tree and cold compile in the same second.
133
+ - **Worktree instances show under their project** — nested under it on the `lensmcp.local/` chooser, and in a Worktrees panel on its Cluster page (running ones link to their own dashboard); an instance's own dashboard says whose worktree it is.
134
+
123
135
  ## How it fits
124
136
 
125
137
  `@lensmcp/cluster` is one library in the LensMCP workspace:
@@ -0,0 +1,5 @@
1
+ /** True when `<root>/node_modules` — and its `.cache`, when there is one — resolves inside `root` itself. A
2
+ * checkout without `node_modules` counts as owning it: whatever is created there will be its own. */
3
+ export declare function ownsNodeModules(root: string): boolean;
4
+ /** The checkout's own directory for the lens state named `name` (see the header). */
5
+ export declare function checkoutCacheDir(root: string, name: string): string;
@@ -0,0 +1 @@
1
+ "use strict";var a=Object.defineProperty;var t=(e,r)=>a(e,"name",{value:r,configurable:!0});var u=Object.defineProperty,o=t((e,r)=>u(e,"name",{value:r,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.ownsNodeModules=ownsNodeModules,exports.checkoutCacheDir=checkoutCacheDir;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));function ownsNodeModules(e){let r;try{r=fs.realpathSync(e)}catch{return!0}const n=path.join(r,"node_modules");for(const[s,i]of[[path.join(e,"node_modules"),n],[path.join(e,"node_modules",".cache"),path.join(n,".cache")]]){let c;try{c=fs.realpathSync(s)}catch{continue}if(c!==i)return!1}return!0}t(ownsNodeModules,"ownsNodeModules"),o(ownsNodeModules,"ownsNodeModules");function checkoutCacheDir(e,r){return ownsNodeModules(e)?path.join(e,"node_modules",".cache",r):path.join(e,".lensmcp","cache",r)}t(checkoutCacheDir,"checkoutCacheDir"),o(checkoutCacheDir,"checkoutCacheDir");
@@ -54,7 +54,7 @@ export interface DevFilesystemCache {
54
54
  }
55
55
  /**
56
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
57
+ * `node_modules/.cache/lensmcp-webpack/<project>` (the checkout's OWN — checkout-cache.ts), so every restart, standby generation and rolling
58
58
  * recycle of that service starts warm instead of compiling everything again.
59
59
  *
60
60
  * Its `version` covers what changes how a module is built or resolved without being a file webpack can
@@ -1 +1 @@
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");
1
+ "use strict";var T=Object.defineProperty;var p=(r,e)=>T(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"),checkout_cache_1=require("./checkout-cache"),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:(0,checkout_cache_1.checkoutCacheDir)(o,"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),C=process.env.LENSMCP_TYPECHECK,S=C==="tsgo"?(0,tsgo_check_plugin_1.resolveTsgoBin)(s,process.env.LENSMCP_TSGO_BIN):null;if(C==="tsgo"&&!S){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=!!S,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");
@@ -43,7 +43,7 @@ export declare class DevserverNotifier {
43
43
  built(reload?: boolean): Promise<void>;
44
44
  }
45
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`). */
46
+ * `SOCK_DIR` (`$TMPDIR[/<LENSMCP_SOCK_NS or LENSMCP_WS_KEY>]/<service>-devserver/parent.sock`). */
47
47
  export declare function devserverControlSock(serviceName: string, env?: NodeJS.ProcessEnv): string;
48
48
  /** Wire shape of the testimony (mirrors `PodWatcherTestimony` in the gateway runtime; kept structural so the
49
49
  * webpack side does not import the gateway's types). */
@@ -1 +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
+ "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_SOCK_NS||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");
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Who may ask a devserver parent what (main.devserver.ts `requestHandler`): pure checks on a request's transport.
3
+ *
4
+ * Here rather than in main.devserver.ts, which STARTS a devserver the moment it is loaded: a spec that imported these
5
+ * from it booted a parent and its pods inside the jest process, and they kept the run from ever exiting
6
+ * (issues/closed/test-infra-08).
7
+ */
8
+ /**
9
+ * Did this request arrive on the OWNER-ONLY control socket, rather than the public TCP port?
10
+ *
11
+ * `requestHandler` serves both: the Unix socket at `$TMPDIR/<ws>/<svc>-devserver/parent.sock`
12
+ * (0700 dir, see issues/closed/codex-security-23) and `server.listen(publicPort)`. Admin branches were
13
+ * reachable on both, so `POST /webpack/debug` — which opens a Node inspector in every pod and
14
+ * returns the ws:// URLs, and whose UUID is the only thing guarding `Runtime.evaluate` — could be
15
+ * called by anything that could reach the port (issues/closed/codex-security-05).
16
+ *
17
+ * Node's `Server.address()` returns the PATH STRING for a pipe/Unix socket and an object
18
+ * `{ address, family, port }` for TCP. That is the discriminator, and it needs no extra plumbing
19
+ * through the handler.
20
+ */
21
+ export declare function isOwnerOnlyTransport(req: {
22
+ socket?: unknown;
23
+ }): boolean;
24
+ /** A devserver the gateway started carries its pid; one run by hand (`nx run <svc>:serve-hmr`) does not. */
25
+ export declare function gatewayManaged(env?: NodeJS.ProcessEnv): boolean;
26
+ /**
27
+ * May this request DRIVE THE POOL — `/webpack/reload`, `/webpack/watching`, `/webpack/scale`?
28
+ *
29
+ * Under a gateway, only the control socket may. The TCP port is not an identity: it comes from a
30
+ * tracked config file, so every checkout of a repo has the same number. A second checkout's FIRST
31
+ * build has no control socket yet, so its plugin fell back to `localhost:<port>` — and that port
32
+ * belonged to the first checkout, whose pods it hot-swapped (issues/closed/foodguard-35). Nothing checked
33
+ * who was asking. The gateway and the service's own build plugin both reach the control socket,
34
+ * which lives in the workspace's owner-only socket dir; nobody else has a reason to.
35
+ *
36
+ * A devserver run by hand has no gateway and no second checkout behind one door; its port stays
37
+ * its admin surface, as before.
38
+ */
39
+ export declare function mayDriveThePool(req: {
40
+ socket?: unknown;
41
+ }, managed?: boolean): boolean;
@@ -0,0 +1 @@
1
+ "use strict";var o=Object.defineProperty;var a=(e,r)=>o(e,"name",{value:r,configurable:!0});var n=Object.defineProperty,t=a((e,r)=>n(e,"name",{value:r,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isOwnerOnlyTransport=isOwnerOnlyTransport,exports.gatewayManaged=gatewayManaged,exports.mayDriveThePool=mayDriveThePool;function isOwnerOnlyTransport(e){try{return typeof e.socket?.server?.address?.()=="string"}catch{return!1}}a(isOwnerOnlyTransport,"isOwnerOnlyTransport"),t(isOwnerOnlyTransport,"isOwnerOnlyTransport");function gatewayManaged(e=process.env){return Number(e.LENSMCP_GATEWAY_PID??0)>0}a(gatewayManaged,"gatewayManaged"),t(gatewayManaged,"gatewayManaged");function mayDriveThePool(e,r=gatewayManaged()){return!r||isOwnerOnlyTransport(e)}a(mayDriveThePool,"mayDriveThePool"),t(mayDriveThePool,"mayDriveThePool");
@@ -1 +1 @@
1
- "use strict";var h=Object.defineProperty;var i=(t,r)=>h(t,"name",{value:r,configurable:!0});var f=Object.defineProperty,o=i((t,r)=>f(t,"name",{value:r,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.smoothWeightedSelector=smoothWeightedSelector,exports.hostMatches=hostMatches,exports.matchRoute=matchRoute,exports.pathUnderPrefix=pathUnderPrefix,exports.sortBySpecificity=sortBySpecificity,exports.authForPath=authForPath,exports.authPathMatches=authPathMatches,exports.authRuleFor=authRuleFor,exports.authRuleForPath=authRuleForPath,exports.buildRouteTable=buildRouteTable,exports.manifestFromClusterDecl=manifestFromClusterDecl,exports.matchCidr=matchCidr,exports.evalRule=evalRule,exports.pickTargetedVersion=pickTargetedVersion,exports.fnv1a=fnv1a,exports.bucketByWeight=bucketByWeight;function smoothWeightedSelector(t){const r=t.map(()=>0),e=t.reduce((s,n)=>s+Math.max(0,n),0);return()=>{if(e<=0||t.length===0)return 0;let s=0;for(let n=0;n<t.length;n++)r[n]+=Math.max(0,t[n]),r[n]>r[s]&&(s=n);return r[s]-=e,s}}i(smoothWeightedSelector,"smoothWeightedSelector"),o(smoothWeightedSelector,"smoothWeightedSelector");function hostMatches(t,r){const e=t.toLowerCase(),s=r.toLowerCase();return e.startsWith("*.")?s.endsWith(e.slice(1))||s===e.slice(2):s===e}i(hostMatches,"hostMatches"),o(hostMatches,"hostMatches");function matchRoute(t,r,e){const s=t.some(a=>a.host&&hostMatches(a.host,r));let n;if(s)n=t.filter(a=>a.host&&hostMatches(a.host,r)),n.some(a=>a.host===r)&&(n=n.filter(a=>a.host===r));else{const a=t.filter(u=>!u.host);if(a.length<=1)n=a;else{const u=a.filter(l=>l.apex&&(r===l.apex||r.endsWith("."+l.apex))).sort((l,c)=>c.apex.length-l.apex.length);n=u.length>0?u:a.filter(l=>!l.apex)}}return n.find(a=>!a.prefix||pathUnderPrefix(e,a.prefix))}i(matchRoute,"matchRoute"),o(matchRoute,"matchRoute");function pathUnderPrefix(t,r){let e=r.length;for(;e>0&&r.charCodeAt(e-1)===47;)e-=1;if(e===0)return!0;if(t.length<e||t.slice(0,e).toLowerCase()!==r.slice(0,e).toLowerCase())return!1;const s=t.charCodeAt(e);return Number.isNaN(s)||s===47||s===63}i(pathUnderPrefix,"pathUnderPrefix"),o(pathUnderPrefix,"pathUnderPrefix");function sortBySpecificity(t){const r=o(e=>(e.host?2:0)+(e.prefix?1:0),"rank");return t.sort((e,s)=>r(s)-r(e)||(s.prefix?.length??0)-(e.prefix?.length??0)),t}i(sortBySpecificity,"sortBySpecificity"),o(sortBySpecificity,"sortBySpecificity");function authForPath(t,r,e){if(e)return"internal";const s=t.auth?.overrides??[];for(const n of s)if(r.startsWith(n.path))return n.mode;return t.auth?.default??"jwt"}i(authForPath,"authForPath"),o(authForPath,"authForPath");function authPathMatches(t,r){const e=t.split("/").filter(Boolean),s=(r.split("?")[0]??"").split("/").filter(Boolean);if(s.length<e.length)return!1;for(let n=0;n<e.length;n++){const a=e[n];if(!(a==="*"||a.startsWith(":"))&&a.toLowerCase()!==s[n].toLowerCase())return!1}return!0}i(authPathMatches,"authPathMatches"),o(authPathMatches,"authPathMatches");function authRuleFor(t,r,e){const s=e.toUpperCase();for(const n of t?.overrides??[]){if(!authPathMatches(n.path,r)||n.methods&&!n.methods.some(u=>u.toUpperCase()===s))continue;const a=n.rule??(n.permission?{attr:"perms",op:"hastoken",value:n.permission}:void 0);return{mode:n.mode??t?.default??"jwt",...a?{rule:a}:{}}}return{mode:t?.default??"jwt"}}i(authRuleFor,"authRuleFor"),o(authRuleFor,"authRuleFor");function authRuleForPath(t,r,e,s){return s?{mode:"internal"}:authRuleFor(t.auth,r,e)}i(authRuleForPath,"authRuleForPath"),o(authRuleForPath,"authRuleForPath");function buildRouteTable(t){const r=[];for(const e of t){const s=e.auth?.default??"jwt";for(const n of e.mounts){const a={service:e.service,...n.path?{prefix:n.path}:{},...e.prependPrefix?{prependPrefix:e.prependPrefix}:{},...e.upstream?{upstream:e.upstream}:{}};r.push({...a,host:n.host,auth:s}),e.internal&&e.internalHost!==!1&&r.push({...a,host:typeof e.internalHost=="string"?e.internalHost:`internal.${n.host}`,internal:!0,auth:"internal"})}e.default&&r.push({service:e.service,...e.prependPrefix?{prependPrefix:e.prependPrefix}:{},...e.upstream?{upstream:e.upstream}:{},default:!0,auth:s})}return sortBySpecificity(r)}i(buildRouteTable,"buildRouteTable"),o(buildRouteTable,"buildRouteTable");function manifestFromClusterDecl(t,r){const e=r.upstream??(r.port?`http://localhost:${r.port}`:void 0);if(!(!r.service&&!e))return{service:r.service??t,mounts:r.host?[{host:r.host,...r.path?{path:r.path}:{}}]:[],internal:!!r.service,...r.internalHost!==void 0?{internalHost:r.internalHost}:{},...r.default?{default:!0}:{},...r.prependPrefix?{prependPrefix:r.prependPrefix}:{},...r.auth?{auth:r.auth}:{},...e?{upstream:e}:{}}}i(manifestFromClusterDecl,"manifestFromClusterDecl"),o(manifestFromClusterDecl,"manifestFromClusterDecl");const isGroup=o(t=>"all"in t||"any"in t||"not"in t,"isGroup"),reCache=new Map,compileRe=o(t=>{if(t.length>512)return null;if(reCache.has(t))return reCache.get(t);let r;try{r=new RegExp(t)}catch{r=null}return reCache.set(t,r),r},"compileRe");function ipToInt(t){const r=(t.startsWith("::ffff:")?t.slice(7):t).split(".");if(r.length!==4)return null;let e=0;for(const s of r){const n=Number(s);if(!Number.isInteger(n)||n<0||n>255||s.length>1&&s[0]==="0")return null;e=e*256+n}return e>>>0}i(ipToInt,"ipToInt"),o(ipToInt,"ipToInt");function matchCidr(t,r){const e=r.indexOf("/");if(e<0)return!1;const s=Number(r.slice(e+1));if(!Number.isInteger(s)||s<0||s>32)return!1;const n=ipToInt(t),a=ipToInt(r.slice(0,e));if(n===null||a===null)return!1;if(s===0)return!0;const u=(s===32?4294967295:~((1<<32-s)-1))>>>0;return(n&u)>>>0===(a&u)>>>0}i(matchCidr,"matchCidr"),o(matchCidr,"matchCidr");const str=o(t=>t==null?"":String(t),"str"),eq=o((t,r)=>str(t)===str(r),"eq");function evalCondition(t,r){const e=r[t.attr],s=e!=null;switch(t.op){case"exists":return s;case"ne":return!s||!eq(e,t.value)}if(!s)return!1;switch(t.op){case"eq":return eq(e,t.value);case"in":return Array.isArray(t.value)&&t.value.some(n=>Array.isArray(e)?e.some(a=>eq(a,n)):eq(e,n));case"nin":return Array.isArray(t.value)&&!t.value.some(n=>Array.isArray(e)?e.some(a=>eq(a,n)):eq(e,n));case"prefix":return str(e).startsWith(str(t.value));case"suffix":return str(e).endsWith(str(t.value));case"domain":{const n=str(e).lastIndexOf("@"),a=(n===-1?str(e):str(e).slice(n+1)).toLowerCase().replace(/\.$/,""),u=str(t.value).toLowerCase().replace(/^\./,"").replace(/\.$/,"");return!!u&&(a===u||a.endsWith("."+u))}case"contains":return Array.isArray(e)?e.some(n=>eq(n,t.value)):str(e).includes(str(t.value));case"hastoken":{const n=str(t.value);return Array.isArray(e)?e.some(a=>eq(a,t.value)):str(e).split(/[\s,]+/).filter(Boolean).includes(n)}case"regex":{const n=compileRe(str(t.value));return n?n.test(str(e)):!1}case"gt":return Number(e)>Number(t.value);case"lt":return Number(e)<Number(t.value);case"cidr":return matchCidr(str(e),str(t.value));default:return!1}}i(evalCondition,"evalCondition"),o(evalCondition,"evalCondition");function evalRule(t,r){if(!isGroup(t))return evalCondition(t,r);const e=Array.isArray(t.all)&&t.all.length?t.all:void 0,s=Array.isArray(t.any)&&t.any.length?t.any:void 0,n=t.not&&Object.keys(t.not).length?t.not:void 0;return n&&evalRule(n,r)||e&&!e.every(a=>evalRule(a,r))||s&&!s.some(a=>evalRule(a,r))?!1:!!(e||s||n)}i(evalRule,"evalRule"),o(evalRule,"evalRule");function pickTargetedVersion(t,r){for(const e of t)if(evalRule(e.when,r))return{version:e.version,...e.name?{rule:e.name}:{}}}i(pickTargetedVersion,"pickTargetedVersion"),o(pickTargetedVersion,"pickTargetedVersion");function fnv1a(t){let r=2166136261;for(let e=0;e<t.length;e++)r^=t.charCodeAt(e),r=Math.imul(r,16777619);return r>>>0}i(fnv1a,"fnv1a"),o(fnv1a,"fnv1a");function bucketByWeight(t,r){const e=r.reduce((a,u)=>a+Math.max(0,u),0);if(e<=0||r.length===0)return 0;const s=fnv1a(t)%1e4;let n=0;for(let a=0;a<r.length;a++)if(n+=Math.max(0,r[a])/e*1e4,s<n)return a;return r.length-1}i(bucketByWeight,"bucketByWeight"),o(bucketByWeight,"bucketByWeight");
1
+ "use strict";var h=Object.defineProperty;var i=(t,r)=>h(t,"name",{value:r,configurable:!0});var f=Object.defineProperty,o=i((t,r)=>f(t,"name",{value:r,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.smoothWeightedSelector=smoothWeightedSelector,exports.hostMatches=hostMatches,exports.matchRoute=matchRoute,exports.pathUnderPrefix=pathUnderPrefix,exports.sortBySpecificity=sortBySpecificity,exports.authForPath=authForPath,exports.authPathMatches=authPathMatches,exports.authRuleFor=authRuleFor,exports.authRuleForPath=authRuleForPath,exports.buildRouteTable=buildRouteTable,exports.manifestFromClusterDecl=manifestFromClusterDecl,exports.matchCidr=matchCidr,exports.evalRule=evalRule,exports.pickTargetedVersion=pickTargetedVersion,exports.fnv1a=fnv1a,exports.bucketByWeight=bucketByWeight;function smoothWeightedSelector(t){const r=t.map(()=>0),e=t.reduce((s,n)=>s+Math.max(0,n),0);return()=>{if(e<=0||t.length===0)return 0;let s=0;for(let n=0;n<t.length;n++)r[n]+=Math.max(0,t[n]),r[n]>r[s]&&(s=n);return r[s]-=e,s}}i(smoothWeightedSelector,"smoothWeightedSelector"),o(smoothWeightedSelector,"smoothWeightedSelector");function hostMatches(t,r){const e=t.toLowerCase(),s=r.toLowerCase();return e.startsWith("*.")?s.endsWith(e.slice(1))||s===e.slice(2):s===e}i(hostMatches,"hostMatches"),o(hostMatches,"hostMatches");function matchRoute(t,r,e){const s=t.some(a=>a.host&&hostMatches(a.host,r));let n;if(s)n=t.filter(a=>a.host&&hostMatches(a.host,r)),n.some(a=>a.host===r)?n=n.filter(a=>a.host===r):n.length>1&&(n=[...n].sort((a,u)=>u.host.length-a.host.length));else{const a=t.filter(u=>!u.host);if(a.length<=1)n=a;else{const u=a.filter(l=>l.apex&&(r===l.apex||r.endsWith("."+l.apex))).sort((l,c)=>c.apex.length-l.apex.length);n=u.length>0?u:a.filter(l=>!l.apex)}}return n.find(a=>!a.prefix||pathUnderPrefix(e,a.prefix))}i(matchRoute,"matchRoute"),o(matchRoute,"matchRoute");function pathUnderPrefix(t,r){let e=r.length;for(;e>0&&r.charCodeAt(e-1)===47;)e-=1;if(e===0)return!0;if(t.length<e||t.slice(0,e).toLowerCase()!==r.slice(0,e).toLowerCase())return!1;const s=t.charCodeAt(e);return Number.isNaN(s)||s===47||s===63}i(pathUnderPrefix,"pathUnderPrefix"),o(pathUnderPrefix,"pathUnderPrefix");function sortBySpecificity(t){const r=o(e=>(e.host?2:0)+(e.prefix?1:0),"rank");return t.sort((e,s)=>r(s)-r(e)||(s.prefix?.length??0)-(e.prefix?.length??0)),t}i(sortBySpecificity,"sortBySpecificity"),o(sortBySpecificity,"sortBySpecificity");function authForPath(t,r,e){if(e)return"internal";const s=t.auth?.overrides??[];for(const n of s)if(r.startsWith(n.path))return n.mode;return t.auth?.default??"jwt"}i(authForPath,"authForPath"),o(authForPath,"authForPath");function authPathMatches(t,r){const e=t.split("/").filter(Boolean),s=(r.split("?")[0]??"").split("/").filter(Boolean);if(s.length<e.length)return!1;for(let n=0;n<e.length;n++){const a=e[n];if(!(a==="*"||a.startsWith(":"))&&a.toLowerCase()!==s[n].toLowerCase())return!1}return!0}i(authPathMatches,"authPathMatches"),o(authPathMatches,"authPathMatches");function authRuleFor(t,r,e){const s=e.toUpperCase();for(const n of t?.overrides??[]){if(!authPathMatches(n.path,r)||n.methods&&!n.methods.some(u=>u.toUpperCase()===s))continue;const a=n.rule??(n.permission?{attr:"perms",op:"hastoken",value:n.permission}:void 0);return{mode:n.mode??t?.default??"jwt",...a?{rule:a}:{}}}return{mode:t?.default??"jwt"}}i(authRuleFor,"authRuleFor"),o(authRuleFor,"authRuleFor");function authRuleForPath(t,r,e,s){return s?{mode:"internal"}:authRuleFor(t.auth,r,e)}i(authRuleForPath,"authRuleForPath"),o(authRuleForPath,"authRuleForPath");function buildRouteTable(t){const r=[];for(const e of t){const s=e.auth?.default??"jwt";for(const n of e.mounts){const a={service:e.service,...n.path?{prefix:n.path}:{},...e.prependPrefix?{prependPrefix:e.prependPrefix}:{},...e.upstream?{upstream:e.upstream}:{}};r.push({...a,host:n.host,auth:s}),e.internal&&e.internalHost!==!1&&r.push({...a,host:typeof e.internalHost=="string"?e.internalHost:`internal.${n.host}`,internal:!0,auth:"internal"})}e.default&&r.push({service:e.service,...e.prependPrefix?{prependPrefix:e.prependPrefix}:{},...e.upstream?{upstream:e.upstream}:{},default:!0,auth:s})}return sortBySpecificity(r)}i(buildRouteTable,"buildRouteTable"),o(buildRouteTable,"buildRouteTable");function manifestFromClusterDecl(t,r){const e=r.upstream??(r.port?`http://localhost:${r.port}`:void 0);if(!(!r.service&&!e))return{service:r.service??t,mounts:r.host?[{host:r.host,...r.path?{path:r.path}:{}}]:[],internal:!!r.service,...r.internalHost!==void 0?{internalHost:r.internalHost}:{},...r.default?{default:!0}:{},...r.prependPrefix?{prependPrefix:r.prependPrefix}:{},...r.auth?{auth:r.auth}:{},...e?{upstream:e}:{}}}i(manifestFromClusterDecl,"manifestFromClusterDecl"),o(manifestFromClusterDecl,"manifestFromClusterDecl");const isGroup=o(t=>"all"in t||"any"in t||"not"in t,"isGroup"),reCache=new Map,compileRe=o(t=>{if(t.length>512)return null;if(reCache.has(t))return reCache.get(t);let r;try{r=new RegExp(t)}catch{r=null}return reCache.set(t,r),r},"compileRe");function ipToInt(t){const r=(t.startsWith("::ffff:")?t.slice(7):t).split(".");if(r.length!==4)return null;let e=0;for(const s of r){const n=Number(s);if(!Number.isInteger(n)||n<0||n>255||s.length>1&&s[0]==="0")return null;e=e*256+n}return e>>>0}i(ipToInt,"ipToInt"),o(ipToInt,"ipToInt");function matchCidr(t,r){const e=r.indexOf("/");if(e<0)return!1;const s=Number(r.slice(e+1));if(!Number.isInteger(s)||s<0||s>32)return!1;const n=ipToInt(t),a=ipToInt(r.slice(0,e));if(n===null||a===null)return!1;if(s===0)return!0;const u=(s===32?4294967295:~((1<<32-s)-1))>>>0;return(n&u)>>>0===(a&u)>>>0}i(matchCidr,"matchCidr"),o(matchCidr,"matchCidr");const str=o(t=>t==null?"":String(t),"str"),eq=o((t,r)=>str(t)===str(r),"eq");function evalCondition(t,r){const e=r[t.attr],s=e!=null;switch(t.op){case"exists":return s;case"ne":return!s||!eq(e,t.value)}if(!s)return!1;switch(t.op){case"eq":return eq(e,t.value);case"in":return Array.isArray(t.value)&&t.value.some(n=>Array.isArray(e)?e.some(a=>eq(a,n)):eq(e,n));case"nin":return Array.isArray(t.value)&&!t.value.some(n=>Array.isArray(e)?e.some(a=>eq(a,n)):eq(e,n));case"prefix":return str(e).startsWith(str(t.value));case"suffix":return str(e).endsWith(str(t.value));case"domain":{const n=str(e).lastIndexOf("@"),a=(n===-1?str(e):str(e).slice(n+1)).toLowerCase().replace(/\.$/,""),u=str(t.value).toLowerCase().replace(/^\./,"").replace(/\.$/,"");return!!u&&(a===u||a.endsWith("."+u))}case"contains":return Array.isArray(e)?e.some(n=>eq(n,t.value)):str(e).includes(str(t.value));case"hastoken":{const n=str(t.value);return Array.isArray(e)?e.some(a=>eq(a,t.value)):str(e).split(/[\s,]+/).filter(Boolean).includes(n)}case"regex":{const n=compileRe(str(t.value));return n?n.test(str(e)):!1}case"gt":return Number(e)>Number(t.value);case"lt":return Number(e)<Number(t.value);case"cidr":return matchCidr(str(e),str(t.value));default:return!1}}i(evalCondition,"evalCondition"),o(evalCondition,"evalCondition");function evalRule(t,r){if(!isGroup(t))return evalCondition(t,r);const e=Array.isArray(t.all)&&t.all.length?t.all:void 0,s=Array.isArray(t.any)&&t.any.length?t.any:void 0,n=t.not&&Object.keys(t.not).length?t.not:void 0;return n&&evalRule(n,r)||e&&!e.every(a=>evalRule(a,r))||s&&!s.some(a=>evalRule(a,r))?!1:!!(e||s||n)}i(evalRule,"evalRule"),o(evalRule,"evalRule");function pickTargetedVersion(t,r){for(const e of t)if(evalRule(e.when,r))return{version:e.version,...e.name?{rule:e.name}:{}}}i(pickTargetedVersion,"pickTargetedVersion"),o(pickTargetedVersion,"pickTargetedVersion");function fnv1a(t){let r=2166136261;for(let e=0;e<t.length;e++)r^=t.charCodeAt(e),r=Math.imul(r,16777619);return r>>>0}i(fnv1a,"fnv1a"),o(fnv1a,"fnv1a");function bucketByWeight(t,r){const e=r.reduce((a,u)=>a+Math.max(0,u),0);if(e<=0||r.length===0)return 0;const s=fnv1a(t)%1e4;let n=0;for(let a=0;a<r.length;a++)if(n+=Math.max(0,r[a])/e*1e4,s<n)return a;return r.length-1}i(bucketByWeight,"bucketByWeight"),o(bucketByWeight,"bucketByWeight");
@@ -10,6 +10,13 @@ export interface EdgeAuth {
10
10
  */
11
11
  public(req: import('node:http').IncomingMessage, route: Route, trace: GwTrace | undefined, cpath?: string): AuthVerdict;
12
12
  }
13
+ /**
14
+ * The identities a calling project may name on an internal call besides its own name: its `cluster.callerIds`
15
+ * (issues/closed/foodguard-50), MINUS every name that is another service's project in its workspace — a service
16
+ * declaring `iam` must not become iam. `ws` is the caller's workspace (from its validated key); without one (a
17
+ * single-workspace gateway) every service is its peer.
18
+ */
19
+ export declare function declaredCallerIds(rt: Pick<GatewayRuntime, 'services' | 'keyToProject'> & Partial<Pick<GatewayRuntime, 'registry' | 'keyToWorkspace'>>, caller: string, ws: string | undefined): ReadonlySet<string>;
13
20
  /**
14
21
  * HOST-AWARE verifier selection — a SHARED multi-workspace daemon fronts several
15
22
  * IdPs (each workspace's `auth.<apex>`), so the verifier for a request is chosen
@@ -1 +1 @@
1
- "use strict";var y=Object.defineProperty;var f=(n,u)=>y(n,"name",{value:u,configurable:!0});var h=Object.defineProperty,d=f((n,u)=>h(n,"name",{value:u,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),dev_auth_1=require("./dev-auth"),edge_1=require("./edge");function createEdgeAuth(n,u,m){const{traceStep:o}=u;return{internal:d((e,k,r)=>{const c=String(e.headers["x-api-key"]??""),a=n.keyToProject.get(c);if(!a)return o(r,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};const t=n.keyToWorkspace?.get(c),l=k.svc?.wsKey;if(t&&l&&t!==l)return o(r,"auth",{mode:"x-api-key",ok:!1,reason:"cross-workspace",caller:a,callerWorkspace:t,routeWorkspace:l}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};e.headers["x-api-key-id"]=a,t?e.headers["x-api-key-workspace"]=t:delete e.headers["x-api-key-workspace"],e.__lensmcpCaller=a,delete e.headers["x-api-key"];const i=(0,edge_1.callerContextOf)(e);for(const[p,s]of Object.entries(i))e.headers[p]=s;return o(r,"auth",{mode:"x-api-key",ok:!0,caller:a,...t?{callerWorkspace:t}:{},...i["x-tenant-id"]!==void 0?{callerTenant:!0}:{},...i["x-user-id"]!==void 0?{callerUser:!0}:{}}),{ok:!0,caller:a}},"internal"),public:d((e,k,r,c)=>{const a=k.auth;if(a){const t=c??(0,edge_1.canonicalPath)(e.url??"/")??(e.url??"/").split("?")[0],l=(e.method??"GET").toUpperCase(),i=(0,manifest_1.authRuleFor)(a,t,l);if(i.mode==="jwt"){const p=String(e.headers.host??"").split(":")[0].toLowerCase(),s=(0,dev_auth_1.verifyDevJwt)(e,m?.for(p));return s?i.rule&&!(0,manifest_1.evalRule)(i.rule,(0,dev_auth_1.devAttrs)(e,s,t))?(o(r,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):((0,dev_auth_1.stampIdentity)(e,s),o(r,"auth",{mode:"jwt",ok:!0}),{ok:!0,claims:s}):(o(r,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"})}return o(r,"auth",{mode:i.mode}),{ok:!0}}return o(r,"auth",{mode:"public",unconfigured:!0}),{ok:!0}},"publicAuth")}}f(createEdgeAuth,"createEdgeAuth"),d(createEdgeAuth,"createEdgeAuth");
1
+ "use strict";var v=Object.defineProperty;var f=(a,n)=>v(a,"name",{value:n,configurable:!0});var x=Object.defineProperty,p=f((a,n)=>x(a,"name",{value:n,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.declaredCallerIds=declaredCallerIds,exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),dev_auth_1=require("./dev-auth"),edge_1=require("./edge"),NO_CALLER_IDS=new Set;function declaredCallerIds(a,n,d){const s=(a.registry?a.registry.allServices():a.services).filter(r=>d===void 0||r.wsKey===d),e=s.find(r=>r.project===n)?.decl.callerIds;if(!Array.isArray(e)||e.length===0)return NO_CALLER_IDS;const c=new Set(s.map(r=>r.project));for(const[r,u]of a.keyToProject)(d===void 0||a.keyToWorkspace?.get(r)===d)&&c.add(u);return new Set(e.filter(r=>typeof r=="string"&&r!==n&&!c.has(r)))}f(declaredCallerIds,"declaredCallerIds"),p(declaredCallerIds,"declaredCallerIds");function createEdgeAuth(a,n,d){const{traceStep:s}=n;return{internal:p((e,c,r)=>{const u=String(e.headers["x-api-key"]??""),t=a.keyToProject.get(u);if(!t)return s(r,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};const i=a.keyToWorkspace?.get(u),h=c.svc?.wsKey;if(i&&h&&i!==h)return s(r,"auth",{mode:"x-api-key",ok:!1,reason:"cross-workspace",caller:t,callerWorkspace:i,routeWorkspace:h}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};const o=(0,edge_1.claimedCallerIdOf)(e),k=o!==void 0&&o!==t&&declaredCallerIds(a,t,i).has(o)?o:t;e.headers["x-api-key-id"]=k,i?e.headers["x-api-key-workspace"]=i:delete e.headers["x-api-key-workspace"],e.__lensmcpCaller=t,delete e.headers["x-api-key"];const l=(0,edge_1.callerContextOf)(e);for(const[y,m]of Object.entries(l))e.headers[y]=m;return s(r,"auth",{mode:"x-api-key",ok:!0,caller:t,...k!==t?{as:k}:{},...o!==void 0&&o!==t&&k===t?{claimRefused:o}:{},...i?{callerWorkspace:i}:{},...l["x-tenant-id"]!==void 0?{callerTenant:!0}:{},...l["x-user-id"]!==void 0?{callerUser:!0}:{}}),{ok:!0,caller:t}},"internal"),public:p((e,c,r,u)=>{const t=c.auth;if(t){const i=u??(0,edge_1.canonicalPath)(e.url??"/")??(e.url??"/").split("?")[0],h=(e.method??"GET").toUpperCase(),o=(0,manifest_1.authRuleFor)(t,i,h);if(o.mode==="jwt"){const k=String(e.headers.host??"").split(":")[0].toLowerCase(),l=(0,dev_auth_1.verifyDevJwt)(e,d?.for(k));return l?o.rule&&!(0,manifest_1.evalRule)(o.rule,(0,dev_auth_1.devAttrs)(e,l,i))?(s(r,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):((0,dev_auth_1.stampIdentity)(e,l),s(r,"auth",{mode:"jwt",ok:!0}),{ok:!0,claims:l}):(s(r,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"})}return s(r,"auth",{mode:o.mode}),{ok:!0}}return s(r,"auth",{mode:"public",unconfigured:!0}),{ok:!0}},"publicAuth")}}f(createEdgeAuth,"createEdgeAuth"),p(createEdgeAuth,"createEdgeAuth");
@@ -20,7 +20,8 @@ export declare function isLoopbackRequest(req: Pick<http.IncomingMessage, 'socke
20
20
  * Home-relative, last two segments: `/Users/dana/work/acme/web` → `~/…/acme/web`.
21
21
  */
22
22
  export declare function displayRoot(root: string, home?: string): string;
23
- /** Render the chooser HTML from the registry entries (+ which key is running in THIS gateway). */
23
+ /** Render the chooser HTML from the registry entries (+ which key is running in THIS gateway). A worktree instance
24
+ * (`entry.instance`) is listed UNDER its project, not beside it. */
24
25
  export declare function renderChooserPage(entries: WorkspaceEntry[], currentKey: string): string;
25
26
  /**
26
27
  * Serve the chooser at the dashboard-host root. Reads the registry fresh so live status is current.
@@ -1,12 +1,16 @@
1
- "use strict";var l=Object.defineProperty;var i=(o,e)=>l(o,"name",{value:e,configurable:!0});var p=Object.defineProperty,n=i((o,e)=>p(o,"name",{value:e,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isLoopbackRequest=isLoopbackRequest,exports.displayRoot=displayRoot,exports.renderChooserPage=renderChooserPage,exports.serveChooser=serveChooser;const node_os_1=require("node:os"),workspace_registry_1=require("./workspace-registry"),esc=n(o=>o.replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e]),"esc");function isLoopbackRequest(o){const e=o.socket?.remoteAddress;if(!e)return!1;const t=e.startsWith("::ffff:")?e.slice(7):e;return t==="::1"||t==="127.0.0.1"||/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(t)}i(isLoopbackRequest,"isLoopbackRequest"),n(isLoopbackRequest,"isLoopbackRequest");function displayRoot(o,e=(0,node_os_1.homedir)()){const t=o.replace(/\/+$/,"").split("/").filter(Boolean),r=t.slice(-2).join("/");if(!r)return"/";const a=e!==""&&(o===e||o.startsWith(`${e}/`)),s=t.length>2;return a?s?`~/\u2026/${r}`:`~/${r}`:s?`\u2026/${r}`:`/${r}`}i(displayRoot,"displayRoot"),n(displayRoot,"displayRoot");function renderChooserPage(o,e){const t=o.map(a=>({...a,live:a.key===e||(0,workspace_registry_1.isWorkspaceLive)(a)})).sort((a,s)=>Number(s.live)-Number(a.live)||a.key.localeCompare(s.key)),r=t.length===0?'<p class="empty">No workspaces registered yet. Start a gateway in a workspace (<code>lensmcp gateway start</code>) and it will appear here.</p>':t.map(a=>{const s=a.key===e?'<span class="badge here">running here</span>':a.live?'<span class="badge live">live</span>':'<span class="badge off">offline</span>',d=a.live?'<span class="open">Open dashboard \u2192</span>':'<span class="hint">start its gateway to open</span>',c=`<div class="card-main">
2
- <div class="key">${esc(a.key)} ${s}</div>
3
- <div class="root">${esc(displayRoot(a.root))}</div>
1
+ "use strict";var m=Object.defineProperty;var l=(a,t)=>m(a,"name",{value:t,configurable:!0});var b=Object.defineProperty,i=l((a,t)=>b(a,"name",{value:t,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isLoopbackRequest=isLoopbackRequest,exports.displayRoot=displayRoot,exports.renderChooserPage=renderChooserPage,exports.serveChooser=serveChooser;const node_os_1=require("node:os"),workspace_registry_1=require("./workspace-registry"),esc=i(a=>a.replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc");function isLoopbackRequest(a){const t=a.socket?.remoteAddress;if(!t)return!1;const o=t.startsWith("::ffff:")?t.slice(7):t;return o==="::1"||o==="127.0.0.1"||/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(o)}l(isLoopbackRequest,"isLoopbackRequest"),i(isLoopbackRequest,"isLoopbackRequest");function displayRoot(a,t=(0,node_os_1.homedir)()){const o=a.replace(/\/+$/,"").split("/").filter(Boolean),c=o.slice(-2).join("/");if(!c)return"/";const p=t!==""&&(a===t||a.startsWith(`${t}/`)),s=o.length>2;return p?s?`~/\u2026/${c}`:`~/${c}`:s?`\u2026/${c}`:`/${c}`}l(displayRoot,"displayRoot"),i(displayRoot,"displayRoot");function renderChooserPage(a,t){const o=a.map(e=>({...e,live:e.key===t||(0,workspace_registry_1.isWorkspaceLive)(e)})).sort((e,n)=>Number(n.live)-Number(e.live)||e.key.localeCompare(n.key)),c=new Set(o.map(e=>e.key)),p=i(e=>!!e.instance&&c.has(e.instance.baseKey),"nested"),s=o.filter(e=>!p(e)),x=i(e=>o.filter(n=>n.instance?.baseKey===e),"worktreesOf"),f=i(e=>e.key===t?'<span class="badge here">running here</span>':e.live?'<span class="badge live">live</span>':'<span class="badge off">offline</span>',"badgeOf"),h=i(e=>{const n=x(e);if(n.length===0)return"";const d=n.map(r=>{const g=`<span class="wt-name">${esc(r.instance?.name??r.key)}</span> ${f(r)}<span class="wt-apex">${esc(r.instance?.apex??"")}</span>`;return r.live?`<a class="wt is-live" href="${esc(r.base)}/">${g}<span class="open">Open \u2192</span></a>`:`<div class="wt is-off">${g}</div>`}).join(`
2
+ `);return`
3
+ <div class="worktrees"><div class="wt-head">Worktrees \xB7 ${n.filter(r=>r.live).length} running</div>
4
+ ${d}
5
+ </div>`},"worktreeList"),u=s.length===0?'<p class="empty">No workspaces registered yet. Start a gateway in a workspace (<code>lensmcp gateway start</code>) and it will appear here.</p>':s.map(e=>{const n=e.live?'<span class="open">Open dashboard \u2192</span>':'<span class="hint">start its gateway to open</span>',d=e.instance?`<div class="root">worktree <b>${esc(e.instance.name)}</b> of ${esc(e.instance.baseKey)} \xB7 ${esc(e.instance.apex)}</div>`:"",r=`<div class="card-main">
6
+ <div class="key">${esc(e.key)} ${f(e)}</div>
7
+ <div class="root">${esc(displayRoot(e.root))}</div>${d}
4
8
  </div>
5
- <div class="card-action">${d}</div>`;return a.live?`<a class="card is-live" href="${esc(a.base)}/">
6
- ${c}
9
+ <div class="card-action">${n}</div>`;return`<div class="group">${e.live?`<a class="card is-live" href="${esc(e.base)}/">
10
+ ${r}
7
11
  </a>`:`<div class="card is-off">
8
- ${c}
9
- </div>`}).join(`
12
+ ${r}
13
+ </div>`}${h(e.key)}</div>`}).join(`
10
14
  `);return`<!doctype html>
11
15
  <html lang="en"><head>
12
16
  <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
@@ -36,16 +40,25 @@
36
40
  .open { color: #8aa8ff; font-weight: 600; font-size: 13.5px; text-decoration: none; }
37
41
  .hint { color: #6b7280; font-size: 12.5px; }
38
42
  .empty { color: #8b93a3; }
43
+ .group { display: flex; flex-direction: column; gap: 6px; }
44
+ .worktrees { margin-left: 22px; padding-left: 14px; border-left: 2px solid #232a3a; display: flex; flex-direction: column; gap: 6px; }
45
+ .wt-head { color: #79808f; font-size: 11.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; margin: 2px 0; }
46
+ .wt { display: flex; align-items: center; gap: 9px; padding: 9px 12px; border: 1px solid #232a3a; border-radius: 9px; background: #121722;
47
+ text-decoration: none; color: inherit; font-size: 13.5px; }
48
+ .wt.is-live:hover { border-color: #3355ff; }
49
+ .wt.is-off { opacity: .62; }
50
+ .wt-name { font-weight: 650; }
51
+ .wt-apex { color: #79808f; font-size: 12px; flex: 1; }
39
52
  code { background: #1c2333; padding: 1px 6px; border-radius: 5px; font-size: 12.5px; }
40
53
  footer { margin-top: 24px; color: #5b6270; font-size: 12px; }
41
54
  </style></head>
42
55
  <body><div class="wrap">
43
56
  <header>
44
57
  <h1>LensMCP workspaces</h1>
45
- <p class="sub">Pick a workspace to open its dashboard. ${t.filter(a=>a.live).length} live \xB7 ${t.length} known.</p>
58
+ <p class="sub">Pick a workspace to open its dashboard. ${s.filter(e=>e.live).length} live \xB7 ${s.length} known${o.length>s.length?` \xB7 ${o.length-s.length} worktree${o.length-s.length===1?"":"s"}`:""}.</p>
46
59
  </header>
47
60
  <div class="list">
48
- ${r}
61
+ ${u}
49
62
  </div>
50
63
  <footer>Served by the gateway on <code>lensmcp.local</code>. This list updates as gateways start and stop.</footer>
51
- </div></body></html>`}i(renderChooserPage,"renderChooserPage"),n(renderChooserPage,"renderChooserPage");function serveChooser(o,e,t){if(t&&!isLoopbackRequest(t)){o.writeHead(404,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),o.end("not found");return}const r=renderChooserPage((0,workspace_registry_1.readWorkspaces)(),e);o.writeHead(200,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),o.end(r)}i(serveChooser,"serveChooser"),n(serveChooser,"serveChooser");
64
+ </div></body></html>`}l(renderChooserPage,"renderChooserPage"),i(renderChooserPage,"renderChooserPage");function serveChooser(a,t,o){if(o&&!isLoopbackRequest(o)){a.writeHead(404,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),a.end("not found");return}const c=renderChooserPage((0,workspace_registry_1.readWorkspaces)(),t);a.writeHead(200,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),a.end(c)}l(serveChooser,"serveChooser"),i(serveChooser,"serveChooser");
@@ -1,5 +1,13 @@
1
1
  import type { GatewayRuntime } from './types';
2
2
  import type { WorkspaceFragment } from './route-registry';
3
+ /** What `/list` and `/status` say about a workspace that is a worktree instance; nothing for any other. */
4
+ export declare function instanceSummary(f: WorkspaceFragment | undefined): {
5
+ instance?: string;
6
+ baseKey?: string;
7
+ apex?: string;
8
+ baseApex?: string;
9
+ frozen?: boolean;
10
+ };
3
11
  /** `~/.lensmcp/control.sock` — the well-known daemon control socket (override the home via `LENSMCP_HOME`). */
4
12
  export declare function controlSocketPath(): string;
5
13
  export interface ControlServerDeps {
@@ -13,6 +21,12 @@ export interface ControlServerDeps {
13
21
  }
14
22
  export interface ControlServer {
15
23
  socketPath: string;
24
+ /** Host again every workspace registered with this daemon's workspace before it restarted (guest-replay.ts). */
25
+ replay: () => Promise<{
26
+ replayed: string[];
27
+ refused: string[];
28
+ gone: string[];
29
+ }>;
16
30
  close: () => void;
17
31
  }
18
32
  /**
@@ -1,2 +1,2 @@
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");
1
+ "use strict";var O=Object.defineProperty;var d=(s,e)=>O(s,"name",{value:e,configurable:!0});var _=Object.defineProperty,u=d((s,e)=>_(s,"name",{value:e,configurable:!0}),"u");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(s){const e=s?.instance;return e?{instance:e.name,baseKey:e.baseKey,apex:e.apex,baseApex:e.baseApex,...e.frozen?{frozen:!0}:{}}:{}}d(instanceSummary,"instanceSummary"),u(instanceSummary,"instanceSummary");function controlSocketPath(){const s=process.env.LENSMCP_HOME||os.homedir();return path.join(s,".lensmcp","control.sock")}d(controlSocketPath,"controlSocketPath"),u(controlSocketPath,"controlSocketPath");const CONTROL_BODY_BYTES=1e6;class BodyTooLarge extends Error{static{d(this,"BodyTooLarge")}static{u(this,"BodyTooLarge")}constructor(e){super(`body too large (over ${e} bytes)`),this.limit=e}}exports.clusterVersion=versionAtLoad(()=>readClusterVersion(__dirname));function versionAtLoad(s){const e=s();return()=>e}d(versionAtLoad,"versionAtLoad"),u(versionAtLoad,"versionAtLoad");function readClusterVersion(s){let e=s;for(let y=0;y<6;y++){const l=path.join(e,"package.json");if(fs.existsSync(l))try{const p=JSON.parse(fs.readFileSync(l,"utf8"));if(p.name==="@lensmcp/cluster"&&typeof p.version=="string")return p.version}catch{}const i=path.dirname(e);if(i===e)break;e=i}}d(readClusterVersion,"readClusterVersion"),u(readClusterVersion,"readClusterVersion");function readJsonBody(s,e=CONTROL_BODY_BYTES){return new Promise((y,l)=>{const i=[];let p=0,h=!1;s.on("data",r=>{if(!h){if(p+=r.length,p>e){h=!0,i.length=0,l(new BodyTooLarge(e));return}i.push(r)}}),s.on("end",()=>{if(h)return;const r=Buffer.concat(i).toString("utf8");try{y({value:r?JSON.parse(r):{},bytes:p})}catch(n){l(n)}}),s.on("error",l)})}d(readJsonBody,"readJsonBody"),u(readJsonBody,"readJsonBody");function graphShare(s,e){return e-Buffer.byteLength(JSON.stringify({...s,projectGraph:null}))}d(graphShare,"graphShare"),u(graphShare,"graphShare");function graphVerdict(s){return s?s.refused===void 0?{forwarded:!0,accepted:!0}:{forwarded:!0,accepted:!1,reason:s.refused}:{forwarded:!1,accepted:!1}}d(graphVerdict,"graphVerdict"),u(graphVerdict,"graphVerdict");function ensureOwnerOnlyDir(s){try{fs.mkdirSync(s,{recursive:!0,mode:448})}catch{}try{const e=fs.lstatSync(s);return e.isDirectory()?((e.mode&63)!==0&&fs.chmodSync(s,448),!0):!1}catch{return!1}}d(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),u(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(s){const{rt:e}=s,y=controlSocketPath(),l=path.dirname(y);ensureOwnerOnlyDir(l)||console.warn(`[gateway] control home ${l} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(y)}catch{}const i=u((r,n,a)=>{r.writeHead(n,{"content-type":"application/json"}),r.end(JSON.stringify(a))},"send"),p=u(async(r,n)=>{if(!r.wsKey||!r.root)return{status:400,json:{error:"wsKey and root are required"}};const a=r.projects??{},t=(0,discovery_1.projectSourceRoots)(r.root,a,r.projectDependencies),o=(0,scope_1.readLensScope)(r.root),c=o.key===r.wsKey,w=c&&o.sockNs!==r.wsKey?o.sockNs:void 0,{routes:v,services:f,instance:$}=(0,discovery_1.discoverRoutes)(r.root,a,void 0,r.wsKey,t,w,c?o.instance:void 0),j=(0,workspace_middleware_1.findGatewayMiddleware)(r.root,a),K={wsKey:r.wsKey,...w?{sockNs:w}:{},root:r.root,routes:v,services:f,...$?{instance:$}:{},...j?{middleware:j}:{}},S=(0,registration_guards_1.refuseRegistration)(e.registry,K);if(S)return console.warn(`[gateway] refused to register '${r.wsKey}' from ${r.root}: ${S.error}`),{status:409,json:S};try{const g=(0,service_keys_1.adoptServiceKeys)(e,r.root,f,r.wsKey);g.added.length>0&&console.log(`[gateway] adopted east-west keys for ${r.wsKey}: ${g.added.join(", ")}`),g.regenerated.length>0&&console.warn(`[gateway] ${r.root} carried another workspace's east-west keys (a copied key file) \u2014 new keys for ${r.wsKey}: ${g.regenerated.join(", ")}`)}catch(g){console.error(`[gateway] could not adopt east-west keys for ${r.wsKey}: ${g.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 m;if(e.graphStores){const g={root:r.root,daemonRoot:e.root,otherRoots:e.registry.keys().filter(k=>k!==r.wsKey).flatMap(k=>e.registry.get(k)?.root??[])};m=r.projectGraph===void 0?void 0:(0,guest_graph_1.admitForwardedGraph)(r.projectGraph,{...g,bytes:graphShare(r,n)}),m?.refused!==void 0&&console.warn(`[gateway] refused the project graph '${r.wsKey}' forwarded from ${r.root}: ${m.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,...m?{admission:m}:{},context:g}))}return e.registry.register(K),await s.onRegister?.(K),e.rebuildRoutes(),e.root&&!(0,registration_guards_1.sameRoot)(r.root,e.root)&&(0,guest_replay_1.recordGuest)({wsKey:r.wsKey,root:r.root,projects:a,...r.projectDependencies?{projectDependencies:r.projectDependencies}:{},daemonRoot:e.root,registeredAt:new Date().toISOString()}),{status:200,json:{ok:!0,wsKey:r.wsKey,routes:v.length,services:f.length,workspaces:e.registry.keys(),...e.graphStores?{projectGraph:graphVerdict(m)}:{}}}},"register"),h=http.createServer((r,n)=>{(async()=>{try{const a=(r.url??"").split("?")[0];if(r.method==="GET"&&a==="/list")return i(n,200,{workspaces:e.registry.keys().map(t=>{const o=e.registry.get(t);return{key:t,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0,...instanceSummary(o)}})});if(r.method==="GET"&&a==="/status"){const t=(0,exports.clusterVersion)();return i(n,200,{daemon:{pid:process.pid,wsKey:e.wsKey,...t?{version:t}:{}},workspaces:e.registry.keys().map(o=>{const c=e.registry.get(o);return{key:o,root:c?.root,routes:c?.routes.length??0,services:c?.services.length??0,isDaemon:o===e.wsKey,...instanceSummary(c)}}),services:e.registry.allServices().map(o=>({project:o.project,wsKey:o.wsKey,host:o.decl.host,state:o.state}))})}if(r.method==="GET"&&a==="/instance-env"){const t=new URL(r.url??"/","http://control").searchParams.get("wsKey")??"",o=e.registry.get(t);return o?o.instance?i(n,200,{wsKey:t,root:o.root,...instanceSummary(o),env:o.instance.env}):i(n,404,{error:`'${t}' is not an instance`}):i(n,404,{error:`no workspace '${t}' is registered`})}if(r.method==="POST"&&a==="/register"){const{value:t,bytes:o}=await readJsonBody(r,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),c=await p(t,o);return i(n,c.status,c.json)}if(r.method==="POST"&&a==="/unregister"){const t=(await readJsonBody(r)).value;if(!t.wsKey)return i(n,400,{error:"wsKey is required"});const o=(0,registration_guards_1.refuseUnregistration)(e.registry,{wsKey:t.wsKey,...t.root?{root:t.root}:{}},e.wsKey);if(o)return console.warn(`[gateway] refused to unregister '${t.wsKey}'${t.root?` for ${t.root}`:""}: ${o.error}`),i(n,409,o);const c=e.registry.unregister(t.wsKey);if(c){for(const w of c.services){const v=(0,service_keys_1.serviceKeyId)(w.wsKey??t.wsKey,w.project),f=e.serviceKeys[v];f&&(delete e.serviceKeys[v],e.keyToProject.delete(f),e.keyToWorkspace?.delete(f))}for(const w of c.services)s.killService(w,"workspace unregistered");e.graphStores?.delete(c.root),(0,guest_replay_1.forgetGuest)(t.wsKey),s.onUnregister?.(c)}return e.rebuildRoutes(),i(n,200,{ok:!0,removed:!!c,workspaces:e.registry.keys()})}i(n,404,{error:"not found"})}catch(a){if(a instanceof BodyTooLarge)return r.resume(),i(n,413,{error:a.message});i(n,500,{error:a.message})}})()});return h.listen(y,()=>{try{fs.chmodSync(y,384)}catch(r){console.warn(`[gateway] could not restrict ${y} to 0600:`,r.message)}}),{socketPath:y,async replay(){const{replay:r,gone:n}=(0,guest_replay_1.guestsToReplay)(e.root,(0,guest_replay_1.readGuests)()),a={replayed:[],refused:[],gone:n.map(t=>t.wsKey)};for(const t of n)(0,guest_replay_1.forgetGuest)(t.wsKey),console.log(`[gateway] not re-hosting '${t.wsKey}': ${t.root} is no longer a workspace \u2014 forgotten`);for(const t of r)if(!e.registry.has(t.wsKey))try{const o=await p({wsKey:t.wsKey,root:t.root,projects:t.projects,...t.projectDependencies?{projectDependencies:t.projectDependencies}:{}},0);o.status===200?(a.replayed.push(t.wsKey),console.log(`[gateway] re-hosted '${t.wsKey}' (${t.root}) \u2014 registered before this daemon restarted`)):(a.refused.push(t.wsKey),console.warn(`[gateway] could not re-host '${t.wsKey}' (${t.root}): ${JSON.stringify(o.json)} \u2014 \`lensmcp gateway start\` there`))}catch(o){a.refused.push(t.wsKey),console.warn(`[gateway] could not re-host '${t.wsKey}' (${t.root}): ${o.message}`)}return a},close:u(()=>{h.close();try{fs.unlinkSync(y)}catch{}},"close")}}d(startControlServer,"startControlServer"),u(startControlServer,"startControlServer");
@@ -1,4 +1,5 @@
1
- import type { Route, ServiceCtl, SockPool } from './types';
1
+ import { type InstanceDecl, type InstanceRouting } from './instance';
2
+ import type { ClusterProjectDecl, Route, ServiceCtl, SockPool } from './types';
2
3
  /** A pod that is about to stop accepting connections leaves `<socket>.retiring` beside its socket first
3
4
  * (main.devserver.ts `retire`, MUST match its `sockRetiringPath`). */
4
5
  export declare const RETIRING_SUFFIX = ".retiring";
@@ -6,6 +7,17 @@ export declare const RETIRING_SUFFIX = ".retiring";
6
7
  export declare function pickSock(pool: SockPool, scanTtlMs?: number): string | undefined;
7
8
  /** Extract the port from an `upstream` URL like `http://localhost:5173`. */
8
9
  export declare function lensPortFromUpstream(upstream?: string): number | undefined;
10
+ /**
11
+ * The declarations of a worktree INSTANCE, with every host moved under the instance's name (instance.ts
12
+ * `rewriteHost`): `host` and a string `internalHost`. Routes, `internal.*`, the default's apex, the certificate, CORS
13
+ * and the edge's sign-in verifier all read the route table, so they follow. A host outside the apex has no
14
+ * instance name — it is dropped with a warning rather than served as declared, where it is the main checkout's.
15
+ */
16
+ export declare function rewriteInstanceDecls(decls: ReadonlyArray<readonly [string, ClusterProjectDecl]>, instance: InstanceDecl): {
17
+ decls: Array<[string, ClusterProjectDecl]>;
18
+ baseApex?: string;
19
+ apex?: string;
20
+ };
9
21
  /**
10
22
  * Read every project's `cluster` (or legacy `davnx`) declaration and build
11
23
  * the route table: host routes first (plus auto `internal.<host>` for pod
@@ -13,9 +25,10 @@ export declare function lensPortFromUpstream(upstream?: string): number | undefi
13
25
  */
14
26
  export declare function discoverRoutes(rootDir: string, projects: Record<string, {
15
27
  root: string;
16
- }>, sockBaseDir?: string, wsKey?: string, sourceRootsByProject?: Readonly<Record<string, string[]>>): {
28
+ }>, sockBaseDir?: string, wsKey?: string, sourceRootsByProject?: Readonly<Record<string, string[]>>, sockNs?: string, instanceDecl?: InstanceDecl): {
17
29
  routes: Route[];
18
30
  services: ServiceCtl[];
31
+ instance?: InstanceRouting;
19
32
  };
20
33
  /** Resolve each project's own root plus every transitive Nx dependency root. */
21
34
  export declare function projectSourceRoots(workspaceRoot: string, projects: Record<string, {
@@ -1 +1 @@
1
- "use strict";var y=Object.defineProperty;var d=(e,s)=>y(e,"name",{value:s,configurable:!0});var g=Object.defineProperty,p=d((e,s)=>g(e,"name",{value:s,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.RETIRING_SUFFIX=void 0,exports.pickSock=pickSock,exports.lensPortFromUpstream=lensPortFromUpstream,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"),scope_1=require("./scope");exports.RETIRING_SUFFIX=".retiring";function pickSock(e,s=1500){const u=Date.now();if(u-e.scannedAt>s){try{const n=fs.readdirSync(e.dir),a=n.filter(o=>/^child-\d+(?:-g\d+)?\.sock$/.test(o)),l=a.filter(o=>!n.includes(o+exports.RETIRING_SUFFIX));e.socks=(l.length>0?l:a).map(o=>path.join(e.dir,o))}catch{e.socks=[]}e.scannedAt=u}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}d(pickSock,"pickSock"),p(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const s=/:(\d+)/.exec(e);return s?Number(s[1]):void 0}d(lensPortFromUpstream,"lensPortFromUpstream"),p(lensPortFromUpstream,"lensPortFromUpstream");function discoverRoutes(e,s,u=os.tmpdir(),n="",a={}){const l=p(c=>n?path.join(u,n,`${c}-devserver`):path.join(u,`${c}-devserver`),"sockDirFor"),o=[],h=[],r=[];let i;for(const[c,S]of Object.entries(s)){let t;try{const f=JSON.parse(fs.readFileSync(path.join(e,S.root,"project.json"),"utf8"));t=f.cluster??f.davnx}catch{continue}if(!t)continue;let v,k,m,j;if(t.service)t.lens&&console.warn(`[gateway] ${c}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),v={dir:l(t.service),socks:[],idx:-1,scannedAt:0},k={project:c,decl:t,root:e,wsKey:n,pool:v,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},h.push(k);else if(t.lens){const f=t.port??lensPortFromUpstream(t.upstream)??5173;m=`http://localhost:${f}`,j={project:c,...n?{workspaceKey:n}:{},workspaceRoot:e,projectRoot:path.join(e,S.root),port:f,...a[c]?.length?{sourceRoots:a[c]}:{}}}else if(m=t.upstream??(t.port?`http://localhost:${t.port}`:void 0),!m){console.warn(`[gateway] ${c}: no service/upstream/port \u2014 skipped.`);continue}const x={project:c,prependPrefix:t.prependPrefix,target:m,pool:v,svc:k,prefix:t.path,auth:t.auth,lens:j};t.host&&(o.push({...x,host:t.host}),r.push(t.host),v&&t.internalHost!==!1&&o.push({...x,host:typeof t.internalHost=="string"?t.internalHost:`internal.${t.host}`,internal:!0})),t.default&&(i={...x,prefix:void 0})}return i&&(i.apex=(0,scope_1.baseDomainOf)(r),o.push(i)),(0,manifest_1.sortBySpecificity)(o),{routes:o,services:h}}d(discoverRoutes,"discoverRoutes"),p(discoverRoutes,"discoverRoutes");function projectSourceRoots(e,s,u={}){const n={};for(const a of Object.keys(s)){const l=new Set,o=p(r=>{if(!l.has(r)){l.add(r);for(const i of u[r]??[])o(i)}},"visit");o(a);const h=[...l].map(r=>s[r]?.root).filter(r=>typeof r=="string").map(r=>path.resolve(e,r)).filter(r=>{const i=path.relative(e,r);return i===""||i!==".."&&!i.startsWith(`..${path.sep}`)&&!path.isAbsolute(i)});n[a]=[...new Set(h)]}return n}d(projectSourceRoots,"projectSourceRoots"),p(projectSourceRoots,"projectSourceRoots");
1
+ "use strict";var A=Object.defineProperty;var m=(e,n)=>A(e,"name",{value:n,configurable:!0});var F=Object.defineProperty,d=m((e,n)=>F(e,"name",{value:n,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.RETIRING_SUFFIX=void 0,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"),instance_1=require("./instance"),scope_1=require("./scope");exports.RETIRING_SUFFIX=".retiring";function pickSock(e,n=1500){const u=Date.now();if(u-e.scannedAt>n){try{const s=fs.readdirSync(e.dir),p=s.filter(o=>/^child-\d+(?:-g\d+)?\.sock$/.test(o)),r=p.filter(o=>!s.includes(o+exports.RETIRING_SUFFIX));e.socks=(r.length>0?r:p).map(o=>path.join(e.dir,o))}catch{e.socks=[]}e.scannedAt=u}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}m(pickSock,"pickSock"),d(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const n=/:(\d+)/.exec(e);return n?Number(n[1]):void 0}m(lensPortFromUpstream,"lensPortFromUpstream"),d(lensPortFromUpstream,"lensPortFromUpstream");function rewriteInstanceDecls(e,n){const u=e.flatMap(([,r])=>[...r.host?[r.host]:[],...typeof r.internalHost=="string"?[r.internalHost]:[]]),s=n.apex??(0,instance_1.commonApex)(u);if(!s)return console.warn(`[gateway] instance '${n.name}': the declared hosts share no apex \u2014 set \`instance.apex\` in .lensmcp/config.json; no host is served for it.`),{decls:e.map(([r,o])=>[r,{...o,host:void 0,internalHost:!1,default:!1}])};const p=[];for(const[r,o]of e){const l={...o};if(o.host){const i=(0,instance_1.rewriteHost)(o.host,s,n.name);i||console.warn(`[gateway] instance '${n.name}': ${r}'s host ${o.host} is outside ${s} \u2014 not served for the instance.`),l.host=i}if(typeof o.internalHost=="string"){const i=(0,instance_1.rewriteHost)(o.internalHost,s,n.name);i||console.warn(`[gateway] instance '${n.name}': ${r}'s internal host ${o.internalHost} is outside ${s} \u2014 not served for the instance.`),l.internalHost=i??!1}p.push([r,l])}return{decls:p,baseApex:s,apex:`${n.name}.${s}`}}m(rewriteInstanceDecls,"rewriteInstanceDecls"),d(rewriteInstanceDecls,"rewriteInstanceDecls");function discoverRoutes(e,n,u=os.tmpdir(),s="",p={},r=s,o){const l=r||s,i=d(c=>l?path.join(u,l,`${c}-devserver`):path.join(u,`${c}-devserver`),"sockDirFor"),a=[],k=[],_=[],H=[],$={};let w,y=[];const I=new Map;for(const[c,t]of Object.entries(n))try{const f=JSON.parse(fs.readFileSync(path.join(e,t.root,"project.json"),"utf8")),h=f.cluster??f.davnx;h&&(y.push([c,h]),I.set(c,t.root))}catch{continue}let v={};o&&({decls:y,...v}=rewriteInstanceDecls(y,o));for(const[c,t]of y){const f={root:I.get(c)};let h,S,g,b;if(t.service)t.lens&&console.warn(`[gateway] ${c}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),h={dir:i(t.service),socks:[],idx:-1,scannedAt:0},S={project:c,decl:t,root:e,wsKey:s,...l!==s?{sockNs:l}:{},pool:h,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},k.push(S);else if(t.lens){const x=t.port??lensPortFromUpstream(t.upstream)??5173;g=`http://localhost:${x}`,b={project:c,...s?{workspaceKey:s}:{},workspaceRoot:e,projectRoot:path.join(e,f.root),port:x,...p[c]?.length?{sourceRoots:p[c]}:{}},_.push(b)}else if(g=t.upstream??(t.port?`http://localhost:${t.port}`:void 0),!g){console.warn(`[gateway] ${c}: no service/upstream/port \u2014 skipped.`);continue}const R={...s?{wsKey:s}:{},project:c,prependPrefix:t.prependPrefix,target:g,pool:h,svc:S,prefix:t.path,auth:t.auth,lens:b};if(t.host&&(a.push({...R,host:t.host}),H.push(t.host),$[c]={host:t.host},h&&t.internalHost!==!1)){const x=typeof t.internalHost=="string"?t.internalHost:`internal.${t.host}`;a.push({...R,host:x,internal:!0}),$[c]={host:t.host,internalHost:x}}t.default&&(w={...R,prefix:void 0})}w&&(w.apex=v.apex??(0,scope_1.baseDomainOf)(H),a.push(w));let j;if(o&&v.apex&&v.baseApex){const c={name:o.name,apex:v.apex,baseApex:v.baseApex},t=(0,instance_1.instanceEnv)(e,(0,instance_1.instanceBuiltinEnv)(c,$));j={...c,baseKey:o.baseKey,env:t,...o.frozen?{frozen:!0}:{}};for(const f of k)f.instanceEnv=t,o.frozen&&(f.frozen=!0);for(const f of _)f.env=t,o.frozen&&(f.frozen=!0)}return(0,manifest_1.sortBySpecificity)(a),{routes:a,services:k,...j?{instance:j}:{}}}m(discoverRoutes,"discoverRoutes"),d(discoverRoutes,"discoverRoutes");function projectSourceRoots(e,n,u={}){const s={};for(const p of Object.keys(n)){const r=new Set,o=d(i=>{if(!r.has(i)){r.add(i);for(const a of u[i]??[])o(a)}},"visit");o(p);const l=[...r].map(i=>n[i]?.root).filter(i=>typeof i=="string").map(i=>path.resolve(e,i)).filter(i=>{const a=path.relative(e,i);return a===""||a!==".."&&!a.startsWith(`..${path.sep}`)&&!path.isAbsolute(a)});s[p]=[...new Set(l)]}return s}m(projectSourceRoots,"projectSourceRoots"),d(projectSourceRoots,"projectSourceRoots");
@@ -50,6 +50,11 @@ export type CallerContext = Readonly<Partial<Record<CallerContextHeader, string>
50
50
  * auth, once the calling service's own key has been validated ({@link callerContextOf}).
51
51
  */
52
52
  export declare const CALLER_CONTEXT: unique symbol;
53
+ /**
54
+ * Where {@link stripOwnedHeaders} sets aside the `x-api-key-id` a caller NAMED — read back only by the internal-route
55
+ * auth, after the caller's key is validated, and honoured only if that service declares it (`cluster.callerIds`).
56
+ */
57
+ export declare const CLAIMED_CALLER_ID: unique symbol;
53
58
  /**
54
59
  * Strip every gateway-owned header at ingress — the HTTP handler and the WS upgrade both call this —
55
60
  * keeping the caller context ({@link CALLER_CONTEXT_HEADERS}) aside on the request.
@@ -64,6 +69,8 @@ export declare const CALLER_CONTEXT: unique symbol;
64
69
  * token's.
65
70
  */
66
71
  export declare function stripOwnedHeaders(req: http.IncomingMessage): void;
72
+ /** The `x-api-key-id` the caller named at ingress (see {@link CLAIMED_CALLER_ID}) — a claim, never a credential. */
73
+ export declare const claimedCallerIdOf: (req: http.IncomingMessage) => string | undefined;
67
74
  /** The context the caller named — empty when it sent none, or only values not worth handing back. */
68
75
  export declare const callerContextOf: (req: http.IncomingMessage) => CallerContext;
69
76
  /**
@@ -1 +1 @@
1
- "use strict";var d=Object.defineProperty;var c=(e,r)=>d(e,"name",{value:r,configurable:!0});var p=Object.defineProperty,o=c((e,r)=>p(e,"name",{value:r,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.PREFLIGHT_METHODS_ROUTED=exports.PREFLIGHT_METHODS_NOROUTE=exports.corsPreflight=exports.cors=exports.isRoutedOrigin=exports.callerContextOf=exports.CALLER_CONTEXT=exports.CALLER_CONTEXT_HEADERS=exports.GATEWAY_OWNED_HEADERS=exports.sendError=exports.stampForwardingHeaders=exports.clientIpOf=exports.resolveClientIp=exports.CLIENT_IP=exports.rememberRawTarget=exports.rawTargetOf=exports.RAW_TARGET=exports.canonicalPath=exports.canonicalize=exports.ENCODED_STRUCTURE=void 0,exports.stripOwnedHeaders=stripOwnedHeaders;const gateway_errors_1=require("../gateway-errors"),client_ip_1=require("../client-ip"),manifest_1=require("../manifest");var canonical_path_1=require("../canonical-path");Object.defineProperty(exports,"ENCODED_STRUCTURE",{enumerable:!0,get:o(function(){return canonical_path_1.ENCODED_STRUCTURE},"get")}),Object.defineProperty(exports,"canonicalize",{enumerable:!0,get:o(function(){return canonical_path_1.canonicalize},"get")}),Object.defineProperty(exports,"canonicalPath",{enumerable:!0,get:o(function(){return canonical_path_1.canonicalPath},"get")}),Object.defineProperty(exports,"RAW_TARGET",{enumerable:!0,get:o(function(){return canonical_path_1.RAW_TARGET},"get")}),Object.defineProperty(exports,"rawTargetOf",{enumerable:!0,get:o(function(){return canonical_path_1.rawTargetOf},"get")}),Object.defineProperty(exports,"rememberRawTarget",{enumerable:!0,get:o(function(){return canonical_path_1.rememberRawTarget},"get")}),exports.CLIENT_IP=Symbol.for("lensmcp.gateway.clientIp");const FORWARDED=Symbol.for("lensmcp.gateway.forwarded"),peerOnly=(0,client_ip_1.createClientIpResolver)({}),resolverOf=o(e=>e.clientIp??peerOnly,"resolverOf"),peerOf=o(e=>e.socket?.remoteAddress??"","peerOf"),resolveClientIp=o((e,r)=>{const t=resolverOf(e).clientIpOf(r,peerOf(r));return r[exports.CLIENT_IP]=t,t},"resolveClientIp");exports.resolveClientIp=resolveClientIp;const clientIpOf=o(e=>e[exports.CLIENT_IP]??peerOf(e),"clientIpOf");exports.clientIpOf=clientIpOf;const stampForwardingHeaders=o((e,r)=>{const t=r;if(t[FORWARDED])return;t[FORWARDED]=!0;const s=peerOf(r);r.headers["x-forwarded-for"]=resolverOf(e).forwardedFor(r,s),r.headers["x-forwarded-proto"]=e.https?"https":"http";const n=r.headers.host;(resolverOf(e).trustProxyHops===0||!r.headers["x-forwarded-host"])&&(n?r.headers["x-forwarded-host"]=n:delete r.headers["x-forwarded-host"])},"stampForwardingHeaders");exports.stampForwardingHeaders=stampForwardingHeaders;const sendError=o((e,r,t,s,n,i,l)=>{const a=(0,gateway_errors_1.ensureRequestId)(r);return e.headersSent||e.writeHead(t,{"content-type":gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE,"x-request-id":a,...l??{}}),e.end((0,gateway_errors_1.gatewayErrorBody)(t,s,a,n,i)),a},"sendError");exports.sendError=sendError,exports.GATEWAY_OWNED_HEADERS=["x-internal-token","x-api-key-id","x-api-key-workspace","x-user-id","x-tenant-id","x-user-roles","x-user-permissions"],exports.CALLER_CONTEXT_HEADERS=["x-tenant-id","x-user-id"],exports.CALLER_CONTEXT=Symbol.for("lensmcp.gateway.callerContext");const CALLER_CONTEXT_VALUE=/^[\x21-\x2b\x2d-\x7e]{1,128}$/;function stripOwnedHeaders(e){const r={};for(const t of exports.CALLER_CONTEXT_HEADERS){const s=e.headers[t];typeof s=="string"&&CALLER_CONTEXT_VALUE.test(s)&&(r[t]=s)}e[exports.CALLER_CONTEXT]=r;for(const t of exports.GATEWAY_OWNED_HEADERS)delete e.headers[t]}c(stripOwnedHeaders,"stripOwnedHeaders"),o(stripOwnedHeaders,"stripOwnedHeaders");const callerContextOf=o(e=>e[exports.CALLER_CONTEXT]??{},"callerContextOf");exports.callerContextOf=callerContextOf;const isRoutedOrigin=o((e,r)=>{if(!e||e==="null")return!1;let t;try{t=new URL(e).hostname.toLowerCase()}catch{return!1}return t?t==="localhost"||t==="127.0.0.1"||t==="::1"?!0:r.some(s=>!!s.host&&(0,manifest_1.hostMatches)(s.host,t)):!1},"isRoutedOrigin");exports.isRoutedOrigin=isRoutedOrigin;const cors=o((e,r,t)=>{const s=e.headers.origin;typeof s!="string"||s===""||(r.setHeader("vary","Origin"),(0,exports.isRoutedOrigin)(s,t)&&(r.setHeader("access-control-allow-origin",s),r.setHeader("access-control-allow-credentials","true")))},"cors");exports.cors=cors;const corsPreflight=o((e,r,t,s)=>{(0,exports.cors)(e,r,s),r.writeHead(204,{"access-control-allow-methods":t,"access-control-allow-headers":String(e.headers["access-control-request-headers"]??"*")}),r.end()},"corsPreflight");exports.corsPreflight=corsPreflight,exports.PREFLIGHT_METHODS_NOROUTE="GET,HEAD,OPTIONS",exports.PREFLIGHT_METHODS_ROUTED="GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS";
1
+ "use strict";var d=Object.defineProperty;var c=(e,r)=>d(e,"name",{value:r,configurable:!0});var p=Object.defineProperty,o=c((e,r)=>p(e,"name",{value:r,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.PREFLIGHT_METHODS_ROUTED=exports.PREFLIGHT_METHODS_NOROUTE=exports.corsPreflight=exports.cors=exports.isRoutedOrigin=exports.callerContextOf=exports.claimedCallerIdOf=exports.CLAIMED_CALLER_ID=exports.CALLER_CONTEXT=exports.CALLER_CONTEXT_HEADERS=exports.GATEWAY_OWNED_HEADERS=exports.sendError=exports.stampForwardingHeaders=exports.clientIpOf=exports.resolveClientIp=exports.CLIENT_IP=exports.rememberRawTarget=exports.rawTargetOf=exports.RAW_TARGET=exports.canonicalPath=exports.canonicalize=exports.ENCODED_STRUCTURE=void 0,exports.stripOwnedHeaders=stripOwnedHeaders;const gateway_errors_1=require("../gateway-errors"),client_ip_1=require("../client-ip"),manifest_1=require("../manifest");var canonical_path_1=require("../canonical-path");Object.defineProperty(exports,"ENCODED_STRUCTURE",{enumerable:!0,get:o(function(){return canonical_path_1.ENCODED_STRUCTURE},"get")}),Object.defineProperty(exports,"canonicalize",{enumerable:!0,get:o(function(){return canonical_path_1.canonicalize},"get")}),Object.defineProperty(exports,"canonicalPath",{enumerable:!0,get:o(function(){return canonical_path_1.canonicalPath},"get")}),Object.defineProperty(exports,"RAW_TARGET",{enumerable:!0,get:o(function(){return canonical_path_1.RAW_TARGET},"get")}),Object.defineProperty(exports,"rawTargetOf",{enumerable:!0,get:o(function(){return canonical_path_1.rawTargetOf},"get")}),Object.defineProperty(exports,"rememberRawTarget",{enumerable:!0,get:o(function(){return canonical_path_1.rememberRawTarget},"get")}),exports.CLIENT_IP=Symbol.for("lensmcp.gateway.clientIp");const FORWARDED=Symbol.for("lensmcp.gateway.forwarded"),peerOnly=(0,client_ip_1.createClientIpResolver)({}),resolverOf=o(e=>e.clientIp??peerOnly,"resolverOf"),peerOf=o(e=>e.socket?.remoteAddress??"","peerOf"),resolveClientIp=o((e,r)=>{const t=resolverOf(e).clientIpOf(r,peerOf(r));return r[exports.CLIENT_IP]=t,t},"resolveClientIp");exports.resolveClientIp=resolveClientIp;const clientIpOf=o(e=>e[exports.CLIENT_IP]??peerOf(e),"clientIpOf");exports.clientIpOf=clientIpOf;const stampForwardingHeaders=o((e,r)=>{const t=r;if(t[FORWARDED])return;t[FORWARDED]=!0;const s=peerOf(r);r.headers["x-forwarded-for"]=resolverOf(e).forwardedFor(r,s),r.headers["x-forwarded-proto"]=e.https?"https":"http";const n=r.headers.host;(resolverOf(e).trustProxyHops===0||!r.headers["x-forwarded-host"])&&(n?r.headers["x-forwarded-host"]=n:delete r.headers["x-forwarded-host"])},"stampForwardingHeaders");exports.stampForwardingHeaders=stampForwardingHeaders;const sendError=o((e,r,t,s,n,i,l)=>{const a=(0,gateway_errors_1.ensureRequestId)(r);return e.headersSent||e.writeHead(t,{"content-type":gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE,"x-request-id":a,...l??{}}),e.end((0,gateway_errors_1.gatewayErrorBody)(t,s,a,n,i)),a},"sendError");exports.sendError=sendError,exports.GATEWAY_OWNED_HEADERS=["x-internal-token","x-api-key-id","x-api-key-workspace","x-user-id","x-tenant-id","x-user-roles","x-user-permissions"],exports.CALLER_CONTEXT_HEADERS=["x-tenant-id","x-user-id"],exports.CALLER_CONTEXT=Symbol.for("lensmcp.gateway.callerContext"),exports.CLAIMED_CALLER_ID=Symbol.for("lensmcp.gateway.claimedCallerId");const CALLER_ID_VALUE=/^[a-z0-9][a-z0-9._-]{0,63}$/i,CALLER_CONTEXT_VALUE=/^[\x21-\x2b\x2d-\x7e]{1,128}$/;function stripOwnedHeaders(e){const r={};for(const s of exports.CALLER_CONTEXT_HEADERS){const n=e.headers[s];typeof n=="string"&&CALLER_CONTEXT_VALUE.test(n)&&(r[s]=n)}e[exports.CALLER_CONTEXT]=r;const t=e.headers["x-api-key-id"];typeof t=="string"&&CALLER_ID_VALUE.test(t)&&(e[exports.CLAIMED_CALLER_ID]=t);for(const s of exports.GATEWAY_OWNED_HEADERS)delete e.headers[s]}c(stripOwnedHeaders,"stripOwnedHeaders"),o(stripOwnedHeaders,"stripOwnedHeaders");const claimedCallerIdOf=o(e=>e[exports.CLAIMED_CALLER_ID],"claimedCallerIdOf");exports.claimedCallerIdOf=claimedCallerIdOf;const callerContextOf=o(e=>e[exports.CALLER_CONTEXT]??{},"callerContextOf");exports.callerContextOf=callerContextOf;const isRoutedOrigin=o((e,r)=>{if(!e||e==="null")return!1;let t;try{t=new URL(e).hostname.toLowerCase()}catch{return!1}return t?t==="localhost"||t==="127.0.0.1"||t==="::1"?!0:r.some(s=>!!s.host&&(0,manifest_1.hostMatches)(s.host,t)):!1},"isRoutedOrigin");exports.isRoutedOrigin=isRoutedOrigin;const cors=o((e,r,t)=>{const s=e.headers.origin;typeof s!="string"||s===""||(r.setHeader("vary","Origin"),(0,exports.isRoutedOrigin)(s,t)&&(r.setHeader("access-control-allow-origin",s),r.setHeader("access-control-allow-credentials","true")))},"cors");exports.cors=cors;const corsPreflight=o((e,r,t,s)=>{(0,exports.cors)(e,r,s),r.writeHead(204,{"access-control-allow-methods":t,"access-control-allow-headers":String(e.headers["access-control-request-headers"]??"*")}),r.end()},"corsPreflight");exports.corsPreflight=corsPreflight,exports.PREFLIGHT_METHODS_NOROUTE="GET,HEAD,OPTIONS",exports.PREFLIGHT_METHODS_ROUTED="GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS";
@@ -0,0 +1,22 @@
1
+ export interface GuestRecord {
2
+ wsKey: string;
3
+ root: string;
4
+ projects: Record<string, {
5
+ root: string;
6
+ }>;
7
+ projectDependencies?: Record<string, string[]>;
8
+ /** The root of the daemon it was registered with. */
9
+ daemonRoot: string;
10
+ registeredAt: string;
11
+ }
12
+ export declare function guestsFile(): string;
13
+ export declare function readGuests(file?: string): GuestRecord[];
14
+ /** Record (or replace) a registration. Best-effort: a daemon that cannot write its home still serves the guest. */
15
+ export declare function recordGuest(entry: GuestRecord, file?: string): void;
16
+ /** Forget a registration (an unregister, or a root that is gone). */
17
+ export declare function forgetGuest(wsKey: string, file?: string): void;
18
+ /** What a daemon serving `daemonRoot` replays: its own guests whose folder is still a workspace — not itself. */
19
+ export declare function guestsToReplay(daemonRoot: string, list: readonly GuestRecord[]): {
20
+ replay: GuestRecord[];
21
+ gone: GuestRecord[];
22
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";var a=Object.defineProperty;var n=(r,t)=>a(r,"name",{value:t,configurable:!0});var c=Object.defineProperty,o=n((r,t)=>c(r,"name",{value:t,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.guestsFile=guestsFile,exports.readGuests=readGuests,exports.recordGuest=recordGuest,exports.forgetGuest=forgetGuest,exports.guestsToReplay=guestsToReplay;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"));function guestsFile(){return path.join(process.env.LENSMCP_HOME||os.homedir(),".lensmcp","guests.json")}n(guestsFile,"guestsFile"),o(guestsFile,"guestsFile");function readGuests(r=guestsFile()){try{const t=JSON.parse(fs.readFileSync(r,"utf8"));return Array.isArray(t)?t.filter(e=>!!e&&typeof e=="object"&&typeof e.wsKey=="string"&&typeof e.root=="string"&&typeof e.daemonRoot=="string"&&!!e.projects&&typeof e.projects=="object"):[]}catch{return[]}}n(readGuests,"readGuests"),o(readGuests,"readGuests");function writeGuests(r,t){fs.mkdirSync(path.dirname(t),{recursive:!0,mode:448});const e=`${t}.${process.pid}.tmp`;fs.writeFileSync(e,JSON.stringify(r,null,2)+`
2
+ `,{mode:384}),fs.renameSync(e,t)}n(writeGuests,"writeGuests"),o(writeGuests,"writeGuests");function recordGuest(r,t=guestsFile()){try{writeGuests([...readGuests(t).filter(e=>e.wsKey!==r.wsKey),r],t)}catch(e){console.warn(`[gateway] could not record '${r.wsKey}' for replay (${e.message}) \u2014 a daemon restart will not bring it back`)}}n(recordGuest,"recordGuest"),o(recordGuest,"recordGuest");function forgetGuest(r,t=guestsFile()){try{const e=readGuests(t),s=e.filter(i=>i.wsKey!==r);s.length!==e.length&&writeGuests(s,t)}catch{}}n(forgetGuest,"forgetGuest"),o(forgetGuest,"forgetGuest");const same=o((r,t)=>{const e=o(s=>{try{return fs.realpathSync(s)}catch{return path.resolve(s)}},"real");return e(r)===e(t)},"same");function guestsToReplay(r,t){const e=[],s=[];for(const i of t)!same(i.daemonRoot,r)||same(i.root,r)||(fs.existsSync(path.join(i.root,"package.json"))?e:s).push(i);return{replay:e,gone:s}}n(guestsToReplay,"guestsToReplay"),o(guestsToReplay,"guestsToReplay");