@lensmcp/cluster 1.25.5 → 1.25.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,6 +83,7 @@ A second backend **path-mounted** under a shared API domain:
83
83
  | `idleKillSec` | Kill a gateway-spawned (non-eager) service after this many idle seconds. |
84
84
  | `serveTarget` | Nx target that runs the service. Default `serve-hmr`. |
85
85
  | `adminUrl` | Devserver admin endpoint for scaling (defaults from `port`). |
86
+ | `sources` | **Custom sources** (dev only): what the service REPORTS that the lens cannot observe — e.g. work waiting in its own outbox table. `[{ kind: "queue", name, path, intervalSec?, thresholds?: { oldestWaitMin? } }]`; `path` is a route on the service answering `lensmcp.queue/v1`, read from a running pod over its socket (a service scaled to zero is never woken). An unknown kind or a malformed entry is left out and logged. See the docs' *Custom sources* page. |
86
87
 
87
88
  A declaration must route *somewhere*: either a pod `service` or a TCP `upstream`/`port`.
88
89
 
@@ -0,0 +1,68 @@
1
+ import type * as http from 'node:http';
2
+ /** The header a ticketed request carries. Gateway-owned: set aside at ingress, never forwarded. */
3
+ export declare const ACT_HEADER = "x-lensmcp-act";
4
+ /** Where `stripOwnedHeaders` sets the presented ticket aside for the JWT step. */
5
+ export declare const ACT_TICKET: unique symbol;
6
+ /** How long a ticket can be redeemed. */
7
+ export declare const ACT_TICKET_MS = 60000;
8
+ /** A remembered identity not seen for this long is forgotten. */
9
+ export declare const ACT_RETAIN_MS: number;
10
+ /** The most identities kept per workspace (the least recently seen go first). */
11
+ export declare const ACT_MAX_IDENTITIES = 20;
12
+ /** One remembered identity, as `GET /identities` and `POST /act` describe it — no claims beyond these, no token. */
13
+ export interface ActIdentityView {
14
+ user: string;
15
+ tenant?: string;
16
+ email?: string;
17
+ name?: string;
18
+ roles?: string[];
19
+ /** The host its browser called. */
20
+ host: string;
21
+ firstSeenAt: number;
22
+ lastSeenAt: number;
23
+ /** `token`: a request acting as it now carries its browser's own token; `identity`: the gateway stamps it. */
24
+ mode: 'token' | 'identity';
25
+ }
26
+ /** What a redeemed ticket hands the JWT step. */
27
+ export interface ActGrant {
28
+ /** The browser's own, still-valid token — verify it as usual. Absent: stamp {@link claims}. */
29
+ token?: string;
30
+ claims: Record<string, unknown>;
31
+ view: ActIdentityView;
32
+ }
33
+ export interface ActStore {
34
+ /** A request's verified token: remember who it was — when it came from a browser. */
35
+ remember(wsKey: string, claims: Record<string, unknown>, token: string | undefined, host: string, now?: number): void;
36
+ /** The identities remembered for a workspace, most recently seen first. */
37
+ list(wsKey: string, now?: number): ActIdentityView[];
38
+ /** A ticket to act as one of them: `user` (id, email or name) and `tenant` narrow it; none = the most recent. */
39
+ mint(wsKey: string, pick: {
40
+ user?: string;
41
+ tenant?: string;
42
+ }, now?: number): {
43
+ ticket: string;
44
+ expiresAt: number;
45
+ identity: ActIdentityView;
46
+ } | {
47
+ error: string;
48
+ identities: ActIdentityView[];
49
+ };
50
+ /** Spend a ticket. Undefined: unknown, spent, expired, or for another workspace. */
51
+ redeem(ticket: string, wsKey: string, now?: number): ActGrant | undefined;
52
+ /** Write what is pending (the gateway is stopping). */
53
+ flush(): void;
54
+ }
55
+ /** Did a browser send this? Fetch Metadata (`sec-fetch-*`) is sent by every current browser and by no script. */
56
+ export declare function fromBrowser(req: Pick<http.IncomingMessage, 'headers'>): boolean;
57
+ /** The ticket the request presented (set aside by `stripOwnedHeaders`), if it is shaped like one. */
58
+ export declare function actTicketOf(req: http.IncomingMessage): string | undefined;
59
+ /** Set the presented ticket aside — called by `stripOwnedHeaders` before the header is dropped. */
60
+ export declare function setActTicketAside(req: http.IncomingMessage): void;
61
+ /**
62
+ * The store. `dir` is where the identities are kept (`~/.lensmcp/identities`); without one nothing is written —
63
+ * the specs' store, and a gateway whose home cannot be written.
64
+ */
65
+ export declare function createActStore(opts?: {
66
+ dir?: string;
67
+ now?: () => number;
68
+ }): ActStore;
@@ -0,0 +1 @@
1
+ "use strict";var b=Object.defineProperty;var S=(e,a)=>b(e,"name",{value:a,configurable:!0});var h=Object.defineProperty,c=S((e,a)=>h(e,"name",{value:a,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.ACT_MAX_IDENTITIES=exports.ACT_RETAIN_MS=exports.ACT_TICKET_MS=exports.ACT_TICKET=exports.ACT_HEADER=void 0,exports.fromBrowser=fromBrowser,exports.actTicketOf=actTicketOf,exports.setActTicketAside=setActTicketAside,exports.createActStore=createActStore;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));exports.ACT_HEADER="x-lensmcp-act",exports.ACT_TICKET=Symbol.for("lensmcp.gateway.actTicket"),exports.ACT_TICKET_MS=6e4,exports.ACT_RETAIN_MS=720*60*6e4,exports.ACT_MAX_IDENTITIES=20;const TOKEN_MARGIN_S=30,MAX_OPEN_TICKETS=200,WRITE_EVERY_MS=3e4,TIME_CLAIMS=new Set(["exp","iat","nbf","jti"]),TICKET_VALUE=/^[A-Za-z0-9_-]{43}$/;function fromBrowser(e){return typeof e.headers["sec-fetch-mode"]=="string"||typeof e.headers["sec-fetch-site"]=="string"}S(fromBrowser,"fromBrowser"),c(fromBrowser,"fromBrowser");function actTicketOf(e){return e[exports.ACT_TICKET]}S(actTicketOf,"actTicketOf"),c(actTicketOf,"actTicketOf");function setActTicketAside(e){const a=e.headers[exports.ACT_HEADER];typeof a=="string"&&TICKET_VALUE.test(a)&&(e[exports.ACT_TICKET]=a)}S(setActTicketAside,"setActTicketAside"),c(setActTicketAside,"setActTicketAside");const str=c(e=>typeof e=="string"&&e.length>0&&e.length<=256?e:void 0,"str"),keyOf=c(e=>{const a=str(e.sub);return a?`${a}\0${str(e.tid)??""}`:void 0},"keyOf"),safeName=c(e=>e.replace(/[^A-Za-z0-9._-]/g,"_").slice(0,120)||"_","safeName");function viewOf(e,a){const A=e.claims,d=Array.isArray(A.roles)?A.roles.filter(T=>typeof T=="string").slice(0,20):void 0,y=str(A.tid),u=str(A.email),E=str(A.name)??str(A.preferred_username);return{user:str(A.sub),...y?{tenant:y}:{},...u?{email:u}:{},...E?{name:E}:{},...d?.length?{roles:d}:{},host:e.host,firstSeenAt:e.firstSeenAt,lastSeenAt:e.lastSeenAt,mode:e.token&&e.tokenExp!==void 0&&e.tokenExp-TOKEN_MARGIN_S>a/1e3?"token":"identity"}}S(viewOf,"viewOf"),c(viewOf,"viewOf");function createActStore(e={}){const a=e.now??Date.now,A=new Map,d=new Map,y=new Map,u=new Set,E=c(t=>e.dir?path.join(e.dir,`${safeName(t)}.json`):void 0,"file"),T=c(t=>{let s=A.get(t);if(s)return s;s=new Map,A.set(t,s);const n=E(t);if(!n)return s;try{const o=JSON.parse(fs.readFileSync(n,"utf8"));for(const i of Array.isArray(o.identities)?o.identities:[]){const r=i;if(!r||typeof r!="object"||!r.claims||typeof r.claims!="object"||typeof r.host!="string"||typeof r.firstSeenAt!="number"||typeof r.lastSeenAt!="number")continue;const f=keyOf(r.claims);f&&s.set(f,{claims:r.claims,host:r.host,firstSeenAt:r.firstSeenAt,lastSeenAt:r.lastSeenAt})}}catch{}return s},"load"),g=c((t,s)=>{for(const[n,o]of t)s-o.lastSeenAt>exports.ACT_RETAIN_MS&&t.delete(n);if(t.size>exports.ACT_MAX_IDENTITIES){const n=[...t.entries()].sort((o,i)=>o[1].lastSeenAt-i[1].lastSeenAt).slice(0,t.size-exports.ACT_MAX_IDENTITIES);for(const[o]of n)t.delete(o)}},"prune"),k=c((t,s)=>{const n=E(t),o=A.get(t);if(u.delete(t),!(!n||!o)){y.set(t,s);try{fs.mkdirSync(path.dirname(n),{recursive:!0,mode:448});const i=[...o.values()].map(f=>({claims:f.claims,host:f.host,firstSeenAt:f.firstSeenAt,lastSeenAt:f.lastSeenAt})),r=`${n}.${process.pid}.tmp`;fs.writeFileSync(r,JSON.stringify({identities:i},null,1),{mode:384}),fs.renameSync(r,n)}catch{}}},"write"),I=c((t,s,n,o,i=a())=>{const r=keyOf(s);if(!r)return;const f=T(t),_={};for(const[x,w]of Object.entries(s))TIME_CLAIMS.has(x)||(_[x]=w);const m=f.get(r),l=typeof s.exp=="number"?s.exp:void 0,p=!m||m.host!==o||JSON.stringify(m.claims)!==JSON.stringify(_);f.set(r,{claims:_,host:o,firstSeenAt:m?.firstSeenAt??i,lastSeenAt:i,...n&&l!==void 0?{token:n,tokenExp:l}:m?.token?{token:m.token,tokenExp:m.tokenExp}:{}}),g(f,i),p&&u.add(t),(p||i-(y.get(t)??0)>=WRITE_EVERY_MS)&&k(t,i)},"remember"),C=c((t,s=a())=>{const n=T(t);return g(n,s),[...n.values()].sort((o,i)=>i.lastSeenAt-o.lastSeenAt).map(o=>viewOf(o,s))},"list");return{remember:I,list:C,mint:c((t,s,n=a())=>{const o=T(t);g(o,n);const i=s.user?.trim().toLowerCase(),r=s.tenant?.trim(),f=[...o.entries()].filter(([,l])=>{const p=l.claims;return r&&str(p.tid)!==r?!1:i?[p.sub,p.email,p.name,p.preferred_username].some(x=>typeof x=="string"&&x.toLowerCase()===i):!0}).sort((l,p)=>p[1].lastSeenAt-l[1].lastSeenAt)[0];if(!f){const l=C(t,n);return{error:l.length?`No signed-in identity matches${i?` user '${s.user}'`:""}${r?` in tenant '${r}'`:""} \u2014 ${l.length} known (see identities).`:"No one has signed in to this workspace's app since the gateway started remembering identities. Sign in once in the browser \u2014 the gateway keeps the identity it verified for 30 days.",identities:l}}for(const[l,p]of d)p.expiresAt<=n&&d.delete(l);d.size>=MAX_OPEN_TICKETS&&d.delete(d.keys().next().value);const _=(0,node_crypto_1.randomBytes)(32).toString("base64url"),m=n+exports.ACT_TICKET_MS;return d.set(_,{wsKey:t,key:f[0],expiresAt:m}),{ticket:_,expiresAt:m,identity:viewOf(f[1],n)}},"mint"),redeem:c((t,s,n=a())=>{const o=d.get(t);if(!o||(d.delete(t),o.expiresAt<=n||o.wsKey!==s))return;const i=T(s).get(o.key);if(!i)return;const r=viewOf(i,n);return r.mode==="token"?{token:i.token,claims:i.claims,view:r}:{claims:i.claims,view:r}},"redeem"),flush:c(()=>{const t=a();for(const s of[...u])k(s,t)},"flush")}}S(createActStore,"createActStore"),c(createActStore,"createActStore");
@@ -1,3 +1,4 @@
1
+ import { type ActStore } from './act';
1
2
  import type { AuthVerdict, GatewayRuntime, GwTrace, Route } from './types';
2
3
  import type { Observability } from './observability';
3
4
  import type { JwtVerifier } from '../jwks-verify';
@@ -28,4 +29,8 @@ export interface EdgeVerifierPool {
28
29
  for(host: string): JwtVerifier | undefined;
29
30
  stop(): void;
30
31
  }
31
- export declare function createEdgeAuth(rt: GatewayRuntime, obs: Observability, verifiers?: EdgeVerifierPool): EdgeAuth;
32
+ /**
33
+ * `act` (act.ts) remembers who the browser's verified tokens were, and redeems the tickets `lensmcp call` presents —
34
+ * absent (`LENSMCP_GW_ACT=0`, or a spec), nothing is remembered and every ticket is refused.
35
+ */
36
+ export declare function createEdgeAuth(rt: GatewayRuntime, obs: Observability, verifiers?: EdgeVerifierPool, act?: ActStore): EdgeAuth;
@@ -1 +1 @@
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");
1
+ "use strict";var _=Object.defineProperty;var w=(a,s)=>_(a,"name",{value:s,configurable:!0});var b=Object.defineProperty,p=w((a,s)=>b(a,"name",{value:s,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.declaredCallerIds=declaredCallerIds,exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),act_1=require("./act"),chooser_1=require("./chooser"),dev_auth_1=require("./dev-auth"),edge_1=require("./edge"),NO_CALLER_IDS=new Set;function declaredCallerIds(a,s,c){const k=(a.registry?a.registry.allServices():a.services).filter(r=>c===void 0||r.wsKey===c),i=k.find(r=>r.project===s)?.decl.callerIds;if(!Array.isArray(i)||i.length===0)return NO_CALLER_IDS;const e=new Set(k.map(r=>r.project));for(const[r,n]of a.keyToProject)(c===void 0||a.keyToWorkspace?.get(r)===c)&&e.add(n);return new Set(i.filter(r=>typeof r=="string"&&r!==s&&!e.has(r)))}w(declaredCallerIds,"declaredCallerIds"),p(declaredCallerIds,"declaredCallerIds");const bearerOf=p(a=>{const s=a.headers.authorization,c=Array.isArray(s)?s[0]:s;return typeof c=="string"&&c.startsWith("Bearer ")?c.slice(7):void 0},"bearerOf");function createEdgeAuth(a,s,c,k){const{traceStep:i}=s;return{internal:p((e,r,n)=>{const v=String(e.headers["x-api-key"]??""),o=a.keyToProject.get(v);if(!o)return i(n,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};const l=a.keyToWorkspace?.get(v),t=r.svc?.wsKey;if(l&&t&&l!==t)return i(n,"auth",{mode:"x-api-key",ok:!1,reason:"cross-workspace",caller:o,callerWorkspace:l,routeWorkspace:t}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};const d=(0,edge_1.claimedCallerIdOf)(e),h=d!==void 0&&d!==o&&declaredCallerIds(a,o,l).has(d)?d:o;e.headers["x-api-key-id"]=h,l?e.headers["x-api-key-workspace"]=l:delete e.headers["x-api-key-workspace"],e.__lensmcpCaller=o,delete e.headers["x-api-key"];const u=(0,edge_1.callerContextOf)(e);for(const[g,y]of Object.entries(u))e.headers[g]=y;return i(n,"auth",{mode:"x-api-key",ok:!0,caller:o,...h!==o?{as:h}:{},...d!==void 0&&d!==o&&h===o?{claimRefused:d}:{},...l?{callerWorkspace:l}:{},...u["x-tenant-id"]!==void 0?{callerTenant:!0}:{},...u["x-user-id"]!==void 0?{callerUser:!0}:{}}),{ok:!0,caller:o}},"internal"),public:p((e,r,n,v)=>{const o=r.wsKey??r.svc?.wsKey??a.wsKey,l=(0,act_1.actTicketOf)(e);let t;if(l!==void 0){if(t=k&&(0,chooser_1.isLoopbackRequest)(e)?k.redeem(l,o):void 0,!t)return i(n,"auth",{mode:"act",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: the act ticket is unknown, spent, expired or for another workspace \u2014 ask for a new one (lensmcp call)."};t.token?e.headers.authorization=`Bearer ${t.token}`:delete e.headers.authorization}const d=p(u=>t?{act:u,as:t.view.user,...t.view.tenant?{tenant:t.view.tenant}:{}}:{},"actStep"),h=r.auth;if(h){const u=v??(0,edge_1.canonicalPath)(e.url??"/")??(e.url??"/").split("?")[0],g=(e.method??"GET").toUpperCase(),y=(0,manifest_1.authRuleFor)(h,u,g);if(y.mode==="jwt"){const x=String(e.headers.host??"").split(":")[0].toLowerCase();let m=t&&!t.token?"identity":"token",f=m==="identity"?t.claims:(0,dev_auth_1.verifyDevJwt)(e,c?.for(x));return!f&&t&&(m="identity",f=t.claims,delete e.headers.authorization),f?y.rule&&!(0,manifest_1.evalRule)(y.rule,(0,dev_auth_1.devAttrs)(e,f,u))?(i(n,"authz",{ok:!1,...d(m)}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):((0,dev_auth_1.stampIdentity)(e,f),k&&!t&&(0,act_1.fromBrowser)(e)&&k.remember(o,f,bearerOf(e),x),i(n,"auth",{mode:"jwt",ok:!0,...d(m)}),{ok:!0,claims:f}):(i(n,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"})}return i(n,"auth",{mode:y.mode,...d(t?.token?"token":"identity")}),{ok:!0}}return i(n,"auth",{mode:"public",unconfigured:!0,...d(t?.token?"token":"identity")}),{ok:!0}},"publicAuth")}}w(createEdgeAuth,"createEdgeAuth"),p(createEdgeAuth,"createEdgeAuth");
@@ -1,3 +1,4 @@
1
+ import type { ActStore } from './act';
1
2
  import type { ServiceLayer } from './lifecycle';
2
3
  import type { GatewayRuntime } from './types';
3
4
  import type { WorkspaceFragment } from './route-registry';
@@ -17,6 +18,8 @@ export interface ControlServerDeps {
17
18
  killService: (svc: WorkspaceFragment['services'][number], reason: string) => void;
18
19
  /** Restart one service (the svcLayer's `restartService`); `POST /restart` answers 501 without it. */
19
20
  restartService?: ServiceLayer['restartService'];
21
+ /** Who signed in, and tickets to act as them (act.ts); `/identities` and `/act` answer 501 without it. */
22
+ act?: ActStore;
20
23
  /** Hooks so the daemon can host/tear-down a workspace's dashboard around the route merge (P3d).
21
24
  * `onRegister` may be async (port probing) — the register handler AWAITS it before the rebuild. */
22
25
  onRegister?: (fragment: WorkspaceFragment) => void | Promise<void>;
@@ -1,2 +1,2 @@
1
- "use strict";var x=Object.defineProperty;var p=(s,r)=>x(s,"name",{value:r,configurable:!0});var O=Object.defineProperty,u=p((s,r)=>O(s,"name",{value:r,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 r=s?.instance;return r?{instance:r.name,baseKey:r.baseKey,apex:r.apex,baseApex:r.baseApex,...r.frozen?{frozen:!0}:{}}:{}}p(instanceSummary,"instanceSummary"),u(instanceSummary,"instanceSummary");function controlSocketPath(){const s=process.env.LENSMCP_HOME||os.homedir();return path.join(s,".lensmcp","control.sock")}p(controlSocketPath,"controlSocketPath"),u(controlSocketPath,"controlSocketPath");const CONTROL_BODY_BYTES=1e6;class BodyTooLarge extends Error{static{p(this,"BodyTooLarge")}static{u(this,"BodyTooLarge")}constructor(r){super(`body too large (over ${r} bytes)`),this.limit=r}}exports.clusterVersion=versionAtLoad(()=>readClusterVersion(__dirname));function versionAtLoad(s){const r=s();return()=>r}p(versionAtLoad,"versionAtLoad"),u(versionAtLoad,"versionAtLoad");function readClusterVersion(s){let r=s;for(let y=0;y<6;y++){const w=path.join(r,"package.json");if(fs.existsSync(w))try{const g=JSON.parse(fs.readFileSync(w,"utf8"));if(g.name==="@lensmcp/cluster"&&typeof g.version=="string")return g.version}catch{}const i=path.dirname(r);if(i===r)break;r=i}}p(readClusterVersion,"readClusterVersion"),u(readClusterVersion,"readClusterVersion");function readJsonBody(s,r=CONTROL_BODY_BYTES){return new Promise((y,w)=>{const i=[];let g=0,k=!1;s.on("data",f=>{if(!k){if(g+=f.length,g>r){k=!0,i.length=0,w(new BodyTooLarge(r));return}i.push(f)}}),s.on("end",()=>{if(k)return;const f=Buffer.concat(i).toString("utf8");try{y({value:f?JSON.parse(f):{},bytes:g})}catch(S){w(S)}}),s.on("error",w)})}p(readJsonBody,"readJsonBody"),u(readJsonBody,"readJsonBody");function graphShare(s,r){return r-Buffer.byteLength(JSON.stringify({...s,projectGraph:null}))}p(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}}p(graphVerdict,"graphVerdict"),u(graphVerdict,"graphVerdict");function ensureOwnerOnlyDir(s){try{fs.mkdirSync(s,{recursive:!0,mode:448})}catch{}try{const r=fs.lstatSync(s);return r.isDirectory()?((r.mode&63)!==0&&fs.chmodSync(s,448),!0):!1}catch{return!1}}p(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),u(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(s){const{rt:r}=s,y=controlSocketPath(),w=path.dirname(y);ensureOwnerOnlyDir(w)||console.warn(`[gateway] control home ${w} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(y)}catch{}const i=u((e,n,a)=>{e.writeHead(n,{"content-type":"application/json"}),e.end(JSON.stringify(a))},"send"),g=u(async(e,n)=>{if(!e.wsKey||!e.root)return{status:400,json:{error:"wsKey and root are required"}};const a=e.projects??{},t=(0,discovery_1.projectSourceRoots)(e.root,a,e.projectDependencies),o=(0,scope_1.readLensScope)(e.root),c=o.key===e.wsKey,d=c&&o.sockNs!==e.wsKey?o.sockNs:void 0,{routes:l,services:h,instance:m}=(0,discovery_1.discoverRoutes)(e.root,a,void 0,e.wsKey,t,d,c?o.instance:void 0),b=(0,workspace_middleware_1.findGatewayMiddleware)(e.root,a),$={wsKey:e.wsKey,...d?{sockNs:d}:{},root:e.root,routes:l,services:h,...m?{instance:m}:{},...b?{middleware:b}:{}},j=(0,registration_guards_1.refuseRegistration)(r.registry,$);if(j)return console.warn(`[gateway] refused to register '${e.wsKey}' from ${e.root}: ${j.error}`),{status:409,json:j};try{const v=(0,service_keys_1.adoptServiceKeys)(r,e.root,h,e.wsKey);v.added.length>0&&console.log(`[gateway] adopted east-west keys for ${e.wsKey}: ${v.added.join(", ")}`),v.regenerated.length>0&&console.warn(`[gateway] ${e.root} carried another workspace's east-west keys (a copied key file) \u2014 new keys for ${e.wsKey}: ${v.regenerated.join(", ")}`)}catch(v){console.error(`[gateway] could not adopt east-west keys for ${e.wsKey}: ${v.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(e.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${e.wsKey}') is served by the daemon at ${r.root} (pid ${process.pid}); its gateway-level log is ${path.join(r.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
2
- `)}catch{}let K;if(r.graphStores){const v={root:e.root,daemonRoot:r.root,otherRoots:r.registry.keys().filter(_=>_!==e.wsKey).flatMap(_=>r.registry.get(_)?.root??[])};K=e.projectGraph===void 0?void 0:(0,guest_graph_1.admitForwardedGraph)(e.projectGraph,{...v,bytes:graphShare(e,n)}),K?.refused!==void 0&&console.warn(`[gateway] refused the project graph '${e.wsKey}' forwarded from ${e.root}: ${K.refused} \u2014 its services start through \`nx run\` until the gateway computes it`),(0,registration_guards_1.sameRoot)(e.root,r.root)||r.graphStores.set((0,guest_graph_1.createGuestGraphStore)({wsKey:e.wsKey,root:e.root,...K?{admission:K}:{},context:v}))}return r.registry.register($),await s.onRegister?.($),r.rebuildRoutes(),r.root&&!(0,registration_guards_1.sameRoot)(e.root,r.root)&&(0,guest_replay_1.recordGuest)({wsKey:e.wsKey,root:e.root,projects:a,...e.projectDependencies?{projectDependencies:e.projectDependencies}:{},daemonRoot:r.root,registeredAt:new Date().toISOString()}),{status:200,json:{ok:!0,wsKey:e.wsKey,routes:l.length,services:h.length,workspaces:r.registry.keys(),...r.graphStores?{projectGraph:graphVerdict(K)}:{}}}},"register"),k=u((e,n)=>{const a=r.registry.get(e);if(!a)return{error:`no workspace '${e}' is registered`,workspaces:r.registry.keys()};const t=a.services.find(o=>o.project===n);return t?{svc:t}:{error:`'${e}' has no service '${n}'`,services:a.services.map(o=>o.project)}},"findService"),f=u(e=>({wsKey:e.wsKey,service:e.project,state:e.state,...e.generation!==void 0?{generation:e.generation}:{},...e.restart?{restart:e.restart}:{}}),"restartView"),S=http.createServer((e,n)=>{(async()=>{try{const a=(e.url??"").split("?")[0];if(e.method==="GET"&&a==="/list")return i(n,200,{workspaces:r.registry.keys().map(t=>{const o=r.registry.get(t);return{key:t,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0,...instanceSummary(o)}})});if(e.method==="GET"&&a==="/status"){const t=(0,exports.clusterVersion)();return i(n,200,{daemon:{pid:process.pid,wsKey:r.wsKey,...t?{version:t}:{}},workspaces:r.registry.keys().map(o=>{const c=r.registry.get(o);return{key:o,root:c?.root,routes:c?.routes.length??0,services:c?.services.length??0,isDaemon:o===r.wsKey,...instanceSummary(c)}}),services:r.registry.allServices().map(o=>({project:o.project,wsKey:o.wsKey,host:o.decl.host,state:o.state,...o.generation!==void 0?{generation:o.generation}:{},...o.restart?{restart:o.restart}:{}}))})}if(e.method==="GET"&&a==="/instance-env"){const t=new URL(e.url??"/","http://control").searchParams.get("wsKey")??"",o=r.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(a==="/restart"&&(e.method==="POST"||e.method==="GET")){const t=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},o=typeof t.wsKey=="string"?t.wsKey:"",c=typeof t.service=="string"?t.service:"";if(!o||!c)return i(n,400,{error:"wsKey and service are required"});const d=k(o,c);if("error"in d)return i(n,404,d);const l=d.svc;if(e.method==="GET")return i(n,200,f(l));if(!s.restartService)return i(n,501,{error:"this gateway cannot restart a service"});const h=typeof t.reason=="string"&&t.reason.trim()?t.reason.trim().slice(0,200):"asked for",m=s.restartService(l,{hard:t.hard===!0,reason:h});return"refused"in m?i(n,409,{error:m.refused,...f(l)}):i(n,202,{ok:!0,already:m.already,...f(l)})}if(e.method==="POST"&&a==="/register"){const{value:t,bytes:o}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),c=await g(t,o);return i(n,c.status,c.json)}if(e.method==="POST"&&a==="/unregister"){const t=(await readJsonBody(e)).value;if(!t.wsKey)return i(n,400,{error:"wsKey is required"});const o=(0,registration_guards_1.refuseUnregistration)(r.registry,{wsKey:t.wsKey,...t.root?{root:t.root}:{}},r.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=r.registry.unregister(t.wsKey);if(c){for(const d of c.services){const l=(0,service_keys_1.serviceKeyId)(d.wsKey??t.wsKey,d.project),h=r.serviceKeys[l];h&&(delete r.serviceKeys[l],r.keyToProject.delete(h),r.keyToWorkspace?.delete(h))}for(const d of c.services)s.killService(d,"workspace unregistered");r.graphStores?.delete(c.root),(0,guest_replay_1.forgetGuest)(t.wsKey),s.onUnregister?.(c)}return r.rebuildRoutes(),i(n,200,{ok:!0,removed:!!c,workspaces:r.registry.keys()})}i(n,404,{error:"not found"})}catch(a){if(a instanceof BodyTooLarge)return e.resume(),i(n,413,{error:a.message});i(n,500,{error:a.message})}})()});return S.listen(y,()=>{try{fs.chmodSync(y,384)}catch(e){console.warn(`[gateway] could not restrict ${y} to 0600:`,e.message)}}),{socketPath:y,async replay(){const{replay:e,gone:n}=(0,guest_replay_1.guestsToReplay)(r.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 e)if(!r.registry.has(t.wsKey))try{const o=await g({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(()=>{S.close();try{fs.unlinkSync(y)}catch{}},"close")}}p(startControlServer,"startControlServer"),u(startControlServer,"startControlServer");
1
+ "use strict";var b=Object.defineProperty;var g=(s,r)=>b(s,"name",{value:r,configurable:!0});var G=Object.defineProperty,u=g((s,r)=>G(s,"name",{value:r,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 r=s?.instance;return r?{instance:r.name,baseKey:r.baseKey,apex:r.apex,baseApex:r.baseApex,...r.frozen?{frozen:!0}:{}}:{}}g(instanceSummary,"instanceSummary"),u(instanceSummary,"instanceSummary");function controlSocketPath(){const s=process.env.LENSMCP_HOME||os.homedir();return path.join(s,".lensmcp","control.sock")}g(controlSocketPath,"controlSocketPath"),u(controlSocketPath,"controlSocketPath");const CONTROL_BODY_BYTES=1e6;class BodyTooLarge extends Error{static{g(this,"BodyTooLarge")}static{u(this,"BodyTooLarge")}constructor(r){super(`body too large (over ${r} bytes)`),this.limit=r}}exports.clusterVersion=versionAtLoad(()=>readClusterVersion(__dirname));function versionAtLoad(s){const r=s();return()=>r}g(versionAtLoad,"versionAtLoad"),u(versionAtLoad,"versionAtLoad");function readClusterVersion(s){let r=s;for(let w=0;w<6;w++){const h=path.join(r,"package.json");if(fs.existsSync(h))try{const l=JSON.parse(fs.readFileSync(h,"utf8"));if(l.name==="@lensmcp/cluster"&&typeof l.version=="string")return l.version}catch{}const a=path.dirname(r);if(a===r)break;r=a}}g(readClusterVersion,"readClusterVersion"),u(readClusterVersion,"readClusterVersion");function readJsonBody(s,r=CONTROL_BODY_BYTES){return new Promise((w,h)=>{const a=[];let l=0,k=!1;s.on("data",m=>{if(!k){if(l+=m.length,l>r){k=!0,a.length=0,h(new BodyTooLarge(r));return}a.push(m)}}),s.on("end",()=>{if(k)return;const m=Buffer.concat(a).toString("utf8");try{w({value:m?JSON.parse(m):{},bytes:l})}catch(S){h(S)}}),s.on("error",h)})}g(readJsonBody,"readJsonBody"),u(readJsonBody,"readJsonBody");function graphShare(s,r){return r-Buffer.byteLength(JSON.stringify({...s,projectGraph:null}))}g(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}}g(graphVerdict,"graphVerdict"),u(graphVerdict,"graphVerdict");function ensureOwnerOnlyDir(s){try{fs.mkdirSync(s,{recursive:!0,mode:448})}catch{}try{const r=fs.lstatSync(s);return r.isDirectory()?((r.mode&63)!==0&&fs.chmodSync(s,448),!0):!1}catch{return!1}}g(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),u(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(s){const{rt:r}=s,w=controlSocketPath(),h=path.dirname(w);ensureOwnerOnlyDir(h)||console.warn(`[gateway] control home ${h} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(w)}catch{}const a=u((e,n,i)=>{e.writeHead(n,{"content-type":"application/json"}),e.end(JSON.stringify(i))},"send"),l=u(async(e,n)=>{if(!e.wsKey||!e.root)return{status:400,json:{error:"wsKey and root are required"}};const i=e.projects??{},t=(0,discovery_1.projectSourceRoots)(e.root,i,e.projectDependencies),o=(0,scope_1.readLensScope)(e.root),c=o.key===e.wsKey,y=c&&o.sockNs!==e.wsKey?o.sockNs:void 0,{routes:d,services:p,instance:f}=(0,discovery_1.discoverRoutes)(e.root,i,void 0,e.wsKey,t,y,c?o.instance:void 0),O=(0,workspace_middleware_1.findGatewayMiddleware)(e.root,i),$={wsKey:e.wsKey,...y?{sockNs:y}:{},root:e.root,routes:d,services:p,...f?{instance:f}:{},...O?{middleware:O}:{}},j=(0,registration_guards_1.refuseRegistration)(r.registry,$);if(j)return console.warn(`[gateway] refused to register '${e.wsKey}' from ${e.root}: ${j.error}`),{status:409,json:j};try{const v=(0,service_keys_1.adoptServiceKeys)(r,e.root,p,e.wsKey);v.added.length>0&&console.log(`[gateway] adopted east-west keys for ${e.wsKey}: ${v.added.join(", ")}`),v.regenerated.length>0&&console.warn(`[gateway] ${e.root} carried another workspace's east-west keys (a copied key file) \u2014 new keys for ${e.wsKey}: ${v.regenerated.join(", ")}`)}catch(v){console.error(`[gateway] could not adopt east-west keys for ${e.wsKey}: ${v.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(e.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${e.wsKey}') is served by the daemon at ${r.root} (pid ${process.pid}); its gateway-level log is ${path.join(r.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
2
+ `)}catch{}let K;if(r.graphStores){const v={root:e.root,daemonRoot:r.root,otherRoots:r.registry.keys().filter(_=>_!==e.wsKey).flatMap(_=>r.registry.get(_)?.root??[])};K=e.projectGraph===void 0?void 0:(0,guest_graph_1.admitForwardedGraph)(e.projectGraph,{...v,bytes:graphShare(e,n)}),K?.refused!==void 0&&console.warn(`[gateway] refused the project graph '${e.wsKey}' forwarded from ${e.root}: ${K.refused} \u2014 its services start through \`nx run\` until the gateway computes it`),(0,registration_guards_1.sameRoot)(e.root,r.root)||r.graphStores.set((0,guest_graph_1.createGuestGraphStore)({wsKey:e.wsKey,root:e.root,...K?{admission:K}:{},context:v}))}return r.registry.register($),await s.onRegister?.($),r.rebuildRoutes(),r.root&&!(0,registration_guards_1.sameRoot)(e.root,r.root)&&(0,guest_replay_1.recordGuest)({wsKey:e.wsKey,root:e.root,projects:i,...e.projectDependencies?{projectDependencies:e.projectDependencies}:{},daemonRoot:r.root,registeredAt:new Date().toISOString()}),{status:200,json:{ok:!0,wsKey:e.wsKey,routes:d.length,services:p.length,workspaces:r.registry.keys(),...r.graphStores?{projectGraph:graphVerdict(K)}:{}}}},"register"),k=u((e,n)=>{const i=r.registry.get(e);if(!i)return{error:`no workspace '${e}' is registered`,workspaces:r.registry.keys()};const t=i.services.find(o=>o.project===n);return t?{svc:t}:{error:`'${e}' has no service '${n}'`,services:i.services.map(o=>o.project)}},"findService"),m=u(e=>({wsKey:e.wsKey,service:e.project,state:e.state,...e.generation!==void 0?{generation:e.generation}:{},...e.restart?{restart:e.restart}:{}}),"restartView"),S=http.createServer((e,n)=>{(async()=>{try{const i=(e.url??"").split("?")[0];if(e.method==="GET"&&i==="/list")return a(n,200,{workspaces:r.registry.keys().map(t=>{const o=r.registry.get(t);return{key:t,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0,...instanceSummary(o)}})});if(e.method==="GET"&&i==="/status"){const t=(0,exports.clusterVersion)();return a(n,200,{daemon:{pid:process.pid,wsKey:r.wsKey,...t?{version:t}:{}},workspaces:r.registry.keys().map(o=>{const c=r.registry.get(o);return{key:o,root:c?.root,routes:c?.routes.length??0,services:c?.services.length??0,isDaemon:o===r.wsKey,...instanceSummary(c)}}),services:r.registry.allServices().map(o=>({project:o.project,wsKey:o.wsKey,host:o.decl.host,state:o.state,...o.generation!==void 0?{generation:o.generation}:{},...o.restart?{restart:o.restart}:{}}))})}if(e.method==="GET"&&i==="/instance-env"){const t=new URL(e.url??"/","http://control").searchParams.get("wsKey")??"",o=r.registry.get(t);return o?o.instance?a(n,200,{wsKey:t,root:o.root,...instanceSummary(o),env:o.instance.env}):a(n,404,{error:`'${t}' is not an instance`}):a(n,404,{error:`no workspace '${t}' is registered`})}if(i==="/restart"&&(e.method==="POST"||e.method==="GET")){const t=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},o=typeof t.wsKey=="string"?t.wsKey:"",c=typeof t.service=="string"?t.service:"";if(!o||!c)return a(n,400,{error:"wsKey and service are required"});const y=k(o,c);if("error"in y)return a(n,404,y);const d=y.svc;if(e.method==="GET")return a(n,200,m(d));if(!s.restartService)return a(n,501,{error:"this gateway cannot restart a service"});const p=typeof t.reason=="string"&&t.reason.trim()?t.reason.trim().slice(0,200):"asked for",f=s.restartService(d,{hard:t.hard===!0,reason:p});return"refused"in f?a(n,409,{error:f.refused,...m(d)}):a(n,202,{ok:!0,already:f.already,...m(d)})}if(e.method==="GET"&&i==="/identities"||e.method==="POST"&&i==="/act"){const t=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},o=typeof t.wsKey=="string"?t.wsKey:"";if(!o)return a(n,400,{error:"wsKey is required"});if(!r.registry.get(o)&&o!==r.wsKey)return a(n,404,{error:`no workspace '${o}' is registered`,workspaces:r.registry.keys()});if(!s.act)return a(n,501,{error:"acting as a signed-in user is turned off on this gateway (LENSMCP_GW_ACT=0)"});if(e.method==="GET")return a(n,200,{wsKey:o,identities:s.act.list(o)});const c=u(f=>typeof t[f]=="string"&&t[f].trim()?t[f].trim().slice(0,256):void 0,"pick"),y=c("user"),d=c("tenant"),p=s.act.mint(o,{...y?{user:y}:{},...d?{tenant:d}:{}});return"error"in p?a(n,404,{wsKey:o,...p}):a(n,200,{wsKey:o,...p})}if(e.method==="POST"&&i==="/register"){const{value:t,bytes:o}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),c=await l(t,o);return a(n,c.status,c.json)}if(e.method==="POST"&&i==="/unregister"){const t=(await readJsonBody(e)).value;if(!t.wsKey)return a(n,400,{error:"wsKey is required"});const o=(0,registration_guards_1.refuseUnregistration)(r.registry,{wsKey:t.wsKey,...t.root?{root:t.root}:{}},r.wsKey);if(o)return console.warn(`[gateway] refused to unregister '${t.wsKey}'${t.root?` for ${t.root}`:""}: ${o.error}`),a(n,409,o);const c=r.registry.unregister(t.wsKey);if(c){for(const y of c.services){const d=(0,service_keys_1.serviceKeyId)(y.wsKey??t.wsKey,y.project),p=r.serviceKeys[d];p&&(delete r.serviceKeys[d],r.keyToProject.delete(p),r.keyToWorkspace?.delete(p))}for(const y of c.services)s.killService(y,"workspace unregistered");r.graphStores?.delete(c.root),(0,guest_replay_1.forgetGuest)(t.wsKey),s.onUnregister?.(c)}return r.rebuildRoutes(),a(n,200,{ok:!0,removed:!!c,workspaces:r.registry.keys()})}a(n,404,{error:"not found"})}catch(i){if(i instanceof BodyTooLarge)return e.resume(),a(n,413,{error:i.message});a(n,500,{error:i.message})}})()});return S.listen(w,()=>{try{fs.chmodSync(w,384)}catch(e){console.warn(`[gateway] could not restrict ${w} to 0600:`,e.message)}}),{socketPath:w,async replay(){const{replay:e,gone:n}=(0,guest_replay_1.guestsToReplay)(r.root,(0,guest_replay_1.readGuests)()),i={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 e)if(!r.registry.has(t.wsKey))try{const o=await l({wsKey:t.wsKey,root:t.root,projects:t.projects,...t.projectDependencies?{projectDependencies:t.projectDependencies}:{}},0);o.status===200?(i.replayed.push(t.wsKey),console.log(`[gateway] re-hosted '${t.wsKey}' (${t.root}) \u2014 registered before this daemon restarted`)):(i.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){i.refused.push(t.wsKey),console.warn(`[gateway] could not re-host '${t.wsKey}' (${t.root}): ${o.message}`)}return i},close:u(()=>{S.close();try{fs.unlinkSync(w)}catch{}},"close")}}g(startControlServer,"startControlServer"),u(startControlServer,"startControlServer");
@@ -1 +1 @@
1
- "use strict";var A=Object.defineProperty;var h=(e,o)=>A(e,"name",{value:o,configurable:!0});var I=Object.defineProperty,p=h((e,o)=>I(e,"name",{value:o,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.RETIRING_SUFFIX=void 0,exports.stopRoutingTo=stopRoutingTo,exports.pickSock=pickSock,exports.lensPortFromUpstream=lensPortFromUpstream,exports.rewriteInstanceDecls=rewriteInstanceDecls,exports.discoverRoutes=discoverRoutes,exports.projectSourceRoots=projectSourceRoots;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),manifest_1=require("../manifest"),pod_sock_1=require("../../../pod-sock"),instance_1=require("./instance"),scope_1=require("./scope");exports.RETIRING_SUFFIX=".retiring";function sockIdentity(e){try{const o=fs.statSync(e);return`${o.dev}:${o.ino}:${o.ctimeMs}`}catch{return}}h(sockIdentity,"sockIdentity"),p(sockIdentity,"sockIdentity");function stopRoutingTo(e,o=()=>!0){let a;try{a=fs.readdirSync(e.dir)}catch{a=[]}const n=a.filter(t=>(0,pod_sock_1.isPodSockName)(t)&&o(t)).map(t=>path.join(e.dir,t));if(n.length===0)return n;const l=e.retired??=new Map;for(const t of n){const d=sockIdentity(t);d!==void 0&&l.set(t,d)}const r=new Set(n);return e.socks=e.socks.filter(t=>!r.has(t)),e.idx=-1,n}h(stopRoutingTo,"stopRoutingTo"),p(stopRoutingTo,"stopRoutingTo");function stillRetired(e,o){const a=e.retired?.get(o);return a===void 0?!1:sockIdentity(o)===a?!0:(e.retired?.delete(o),!1)}h(stillRetired,"stillRetired"),p(stillRetired,"stillRetired");function pickSock(e,o=1500){const a=Date.now();if(a-e.scannedAt>o){try{const n=fs.readdirSync(e.dir),l=n.filter(t=>(0,pod_sock_1.isPodSockName)(t)&&!stillRetired(e,path.join(e.dir,t)));if(e.retired)for(const t of[...e.retired.keys()])n.includes(path.basename(t))||e.retired.delete(t);const r=l.filter(t=>!n.includes(t+exports.RETIRING_SUFFIX));e.socks=(r.length>0?r:l).map(t=>path.join(e.dir,t))}catch{e.socks=[]}e.scannedAt=a}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}h(pickSock,"pickSock"),p(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const o=/:(\d+)/.exec(e);return o?Number(o[1]):void 0}h(lensPortFromUpstream,"lensPortFromUpstream"),p(lensPortFromUpstream,"lensPortFromUpstream");function rewriteInstanceDecls(e,o){const a=e.flatMap(([,r])=>[...r.host?[r.host]:[],...typeof r.internalHost=="string"?[r.internalHost]:[]]),n=o.apex??(0,instance_1.commonApex)(a);if(!n)return console.warn(`[gateway] instance '${o.name}': the declared hosts share no apex \u2014 set \`instance.apex\` in .lensmcp/config.json; no host is served for it.`),{decls:e.map(([r,t])=>[r,{...t,host:void 0,internalHost:!1,default:!1}])};const l=[];for(const[r,t]of e){const d={...t};if(t.host){const i=(0,instance_1.rewriteHost)(t.host,n,o.name);i||console.warn(`[gateway] instance '${o.name}': ${r}'s host ${t.host} is outside ${n} \u2014 not served for the instance.`),d.host=i}if(typeof t.internalHost=="string"){const i=(0,instance_1.rewriteHost)(t.internalHost,n,o.name);i||console.warn(`[gateway] instance '${o.name}': ${r}'s internal host ${t.internalHost} is outside ${n} \u2014 not served for the instance.`),d.internalHost=i??!1}l.push([r,d])}return{decls:l,baseApex:n,apex:`${o.name}.${n}`}}h(rewriteInstanceDecls,"rewriteInstanceDecls"),p(rewriteInstanceDecls,"rewriteInstanceDecls");function discoverRoutes(e,o,a=os.tmpdir(),n="",l={},r=n,t){const d=r||n,i=p(c=>d?path.join(a,d,`${c}-devserver`):path.join(a,`${c}-devserver`),"sockDirFor"),f=[],g=[],_=[],H=[],S={};let y,k=[];const F=new Map;for(const[c,s]of Object.entries(o))try{const u=JSON.parse(fs.readFileSync(path.join(e,s.root,"project.json"),"utf8")),m=u.cluster??u.davnx;m&&(k.push([c,m]),F.set(c,s.root))}catch{continue}let v={};t&&({decls:k,...v}=rewriteInstanceDecls(k,t));for(const[c,s]of k){const u={root:F.get(c)};let m,j,w,R;if(s.service)s.lens&&console.warn(`[gateway] ${c}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),m={dir:i(s.service),socks:[],idx:-1,scannedAt:0},j={project:c,decl:s,root:e,wsKey:n,...d!==n?{sockNs:d}:{},pool:m,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},g.push(j);else if(s.lens){const x=s.port??lensPortFromUpstream(s.upstream)??5173;w=`http://localhost:${x}`,R={project:c,...n?{workspaceKey:n}:{},workspaceRoot:e,projectRoot:path.join(e,u.root),port:x,...l[c]?.length?{sourceRoots:l[c]}:{}},_.push(R)}else if(w=s.upstream??(s.port?`http://localhost:${s.port}`:void 0),!w){console.warn(`[gateway] ${c}: no service/upstream/port \u2014 skipped.`);continue}const b={...n?{wsKey:n}:{},project:c,prependPrefix:s.prependPrefix,target:w,pool:m,svc:j,prefix:s.path,auth:s.auth,lens:R};if(s.host&&(f.push({...b,host:s.host}),H.push(s.host),S[c]={host:s.host},m&&s.internalHost!==!1)){const x=typeof s.internalHost=="string"?s.internalHost:`internal.${s.host}`;f.push({...b,host:x,internal:!0}),S[c]={host:s.host,internalHost:x}}s.default&&(y={...b,prefix:void 0})}y&&(y.apex=v.apex??(0,scope_1.baseDomainOf)(H),f.push(y));let $;if(t&&v.apex&&v.baseApex){const c={name:t.name,apex:v.apex,baseApex:v.baseApex},s=(0,instance_1.instanceEnv)(e,(0,instance_1.instanceBuiltinEnv)(c,S));$={...c,baseKey:t.baseKey,env:s,...t.frozen?{frozen:!0}:{}};for(const u of g)u.instanceEnv=s,t.frozen&&(u.frozen=!0);for(const u of _)u.env=s,t.frozen&&(u.frozen=!0)}return(0,manifest_1.sortBySpecificity)(f),{routes:f,services:g,...$?{instance:$}:{}}}h(discoverRoutes,"discoverRoutes"),p(discoverRoutes,"discoverRoutes");function projectSourceRoots(e,o,a={}){const n={};for(const l of Object.keys(o)){const r=new Set,t=p(i=>{if(!r.has(i)){r.add(i);for(const f of a[i]??[])t(f)}},"visit");t(l);const d=[...r].map(i=>o[i]?.root).filter(i=>typeof i=="string").map(i=>path.resolve(e,i)).filter(i=>{const f=path.relative(e,i);return f===""||f!==".."&&!f.startsWith(`..${path.sep}`)&&!path.isAbsolute(f)});n[l]=[...new Set(d)]}return n}h(projectSourceRoots,"projectSourceRoots"),p(projectSourceRoots,"projectSourceRoots");
1
+ "use strict";var A=Object.defineProperty;var h=(e,s)=>A(e,"name",{value:s,configurable:!0});var I=Object.defineProperty,p=h((e,s)=>I(e,"name",{value:s,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.RETIRING_SUFFIX=void 0,exports.stopRoutingTo=stopRoutingTo,exports.pickSock=pickSock,exports.lensPortFromUpstream=lensPortFromUpstream,exports.rewriteInstanceDecls=rewriteInstanceDecls,exports.discoverRoutes=discoverRoutes,exports.projectSourceRoots=projectSourceRoots;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),manifest_1=require("../manifest"),pod_sock_1=require("../../../pod-sock"),instance_1=require("./instance"),scope_1=require("./scope");exports.RETIRING_SUFFIX=".retiring";function sockIdentity(e){try{const s=fs.statSync(e);return`${s.dev}:${s.ino}:${s.ctimeMs}`}catch{return}}h(sockIdentity,"sockIdentity"),p(sockIdentity,"sockIdentity");function stopRoutingTo(e,s=()=>!0){let a;try{a=fs.readdirSync(e.dir)}catch{a=[]}const r=a.filter(t=>(0,pod_sock_1.isPodSockName)(t)&&s(t)).map(t=>path.join(e.dir,t));if(r.length===0)return r;const l=e.retired??=new Map;for(const t of r){const d=sockIdentity(t);d!==void 0&&l.set(t,d)}const n=new Set(r);return e.socks=e.socks.filter(t=>!n.has(t)),e.idx=-1,r}h(stopRoutingTo,"stopRoutingTo"),p(stopRoutingTo,"stopRoutingTo");function stillRetired(e,s){const a=e.retired?.get(s);return a===void 0?!1:sockIdentity(s)===a?!0:(e.retired?.delete(s),!1)}h(stillRetired,"stillRetired"),p(stillRetired,"stillRetired");function pickSock(e,s=1500){const a=Date.now();if(a-e.scannedAt>s){try{const r=fs.readdirSync(e.dir),l=r.filter(t=>(0,pod_sock_1.isPodSockName)(t)&&!stillRetired(e,path.join(e.dir,t)));if(e.retired)for(const t of[...e.retired.keys()])r.includes(path.basename(t))||e.retired.delete(t);const n=l.filter(t=>!r.includes(t+exports.RETIRING_SUFFIX));e.socks=(n.length>0?n:l).map(t=>path.join(e.dir,t))}catch{e.socks=[]}e.scannedAt=a}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}h(pickSock,"pickSock"),p(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const s=/:(\d+)/.exec(e);return s?Number(s[1]):void 0}h(lensPortFromUpstream,"lensPortFromUpstream"),p(lensPortFromUpstream,"lensPortFromUpstream");function rewriteInstanceDecls(e,s){const a=e.flatMap(([,n])=>[...n.host?[n.host]:[],...typeof n.internalHost=="string"?[n.internalHost]:[]]),r=s.apex??(0,instance_1.commonApex)(a);if(!r)return console.warn(`[gateway] instance '${s.name}': the declared hosts share no apex \u2014 set \`instance.apex\` in .lensmcp/config.json; no host is served for it.`),{decls:e.map(([n,t])=>[n,{...t,host:void 0,internalHost:!1,default:!1}])};const l=[];for(const[n,t]of e){const d={...t};if(t.host){const c=(0,instance_1.rewriteHost)(t.host,r,s.name);c||console.warn(`[gateway] instance '${s.name}': ${n}'s host ${t.host} is outside ${r} \u2014 not served for the instance.`),d.host=c}if(typeof t.internalHost=="string"){const c=(0,instance_1.rewriteHost)(t.internalHost,r,s.name);c||console.warn(`[gateway] instance '${s.name}': ${n}'s internal host ${t.internalHost} is outside ${r} \u2014 not served for the instance.`),d.internalHost=c??!1}l.push([n,d])}return{decls:l,baseApex:r,apex:`${s.name}.${r}`}}h(rewriteInstanceDecls,"rewriteInstanceDecls"),p(rewriteInstanceDecls,"rewriteInstanceDecls");function discoverRoutes(e,s,a=os.tmpdir(),r="",l={},n=r,t){const d=n||r,c=p(i=>d?path.join(a,d,`${i}-devserver`):path.join(a,`${i}-devserver`),"sockDirFor"),u=[],g=[],_=[],H=[],S={};let y,w=[];const F=new Map;for(const[i,o]of Object.entries(s))try{const f=JSON.parse(fs.readFileSync(path.join(e,o.root,"project.json"),"utf8")),m=f.cluster??f.davnx;m&&(w.push([i,m]),F.set(i,o.root))}catch{continue}let v={};t&&({decls:w,...v}=rewriteInstanceDecls(w,t));for(const[i,o]of w){const f={root:F.get(i)};let m,j,k,R;if(!o.service&&o.sources!==void 0&&console.warn(`[gateway] ${i}: cluster.sources is read from pod (service) decls only \u2014 ignored.`),o.service)o.lens&&console.warn(`[gateway] ${i}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),m={dir:c(o.service),socks:[],idx:-1,scannedAt:0},j={project:i,decl:o,root:e,wsKey:r,...d!==r?{sockNs:d}:{},pool:m,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},g.push(j);else if(o.lens){const x=o.port??lensPortFromUpstream(o.upstream)??5173;k=`http://localhost:${x}`,R={project:i,...r?{workspaceKey:r}:{},workspaceRoot:e,projectRoot:path.join(e,f.root),port:x,...l[i]?.length?{sourceRoots:l[i]}:{}},_.push(R)}else if(k=o.upstream??(o.port?`http://localhost:${o.port}`:void 0),!k){console.warn(`[gateway] ${i}: no service/upstream/port \u2014 skipped.`);continue}const b={...r?{wsKey:r}:{},project:i,prependPrefix:o.prependPrefix,target:k,pool:m,svc:j,prefix:o.path,auth:o.auth,lens:R};if(o.host&&(u.push({...b,host:o.host}),H.push(o.host),S[i]={host:o.host},m&&o.internalHost!==!1)){const x=typeof o.internalHost=="string"?o.internalHost:`internal.${o.host}`;u.push({...b,host:x,internal:!0}),S[i]={host:o.host,internalHost:x}}o.default&&(y={...b,prefix:void 0})}y&&(y.apex=v.apex??(0,scope_1.baseDomainOf)(H),u.push(y));let $;if(t&&v.apex&&v.baseApex){const i={name:t.name,apex:v.apex,baseApex:v.baseApex},o=(0,instance_1.instanceEnv)(e,(0,instance_1.instanceBuiltinEnv)(i,S));$={...i,baseKey:t.baseKey,env:o,...t.frozen?{frozen:!0}:{}};for(const f of g)f.instanceEnv=o,t.frozen&&(f.frozen=!0);for(const f of _)f.env=o,t.frozen&&(f.frozen=!0)}return(0,manifest_1.sortBySpecificity)(u),{routes:u,services:g,...$?{instance:$}:{}}}h(discoverRoutes,"discoverRoutes"),p(discoverRoutes,"discoverRoutes");function projectSourceRoots(e,s,a={}){const r={};for(const l of Object.keys(s)){const n=new Set,t=p(c=>{if(!n.has(c)){n.add(c);for(const u of a[c]??[])t(u)}},"visit");t(l);const d=[...n].map(c=>s[c]?.root).filter(c=>typeof c=="string").map(c=>path.resolve(e,c)).filter(c=>{const u=path.relative(e,c);return u===""||u!==".."&&!u.startsWith(`..${path.sep}`)&&!path.isAbsolute(u)});r[l]=[...new Set(d)]}return r}h(projectSourceRoots,"projectSourceRoots"),p(projectSourceRoots,"projectSourceRoots");
@@ -1 +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.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";
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"),act_1=require("./act");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",act_1.ACT_HEADER],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),(0,act_1.setActTicketAside)(e);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";
@@ -1,3 +1,3 @@
1
- "use strict";var Ee=Object.defineProperty;var A=(s,r)=>Ee(s,"name",{value:r,configurable:!0});var Se=Object.defineProperty,n=A((s,r)=>Se(s,"name",{value:r,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.retirePreviousWorkspace=retirePreviousWorkspace,exports.resolveMaxConnections=resolveMaxConnections,exports.h2BudgetFromEnv=h2BudgetFromEnv,exports.createH2Governor=createH2Governor,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),control_1=require("./control"),workspace_middleware_1=require("./workspace-middleware"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),client_ip_1=require("../client-ip"),workspace_log_1=require("./workspace-log"),own_log_1=require("./own-log"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),process_names_1=require("./process-names"),project_graph_store_1=require("./project-graph-store"),serve_launch_1=require("./serve-launch"),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),types_1=require("./types");function insecureLocalFetch(s,r){return new Promise((l,h)=>{const a=(s.startsWith("https:")?require("node:https"):require("node:http")).request(s,{method:"GET",headers:r?.headers,rejectUnauthorized:!1},y=>{const p=[];y.on("data",S=>p.push(S)),y.on("end",()=>{const S=y.statusCode??0,u=Buffer.concat(p).toString("utf8");l({ok:S>=200&&S<300,status:S,json:n(async()=>JSON.parse(u),"json")})})});a.on("error",h),a.end()})}A(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(s){const r=process.env.LENSMCP_GW_JWKS_URL,l=process.env.LENSMCP_GW_JWT_ISS,h=new Map;let a;const y=n((p,S)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${p}${S}`);const u=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:p,...l?{issuer:l}:{},fetchImpl:insecureLocalFetch});return u.refresh().catch(o=>console.warn("[gateway] edge JWKS warm failed (will retry):",o.message)),u},"make");return{for(p){if(r)return a??=y(r,"");const S=p.split(":")[0].toLowerCase();let u;for(const d of s()){const _=d.host;if(typeof _!="string"||!/^auth\./.test(_)||_.startsWith("internal."))continue;const m=_.slice(5);(S===m||S.endsWith(`.${m}`))&&(!u||m.length>u.length)&&(u=m)}if(!u)return;let o=h.get(u);return o||(o=y(`https://auth.${u}/jwks.json`,` (auto-derived for apex ${u})`),h.set(u,o)),o},stop(){a?.stop();for(const p of h.values())p.stop()}}}A(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(s,r){return s&&exports.BENIGN_SOCKET_ERRNOS.has(s)?{status:null,report:!1,cause:"client-abort"}:s==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:s==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:s==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:s?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:s??r??"unknown"}}A(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(s=1e4){const r=new Map;return l=>{const h=Date.now(),a=r.get(l)??0;return h-a<s?!1:(r.set(l,h),!0)}}A(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(s){s.on("error",r=>{exports.BENIGN_SOCKET_ERRNOS.has(r.code??"")||console.warn(`[gateway] inbound socket error (${r.code??r.message}) \u2014 dropping this connection only`),s.destroy()})}A(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(s,r,l){const h=s.get(r);if(!h)return!1;s.delete(r),l==="re-registered"&&console.log(`[gateway] workspace '${r}' re-registered \u2014 retiring the children of its previous registration first.`);try{h()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${r}' (${l}): ${a.message}`)}return!0}A(retirePreviousWorkspace,"retirePreviousWorkspace"),n(retirePreviousWorkspace,"retirePreviousWorkspace"),exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=512,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=64,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=4096,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=3e5;function parseBudget(s,r){if(s===void 0||s.trim()==="")return r;const l=Number(s);return!Number.isFinite(l)||l<0?r:Math.floor(l)}A(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(s){return parseBudget(s,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}A(resolveMaxConnections,"resolveMaxConnections"),n(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(s=process.env){return{maxSessions:parseBudget(s.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(s.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(s.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}A(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(s,r){const{NGHTTP2_ENHANCE_YOUR_CALM:l,NGHTTP2_REFUSED_STREAM:h}=require("node:http2").constants,a=new Map;let y=0;const p=n(o=>{o.idle&&(clearTimeout(o.idle),o.idle=void 0)},"disarm"),S=n((o,d)=>{p(d),!(s.idleSessionMs<=0||d.open>0)&&(d.idle=setTimeout(()=>{o.destroyed||o.closed||(r("session-reclaimed",{idleMs:s.idleSessionMs,sessions:a.size}),a.delete(o),o.close())},s.idleSessionMs),d.idle.unref?.())},"arm"),u=n(()=>{for(const[o,d]of a)if(!(d.open>0||o.destroyed))return p(d),a.delete(o),r("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:s.maxSessions}),o.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(o){if(s.maxSessions>0&&a.size>=s.maxSessions&&!u()){r("session-refused",{sessions:a.size,maxSessions:s.maxSessions,streams:y});try{o.goaway(l)}catch{}try{o.close()}catch{}return!1}const d={open:0};return a.set(o,d),S(o,d),o.once("close",()=>{p(d),a.delete(o)}),!0},admitStream(o,d){const _=a.get(o);if(!_)return!0;if(s.maxStreams>0&&y>=s.maxStreams){r("stream-refused",{streams:y,maxStreams:s.maxStreams,sessions:a.size});try{d.close(h)}catch{}return!1}return y+=1,_.open+=1,p(_),d.once("close",()=>{y=Math.max(0,y-1),_.open=Math.max(0,_.open-1),_.open===0&&a.has(o)&&S(o,_)}),!0},dispose(){for(const o of a.values())p(o);a.clear()}}}A(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(s,r){const l=s.https!==!1,h=s.ports?.length?s.ports:[443],a=s.scanTtlMs??1500,y=s.startupTimeoutMs??12e4,p=s.sweepMs??1e4,S=s.trafficFlushMs??5e3;let u=!1;const o=process.env.LENSMCP_EVENT_FILE??path.join(r.root,".lensmcp","events.jsonl"),d=(0,own_log_1.startOwnLogRotation)(r.root),_=(0,scope_1.readLensScope)(r.root),m=_.key,x=_.sockNs!==m?_.sockNs:void 0,B=r.projectsConfigurations?.projects??{},X=(0,discovery_1.projectSourceRoots)(r.root,B,r.projectDependencies),{routes:$,services:N,instance:J}=(0,discovery_1.discoverRoutes)(r.root,B,void 0,m,X,x,_.instance);if($.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const G=new route_registry_1.RouteRegistry;G.register({wsKey:m,...x?{sockNs:x}:{},root:r.root,routes:$,services:N,...J?{instance:J}:{}});const R=[];let V=n(()=>{},"refreshCert");const z=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,G),V()},"rebuildRoutes");z();const{serviceKeys:oe,keyToProject:ne,keyToWorkspace:ae}=(0,service_keys_1.loadServiceKeys)(r.root,N,m),ie=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...s.trustProxyHops!==void 0?{trustProxyHops:s.trustProxyHops}:{},...s.clientIpHeader?{clientIpHeader:s.clientIpHeader}:{},...s.clientIpTrustedProxies?{clientIpTrustedProxies:s.clientIpTrustedProxies}:{}}),w={root:r.root,wsKey:m,...x?{sockNs:x}:{},eventFile:o,routes:R,registry:G,rebuildRoutes:z,services:N,serviceKeys:oe,keyToProject:ne,keyToWorkspace:ae,clientIp:ie,bodyStallMs:s.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:y,sweepMs:p,trafficFlushMs:S,https:l,projectRoots:Object.fromEntries(Object.entries(r.projectsConfigurations?.projects??{}).map(([e,t])=>[e,path.resolve(r.root,t.root)])),projectSourceRoots:X,...r.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(r.projectGraphStore)}:{},...s.sharedCompiler!==void 0?{sharedCompiler:s.sharedCompiler}:{},stopped:n(()=>u,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(N,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!r.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,s.sharedCompiler),label:m||path.basename(r.root),lens:{key:m,dashboard:s.dashboard!==!1,mcp:s.mcp!==!1&&$.some(e=>!!e.lens)}}));const f=(0,observability_1.createObservability)(o,{trafficFlushMs:S}),ce=createClientErrorLogGate(),k=h2BudgetFromEnv(),le=createClientErrorLogGate(3e4),q=createH2Governor(k,(e,t)=>{le(e)&&console.warn(`[gateway] h2 aggregate budget: ${e} (${JSON.stringify(t)}). These are the FRONT DOOR's ceilings, not an upstream's \u2014 raise/disable with LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS / LENSMCP_MAX_CONNECTIONS (0 = unbounded).`),f.emit(e==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${e}`,`gateway-h2-budget:${e}`,{kind:"h2-aggregate-budget",event:e,project:"gateway",...t})}),M=(0,lifecycle_1.createServiceLayer)(w,f),O=(0,lens_children_1.createLensChildren)(w,f,s),j=new Map;let W;if(s.middleware){const e=require(path.resolve(r.root,s.middleware));W=e.default??e}const D=(0,workspace_middleware_1.createWorkspaceMiddleware)({ownerKey:m,...W?{owner:W}:{},policy:(0,workspace_middleware_1.guestMiddlewarePolicy)()});let L;try{L=(0,control_1.startControlServer)({rt:w,killService:n((e,t)=>M.killSpawned(e,t),"killService"),restartService:n((e,t)=>M.restartService(e,t),"restartService"),onRegister:n(async e=>{retirePreviousWorkspace(j,e.wsKey,"re-registered"),D.set(e.wsKey,e.root,e.middleware);const t=await O.hostWorkspaceDashboard(e),T=await O.hostWorkspaceApps(e);j.set(e.wsKey,()=>{try{t?.()}finally{T()}})},"onRegister"),onUnregister:n(e=>{retirePreviousWorkspace(j,e.wsKey,"unregistered"),D.drop(e.wsKey),M.stopBuildHost(e.root)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const b=(0,proxy_1.createProxy)(w,f,M),Q=buildEdgeVerifierPool(()=>w.routes),Z=(0,auth_1.createEdgeAuth)(w,f,Q),ue=n(e=>{const t=e.route?.wsKey,T=D.for(t);if(T)try{const i=t?w.registry.get(t):void 0;T(e.req,{...t?{wsKey:t}:{},...i?{root:i.root}:{},env:i?.instance?.env??{}})}catch(i){f.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:i.message}),e.route&&f.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${i.message}`)}},"builtinMiddleware"),ee=s.hooks??{},se=(0,hooks_1.createHooks)({...ee,afterAuth:[ue,...ee.afterAuth??[]]}),re=(0,handler_1.createHandler)({rt:w,obs:f,auth:Z,proxy:b,hooks:se}),te=(0,upgrade_1.createUpgrade)({rt:w,auth:Z,proxy:b,hooks:se,obs:f}),de=f.startEdgeFlusher(),pe=M.startSweeper();await O.bootSingletonsAndApps();for(const e of N)e.decl.eager&&M.ensureUp(e);let P,F,I;if(l){const e=require("../../../basic-ssl"),t=(0,service_keys_1.gatewayCacheDir)(r.root);I=n(()=>e.getCertificateSync(t,"gateway",w.routes.map(T=>T.host).filter(T=>!!T)),"mintCert"),P=I(),F=e.caCertPath()}const U=[],H=[],K=s.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const e of h){const t=P?require("node:http2").createSecureServer({key:P,cert:P,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:s.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:s.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(i,E)=>re(i,E)):http.createServer(re);t.on("upgrade",te),t.on("error",i=>console.error(`[gateway] port ${e}: ${i.code}`)),t.on("connection",guardInboundSocket),t.on("secureConnection",guardInboundSocket),t.on("tlsClientError",(i,E)=>E.destroy()),t.on("clientError",(i,E)=>{const c=i,g=classifyClientError(c.code,c.message);if(g.report&&ce(g.cause)){const v=E.remoteAddress??"unknown",C=g.status===null?"dropped":String(g.status);console.warn(`[gateway] inbound H1 clientError from ${v}: ${g.cause} (${c.code??c.message}${c.bytesParsed!==void 0?`, bytesParsed=${c.bytesParsed}`:""}) \u2192 ${C}. This status is the GATEWAY's, not the upstream service's.`),f.emit("warning",`inbound clientError: ${g.cause} \u2192 ${C}`,`gateway-client-error:${g.cause}`,{kind:"gateway-client-error",cause:g.cause,code:c.code??null,status:g.status,peer:v,bytesParsed:c.bytesParsed??null})}g.status!==null&&E.writable&&E.end(`HTTP/1.1 ${g.status} ${CLIENT_ERROR_REASON[g.status]}\r
1
+ "use strict";var ge=Object.defineProperty;var A=(s,r)=>ge(s,"name",{value:r,configurable:!0});var he=Object.defineProperty,n=A((s,r)=>he(s,"name",{value:r,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.retirePreviousWorkspace=retirePreviousWorkspace,exports.resolveMaxConnections=resolveMaxConnections,exports.h2BudgetFromEnv=h2BudgetFromEnv,exports.createH2Governor=createH2Governor,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),act_1=require("./act"),control_1=require("./control"),workspace_middleware_1=require("./workspace-middleware"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),source_poller_1=require("./source-poller"),client_ip_1=require("../client-ip"),workspace_log_1=require("./workspace-log"),own_log_1=require("./own-log"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),process_names_1=require("./process-names"),project_graph_store_1=require("./project-graph-store"),serve_launch_1=require("./serve-launch"),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),types_1=require("./types");function insecureLocalFetch(s,r){return new Promise((u,m)=>{const a=(s.startsWith("https:")?require("node:https"):require("node:http")).request(s,{method:"GET",headers:r?.headers,rejectUnauthorized:!1},w=>{const _=[];w.on("data",g=>_.push(g)),w.on("end",()=>{const g=w.statusCode??0,l=Buffer.concat(_).toString("utf8");u({ok:g>=200&&g<300,status:g,json:n(async()=>JSON.parse(l),"json")})})});a.on("error",m),a.end()})}A(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(s){const r=process.env.LENSMCP_GW_JWKS_URL,u=process.env.LENSMCP_GW_JWT_ISS,m=new Map;let a;const w=n((_,g)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${_}${g}`);const l=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:_,...u?{issuer:u}:{},fetchImpl:insecureLocalFetch});return l.refresh().catch(o=>console.warn("[gateway] edge JWKS warm failed (will retry):",o.message)),l},"make");return{for(_){if(r)return a??=w(r,"");const g=_.split(":")[0].toLowerCase();let l;for(const d of s()){const S=d.host;if(typeof S!="string"||!/^auth\./.test(S)||S.startsWith("internal."))continue;const T=S.slice(5);(g===T||g.endsWith(`.${T}`))&&(!l||T.length>l.length)&&(l=T)}if(!l)return;let o=m.get(l);return o||(o=w(`https://auth.${l}/jwks.json`,` (auto-derived for apex ${l})`),m.set(l,o)),o},stop(){a?.stop();for(const _ of m.values())_.stop()}}}A(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(s,r){return s&&exports.BENIGN_SOCKET_ERRNOS.has(s)?{status:null,report:!1,cause:"client-abort"}:s==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:s==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:s==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:s?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:s??r??"unknown"}}A(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(s=1e4){const r=new Map;return u=>{const m=Date.now(),a=r.get(u)??0;return m-a<s?!1:(r.set(u,m),!0)}}A(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(s){s.on("error",r=>{exports.BENIGN_SOCKET_ERRNOS.has(r.code??"")||console.warn(`[gateway] inbound socket error (${r.code??r.message}) \u2014 dropping this connection only`),s.destroy()})}A(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(s,r,u){const m=s.get(r);if(!m)return!1;s.delete(r),u==="re-registered"&&console.log(`[gateway] workspace '${r}' re-registered \u2014 retiring the children of its previous registration first.`);try{m()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${r}' (${u}): ${a.message}`)}return!0}A(retirePreviousWorkspace,"retirePreviousWorkspace"),n(retirePreviousWorkspace,"retirePreviousWorkspace"),exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=512,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=64,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=4096,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=3e5;function parseBudget(s,r){if(s===void 0||s.trim()==="")return r;const u=Number(s);return!Number.isFinite(u)||u<0?r:Math.floor(u)}A(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(s){return parseBudget(s,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}A(resolveMaxConnections,"resolveMaxConnections"),n(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(s=process.env){return{maxSessions:parseBudget(s.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(s.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(s.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}A(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(s,r){const{NGHTTP2_ENHANCE_YOUR_CALM:u,NGHTTP2_REFUSED_STREAM:m}=require("node:http2").constants,a=new Map;let w=0;const _=n(o=>{o.idle&&(clearTimeout(o.idle),o.idle=void 0)},"disarm"),g=n((o,d)=>{_(d),!(s.idleSessionMs<=0||d.open>0)&&(d.idle=setTimeout(()=>{o.destroyed||o.closed||(r("session-reclaimed",{idleMs:s.idleSessionMs,sessions:a.size}),a.delete(o),o.close())},s.idleSessionMs),d.idle.unref?.())},"arm"),l=n(()=>{for(const[o,d]of a)if(!(d.open>0||o.destroyed))return _(d),a.delete(o),r("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:s.maxSessions}),o.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(o){if(s.maxSessions>0&&a.size>=s.maxSessions&&!l()){r("session-refused",{sessions:a.size,maxSessions:s.maxSessions,streams:w});try{o.goaway(u)}catch{}try{o.close()}catch{}return!1}const d={open:0};return a.set(o,d),g(o,d),o.once("close",()=>{_(d),a.delete(o)}),!0},admitStream(o,d){const S=a.get(o);if(!S)return!0;if(s.maxStreams>0&&w>=s.maxStreams){r("stream-refused",{streams:w,maxStreams:s.maxStreams,sessions:a.size});try{d.close(m)}catch{}return!1}return w+=1,S.open+=1,_(S),d.once("close",()=>{w=Math.max(0,w-1),S.open=Math.max(0,S.open-1),S.open===0&&a.has(o)&&g(o,S)}),!0},dispose(){for(const o of a.values())_(o);a.clear()}}}A(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(s,r){const u=s.https!==!1,m=s.ports?.length?s.ports:[443],a=s.scanTtlMs??1500,w=s.startupTimeoutMs??12e4,_=s.sweepMs??1e4,g=s.trafficFlushMs??5e3;let l=!1;const o=process.env.LENSMCP_EVENT_FILE??path.join(r.root,".lensmcp","events.jsonl"),d=(0,own_log_1.startOwnLogRotation)(r.root),S=(0,scope_1.readLensScope)(r.root),T=S.key,x=S.sockNs!==T?S.sockNs:void 0,X=r.projectsConfigurations?.projects??{},J=(0,discovery_1.projectSourceRoots)(r.root,X,r.projectDependencies),{routes:G,services:N,instance:V}=(0,discovery_1.discoverRoutes)(r.root,X,void 0,T,J,x,S.instance);if(G.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const j=new route_registry_1.RouteRegistry;j.register({wsKey:T,...x?{sockNs:x}:{},root:r.root,routes:G,services:N,...V?{instance:V}:{}});const R=[];let z=n(()=>{},"refreshCert");const Q=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,j),z()},"rebuildRoutes");Q();const{serviceKeys:ae,keyToProject:ie,keyToWorkspace:ce}=(0,service_keys_1.loadServiceKeys)(r.root,N,T),le=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...s.trustProxyHops!==void 0?{trustProxyHops:s.trustProxyHops}:{},...s.clientIpHeader?{clientIpHeader:s.clientIpHeader}:{},...s.clientIpTrustedProxies?{clientIpTrustedProxies:s.clientIpTrustedProxies}:{}}),E={root:r.root,wsKey:T,...x?{sockNs:x}:{},eventFile:o,routes:R,registry:j,rebuildRoutes:Q,services:N,serviceKeys:ae,keyToProject:ie,keyToWorkspace:ce,clientIp:le,bodyStallMs:s.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:w,sweepMs:_,trafficFlushMs:g,https:u,projectRoots:Object.fromEntries(Object.entries(r.projectsConfigurations?.projects??{}).map(([e,t])=>[e,path.resolve(r.root,t.root)])),projectSourceRoots:J,...r.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(r.projectGraphStore)}:{},...s.sharedCompiler!==void 0?{sharedCompiler:s.sharedCompiler}:{},stopped:n(()=>l,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(N,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!r.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,s.sharedCompiler),label:T||path.basename(r.root),lens:{key:T,dashboard:s.dashboard!==!1,mcp:s.mcp!==!1&&G.some(e=>!!e.lens)}}));const f=(0,observability_1.createObservability)(o,{trafficFlushMs:g}),ue=createClientErrorLogGate(),k=h2BudgetFromEnv(),de=createClientErrorLogGate(3e4),q=createH2Governor(k,(e,t)=>{de(e)&&console.warn(`[gateway] h2 aggregate budget: ${e} (${JSON.stringify(t)}). These are the FRONT DOOR's ceilings, not an upstream's \u2014 raise/disable with LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS / LENSMCP_MAX_CONNECTIONS (0 = unbounded).`),f.emit(e==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${e}`,`gateway-h2-budget:${e}`,{kind:"h2-aggregate-budget",event:e,project:"gateway",...t})}),v=(0,lifecycle_1.createServiceLayer)(E,f),L=(0,lens_children_1.createLensChildren)(E,f,s),W=new Map;let D;if(s.middleware){const e=require(path.resolve(r.root,s.middleware));D=e.default??e}const K=(0,workspace_middleware_1.createWorkspaceMiddleware)({ownerKey:T,...D?{owner:D}:{},policy:(0,workspace_middleware_1.guestMiddlewarePolicy)()}),O=process.env.LENSMCP_GW_ACT==="0"?void 0:(0,act_1.createActStore)({dir:path.join(path.dirname((0,control_1.controlSocketPath)()),"identities")});let b;try{b=(0,control_1.startControlServer)({rt:E,killService:n((e,t)=>v.killSpawned(e,t),"killService"),restartService:n((e,t)=>v.restartService(e,t),"restartService"),...O?{act:O}:{},onRegister:n(async e=>{retirePreviousWorkspace(W,e.wsKey,"re-registered"),K.set(e.wsKey,e.root,e.middleware);const t=await L.hostWorkspaceDashboard(e),h=await L.hostWorkspaceApps(e);W.set(e.wsKey,()=>{try{t?.()}finally{h()}})},"onRegister"),onUnregister:n(e=>{retirePreviousWorkspace(W,e.wsKey,"unregistered"),K.drop(e.wsKey),v.stopBuildHost(e.root)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const I=(0,proxy_1.createProxy)(E,f,v),Z=buildEdgeVerifierPool(()=>E.routes),ee=(0,auth_1.createEdgeAuth)(E,f,Z,O),pe=n(e=>{const t=e.route?.wsKey,h=K.for(t);if(h)try{const i=t?E.registry.get(t):void 0;h(e.req,{...t?{wsKey:t}:{},...i?{root:i.root}:{},env:i?.instance?.env??{}})}catch(i){f.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:i.message}),e.route&&f.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${i.message}`)}},"builtinMiddleware"),se=s.hooks??{},re=(0,hooks_1.createHooks)({...se,afterAuth:[pe,...se.afterAuth??[]]}),te=(0,handler_1.createHandler)({rt:E,obs:f,auth:ee,proxy:I,hooks:re}),oe=(0,upgrade_1.createUpgrade)({rt:E,auth:ee,proxy:I,hooks:re,obs:f}),_e=f.startEdgeFlusher(),Se=v.startSweeper(),ne=(0,source_poller_1.createSourcePoller)({services:n(()=>E.registry.allServices(),"services"),serviceKey:n(e=>E.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],"serviceKey"),emit:n((e,t,h,i,p)=>f.emit(t,h,i,{wsKey:e.wsKey,...p},void 0,e.wsKey===E.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl")),"emit"),scanTtlMs:a,stopped:n(()=>l,"stopped")});(0,source_poller_1.sourcesEnabled)()&&ne.start(),await L.bootSingletonsAndApps();for(const e of N)e.decl.eager&&v.ensureUp(e);let P,F,H;if(u){const e=require("../../../basic-ssl"),t=(0,service_keys_1.gatewayCacheDir)(r.root);H=n(()=>e.getCertificateSync(t,"gateway",E.routes.map(h=>h.host).filter(h=>!!h)),"mintCert"),P=H(),F=e.caCertPath()}const U=[],$=[],Y=s.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const e of m){const t=P?require("node:http2").createSecureServer({key:P,cert:P,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:s.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:s.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(i,p)=>te(i,p)):http.createServer(te);t.on("upgrade",oe),t.on("error",i=>console.error(`[gateway] port ${e}: ${i.code}`)),t.on("connection",guardInboundSocket),t.on("secureConnection",guardInboundSocket),t.on("tlsClientError",(i,p)=>p.destroy()),t.on("clientError",(i,p)=>{const c=i,y=classifyClientError(c.code,c.message);if(y.report&&ue(y.cause)){const C=p.remoteAddress??"unknown",M=y.status===null?"dropped":String(y.status);console.warn(`[gateway] inbound H1 clientError from ${C}: ${y.cause} (${c.code??c.message}${c.bytesParsed!==void 0?`, bytesParsed=${c.bytesParsed}`:""}) \u2192 ${M}. This status is the GATEWAY's, not the upstream service's.`),f.emit("warning",`inbound clientError: ${y.cause} \u2192 ${M}`,`gateway-client-error:${y.cause}`,{kind:"gateway-client-error",cause:y.cause,code:c.code??null,status:y.status,peer:C,bytesParsed:c.bytesParsed??null})}y.status!==null&&p.writable&&p.end(`HTTP/1.1 ${y.status} ${CLIENT_ERROR_REASON[y.status]}\r
2
2
  \r
3
- `),E.destroy()}),t.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,t.headersTimeout=s.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,K>0&&(t.maxConnections=K);const T=s.connectionsCheckingIntervalMs??types_1.GATEWAY_CONN_CHECK_MS;if(T>0&&(t.connectionsCheckingInterval=T),P!=null){const i=t,{NGHTTP2_CANCEL:E}=require("node:http2").constants;i.on("session",c=>{if(!q.admitSession(c)){c.on("error",()=>{});return}let g=0,v=0;c.on("stream",C=>{q.admitStream(c,C)&&C.once("close",()=>{c.destroyed?v+=1:C.rstCode===E&&(g+=1)})}),c.once("close",()=>{v!==0&&(console.warn(`[gateway] h2 session closed with ${v} stream(s) in flight after ${g} client reset(s) \u2014 every in-flight fetch on that connection failed at once (ERR_HTTP2_PROTOCOL_ERROR in the browser). Reset budget: LENSMCP_H2_RESET_BURST/LENSMCP_H2_RESET_RATE (issues/closed/foodguard-14).`),f.emit("warning",`h2 session lost with ${v} stream(s) in flight`,"gateway-h2-session-lost",{kind:"h2-session-lost",project:"gateway",inflight:v,clientResets:g}))}),c.on("error",C=>{const _e=C.code??C.message;console.warn(`[gateway] h2 session error (${_e}) \u2014 dropping this session only`),c.destroyed||c.destroy()})}),i.on("sessionError",c=>{console.warn(`[gateway] h2 sessionError: ${c.code??c.message}`)})}await new Promise(i=>{t.listen(e,()=>{const E=t.address()?.port??e;H.push(E),console.log(`[gateway] Listening on ${P?"https":"http"}://localhost:${E}`),i()})}),U.push(t)}I&&(V=n(()=>{const e=I();for(const t of U)t.setSecureContext?.({key:e,cert:e})},"refreshCert")),console.log("[gateway] Routes:");for(const e of R){const t=e.pool?`pods@${e.pool.dir}`:e.target;console.log(`[gateway] ${e.host??"(default)"} \u2192 ${t}${e.prependPrefix?` (+${e.prependPrefix})`:""}${e.internal?" [internal: no middleware]":""} [${e.project}]`)}F&&console.log(`[gateway] CA: ${F} \u2014 one-time setup via the trust executor.`);const Y=n(e=>e>0?String(e):"unbounded","unbounded");return console.log(`[gateway] front-door budget: \u2264${Y(K)} connections, \u2264${Y(k.maxSessions)} h2 sessions (each \u2264512 MB session memory, \u2264512 streams), \u2264${Y(k.maxStreams)} open streams in total, idle h2 session reclaimed after ${k.idleSessionMs>0?`${Math.round(k.idleSessionMs/1e3)}s`:"never"}. Override: LENSMCP_MAX_CONNECTIONS / LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS (0 = unbounded).`),f.emit("info",`gateway up: ${R.length} routes on ${H.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:H,routes:R.map(e=>({host:e.host??"(default)",project:e.project,mode:e.pool?"pods":"tcp",internal:!!e.internal,prependPrefix:e.prependPrefix}))}),L&&process.env.LENSMCP_GUEST_REPLAY!=="0"&&L.replay().catch(e=>console.warn("[gateway] guest replay failed:",e.message)),{ports:H,routes:R,services:N,stop:n(async()=>{if(!u){u=!0,clearInterval(de),clearInterval(pe),f.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),Q.stop(),L?.close(),(0,workspace_registry_1.unregisterWorkspace)(m);for(const e of N)M.killSpawned(e,"gateway shutdown");M.stopRetiringTrees(),await M.stopBuildHost(),await O.reapAll(),te.closeTunnels(),await Promise.all(U.map(e=>new Promise(t=>{let T=!1;const i=n(()=>{T||(T=!0,t())},"done"),E=e;e.close(()=>i());try{E.closeIdleConnections?.()}catch{}setTimeout(()=>{try{E.closeAllConnections?.()}catch{}i()},types_1.GATEWAY_STOP_GRACE_MS).unref?.()}))),b.closeProxy(),b.destroyAgents(),q.dispose(),(0,workspace_log_1.closeWorkspaceLogs)(),d()}},"stop"),...w.lensSupervisor?{lensSupervisor:w.lensSupervisor}:{},...w.graphStores?{graphStores:w.graphStores}:{}}}A(startGateway,"startGateway"),n(startGateway,"startGateway");
3
+ `),p.destroy()}),t.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,t.headersTimeout=s.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,Y>0&&(t.maxConnections=Y);const h=s.connectionsCheckingIntervalMs??types_1.GATEWAY_CONN_CHECK_MS;if(h>0&&(t.connectionsCheckingInterval=h),P!=null){const i=t,{NGHTTP2_CANCEL:p}=require("node:http2").constants;i.on("session",c=>{if(!q.admitSession(c)){c.on("error",()=>{});return}let y=0,C=0;c.on("stream",M=>{q.admitStream(c,M)&&M.once("close",()=>{c.destroyed?C+=1:M.rstCode===p&&(y+=1)})}),c.once("close",()=>{C!==0&&(console.warn(`[gateway] h2 session closed with ${C} stream(s) in flight after ${y} client reset(s) \u2014 every in-flight fetch on that connection failed at once (ERR_HTTP2_PROTOCOL_ERROR in the browser). Reset budget: LENSMCP_H2_RESET_BURST/LENSMCP_H2_RESET_RATE (issues/closed/foodguard-14).`),f.emit("warning",`h2 session lost with ${C} stream(s) in flight`,"gateway-h2-session-lost",{kind:"h2-session-lost",project:"gateway",inflight:C,clientResets:y}))}),c.on("error",M=>{const Ee=M.code??M.message;console.warn(`[gateway] h2 session error (${Ee}) \u2014 dropping this session only`),c.destroyed||c.destroy()})}),i.on("sessionError",c=>{console.warn(`[gateway] h2 sessionError: ${c.code??c.message}`)})}await new Promise(i=>{t.listen(e,()=>{const p=t.address()?.port??e;$.push(p),console.log(`[gateway] Listening on ${P?"https":"http"}://localhost:${p}`),i()})}),U.push(t)}H&&(z=n(()=>{const e=H();for(const t of U)t.setSecureContext?.({key:e,cert:e})},"refreshCert")),console.log("[gateway] Routes:");for(const e of R){const t=e.pool?`pods@${e.pool.dir}`:e.target;console.log(`[gateway] ${e.host??"(default)"} \u2192 ${t}${e.prependPrefix?` (+${e.prependPrefix})`:""}${e.internal?" [internal: no middleware]":""} [${e.project}]`)}F&&console.log(`[gateway] CA: ${F} \u2014 one-time setup via the trust executor.`);const B=n(e=>e>0?String(e):"unbounded","unbounded");return console.log(`[gateway] front-door budget: \u2264${B(Y)} connections, \u2264${B(k.maxSessions)} h2 sessions (each \u2264512 MB session memory, \u2264512 streams), \u2264${B(k.maxStreams)} open streams in total, idle h2 session reclaimed after ${k.idleSessionMs>0?`${Math.round(k.idleSessionMs/1e3)}s`:"never"}. Override: LENSMCP_MAX_CONNECTIONS / LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS (0 = unbounded).`),f.emit("info",`gateway up: ${R.length} routes on ${$.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:$,routes:R.map(e=>({host:e.host??"(default)",project:e.project,mode:e.pool?"pods":"tcp",internal:!!e.internal,prependPrefix:e.prependPrefix}))}),b&&process.env.LENSMCP_GUEST_REPLAY!=="0"&&b.replay().catch(e=>console.warn("[gateway] guest replay failed:",e.message)),{ports:$,routes:R,services:N,stop:n(async()=>{if(!l){l=!0,clearInterval(_e),clearInterval(Se),ne.stop(),f.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),Z.stop(),b?.close(),O?.flush(),(0,workspace_registry_1.unregisterWorkspace)(T);for(const e of N)v.killSpawned(e,"gateway shutdown");v.stopRetiringTrees(),await v.stopBuildHost(),await L.reapAll(),oe.closeTunnels(),await Promise.all(U.map(e=>new Promise(t=>{let h=!1;const i=n(()=>{h||(h=!0,t())},"done"),p=e;e.close(()=>i());try{p.closeIdleConnections?.()}catch{}setTimeout(()=>{try{p.closeAllConnections?.()}catch{}i()},types_1.GATEWAY_STOP_GRACE_MS).unref?.()}))),I.closeProxy(),I.destroyAgents(),q.dispose(),(0,workspace_log_1.closeWorkspaceLogs)(),d()}},"stop"),...E.lensSupervisor?{lensSupervisor:E.lensSupervisor}:{},...E.graphStores?{graphStores:E.graphStores}:{}}}A(startGateway,"startGateway"),n(startGateway,"startGateway");
@@ -0,0 +1,216 @@
1
+ import type { ServiceCtl } from './types';
2
+ /** `LENSMCP_SOURCES=0` turns the poller off. */
3
+ export declare const sourcesEnabled: () => boolean;
4
+ /** How long one poll may take — connect, answer and body (`LENSMCP_SOURCES_TIMEOUT_MS`, default 3 s). */
5
+ export declare const sourcesTimeoutMs: () => number;
6
+ /** Polls in flight at once, across every service (`LENSMCP_SOURCES_CONCURRENCY`, default 4). */
7
+ export declare const sourcesConcurrency: () => number;
8
+ /** The closed set of kinds, and the ones this release reads. MIRROR of `SOURCE_KINDS` / `SOURCE_KINDS_READ`. */
9
+ export declare const SOURCE_KINDS: readonly ["queue", "loop", "store", "traffic"];
10
+ export declare const SOURCE_KINDS_READ: readonly ["queue"];
11
+ export type SourceKindRead = (typeof SOURCE_KINDS_READ)[number];
12
+ /** MIRROR of `QUEUE_WIRE_SCHEMA`. */
13
+ export declare const QUEUE_WIRE_SCHEMA = "lensmcp.queue/v1";
14
+ export declare const SOURCE_DEFAULT_INTERVAL_SEC = 10;
15
+ export declare const SOURCE_MIN_INTERVAL_SEC = 2;
16
+ export declare const QUEUE_DEFAULT_OLDEST_WAIT_MIN = 10;
17
+ /** A snapshot larger than this is `malformed` — read no further. */
18
+ export declare const SOURCE_MAX_BYTES: number;
19
+ /** Groups one snapshot may hold; a producer with more caps it and says `truncated: { groups }`. */
20
+ export declare const SOURCE_MAX_GROUPS = 500;
21
+ /** Sources one service may declare. */
22
+ export declare const SOURCE_MAX_PER_SERVICE = 16;
23
+ /** A service that is not running is said once, then again at this period — not at every poll. */
24
+ export declare const SOURCE_NOT_RUNNING_REPORT_MS = 60000;
25
+ /** A service's declaration is said again at this period, so a lens that started later still knows what to expect. */
26
+ export declare const SOURCE_DECLARATION_REPORT_MS: number;
27
+ /** The longest a failing source waits between polls (never shorter than its own interval). */
28
+ export declare const SOURCE_BACKOFF_MAX_MS = 60000;
29
+ /** A report is overdue — `stale` — after this many report periods (never sooner than 30 s). MIRROR of `sourceStaleAfterMs`. */
30
+ export declare const sourceStaleAfterMs: (reportEveryMs: number) => number;
31
+ /** How long to wait before polling again after `failures` failed polls in a row: the interval, then doubling, capped. */
32
+ export declare function sourceBackoffMs(intervalMs: number, failures: number): number;
33
+ export interface QueueThresholds {
34
+ /** Warn once a group's oldest pending item has waited longer than this. */
35
+ oldestWaitMin: number;
36
+ /** Accepted now; judged once the lens keeps a history of snapshots (phase 2). */
37
+ stalledMin?: number;
38
+ }
39
+ /** One `cluster.sources` entry, as the gateway polls it. */
40
+ export interface SourceDecl {
41
+ kind: SourceKindRead;
42
+ /** Names the source in the lens (`product-analysis outbox`). Unique per service. */
43
+ name: string;
44
+ /** A dev route ON THE SERVICE, sent to its pod as-is (the edge's `prependPrefix` never applies). */
45
+ path: string;
46
+ intervalSec: number;
47
+ thresholds: QueueThresholds;
48
+ }
49
+ /**
50
+ * `cluster.sources` read back, as `parseWorkerSpecs` reads `workers`: what cannot be used is left out and said in
51
+ * `problems` — a bad entry never stops the others, and never passes silently. An unknown kind is said by name; a kind
52
+ * of the closed set this release does not read yet is said as such; an entry with a key its kind does not know is
53
+ * refused whole, so a misspelt `intervalSecs` is said rather than polled at the default.
54
+ */
55
+ export declare function parseSourceDecls(raw: unknown): {
56
+ sources: SourceDecl[];
57
+ problems: string[];
58
+ };
59
+ /** One group of a queue snapshot, as the WIRE names it. */
60
+ export interface QueueWireGroup {
61
+ key: string;
62
+ labels?: Record<string, string>;
63
+ pending: number;
64
+ inFlight?: number;
65
+ failed?: number;
66
+ oldestPendingAt?: string | null;
67
+ completedInWindow?: number;
68
+ }
69
+ /**
70
+ * A queue snapshot, validated. Any departure from the schema is a REASON, and the caller reports the source `malformed`
71
+ * — never zero. Fields a newer `v1` producer adds are ignored; a wrong type on a known one is not.
72
+ */
73
+ export declare function parseQueueSnapshot(body: unknown): {
74
+ ok: true;
75
+ at: string;
76
+ groups: QueueWireGroup[];
77
+ truncated?: {
78
+ groups: number;
79
+ };
80
+ } | {
81
+ ok: false;
82
+ reason: string;
83
+ };
84
+ export type SourcePollState = 'current' | 'unreachable' | 'refused' | 'malformed' | 'not-running';
85
+ /** A group on the bus: the wire's `key` travels as `group` — the bus redacts any field NAMED `key`. */
86
+ export interface QueueGroupRaw {
87
+ group: string;
88
+ labels?: Record<string, string>;
89
+ pending: number;
90
+ inFlight?: number;
91
+ failed?: number;
92
+ oldestPendingAt?: string | null;
93
+ completedInWindow?: number;
94
+ }
95
+ export interface QueueSnapshotRaw {
96
+ at: string;
97
+ receivedAt: number;
98
+ totals: {
99
+ groups: number;
100
+ pending: number;
101
+ inFlight: number;
102
+ failed: number;
103
+ completedInWindow?: number;
104
+ };
105
+ groups: QueueGroupRaw[];
106
+ truncated?: {
107
+ groups: number;
108
+ };
109
+ }
110
+ /** A declaration on the bus. `sourceKind`, because `kind` is the event's own. */
111
+ export interface SourceDeclRaw {
112
+ sourceKind: SourceKindRead;
113
+ name: string;
114
+ path: string;
115
+ intervalSec: number;
116
+ thresholds: QueueThresholds;
117
+ }
118
+ export interface SourcePollRaw extends SourceDeclRaw {
119
+ kind: 'source-poll';
120
+ provenance: 'reported';
121
+ project: string;
122
+ state: SourcePollState;
123
+ detail?: string;
124
+ status?: number;
125
+ polledAt: number;
126
+ durationMs?: number;
127
+ reportEveryMs: number;
128
+ failures?: number;
129
+ snapshot?: QueueSnapshotRaw;
130
+ }
131
+ export interface SourceDeclarationRaw {
132
+ kind: 'source-declaration';
133
+ project: string;
134
+ sources: SourceDeclRaw[];
135
+ problems: string[];
136
+ }
137
+ /** The snapshot as the bus carries it: every group, the oldest-waiting first (then the largest); totals over them. */
138
+ export declare function keepQueueSnapshot(at: string, groups: readonly QueueWireGroup[], receivedAt: number, truncated?: {
139
+ groups: number;
140
+ }): QueueSnapshotRaw;
141
+ /** One poll, as the bus carries it. */
142
+ export declare function buildSourcePollRaw(project: string, decl: SourceDecl, poll: {
143
+ state: SourcePollState;
144
+ polledAt: number;
145
+ detail?: string;
146
+ status?: number;
147
+ durationMs?: number;
148
+ }, reportEveryMs: number, failures: number, snapshot: QueueSnapshotRaw | undefined): SourcePollRaw;
149
+ export declare function buildSourceDeclarationRaw(project: string, parsed: {
150
+ sources: readonly SourceDecl[];
151
+ problems: readonly string[];
152
+ }): SourceDeclarationRaw;
153
+ /** The event's title: what a person reads in the event list. */
154
+ export declare function sourcePollTitle(raw: SourcePollRaw): string;
155
+ export type SourceHttpOutcome = {
156
+ ok: true;
157
+ status: number;
158
+ body: string;
159
+ truncated: boolean;
160
+ } | {
161
+ ok: false;
162
+ code: string;
163
+ message: string;
164
+ };
165
+ /**
166
+ * GET `path` on the pod behind `socketPath`. Never throws and never outlives `timeoutMs` — connect, headers and body
167
+ * together. A body past `maxBytes` is cut off and said (`truncated`), not read to the end.
168
+ */
169
+ export declare function requestOverSocket(socketPath: string, path: string, headers: Record<string, string>, timeoutMs: number, maxBytes?: number): Promise<SourceHttpOutcome>;
170
+ export interface SourcePollerDeps {
171
+ /** Every service the gateway serves, of every workspace it hosts (`rt.registry.allServices()`). */
172
+ services: () => readonly ServiceCtl[];
173
+ /** The key the gateway stamps on a request it forwards to this service (`x-internal-token`). */
174
+ serviceKey: (svc: ServiceCtl) => string | undefined;
175
+ /** Put one report on the OWNING workspace's bus. */
176
+ emit: (svc: ServiceCtl, severity: 'info' | 'warning', title: string, fingerprint: string, raw: Record<string, unknown>) => void;
177
+ /** A live pod's socket, or none. Default: the gateway's own pick (`pickSock`), the socket a request would go to. */
178
+ socketOf?: (svc: ServiceCtl) => string | undefined;
179
+ request?: typeof requestOverSocket;
180
+ now?: () => number;
181
+ timeoutMs?: number;
182
+ maxConcurrent?: number;
183
+ /** Sock-dir rescan TTL for the default {@link socketOf}. */
184
+ scanTtlMs?: number;
185
+ /** Where a declaration problem is said. Default: the gateway log. */
186
+ log?: (line: string) => void;
187
+ stopped?: () => boolean;
188
+ }
189
+ /** One source as the poller holds it — and as {@link SourcePoller.snapshot} shows it. */
190
+ export interface SourceProbeView {
191
+ wsKey: string;
192
+ project: string;
193
+ name: string;
194
+ kind: SourceKindRead;
195
+ path: string;
196
+ /** The last poll's state, `stale` once its report is overdue, `never-polled` before the first. */
197
+ state: SourcePollState | 'stale' | 'never-polled';
198
+ detail?: string;
199
+ status?: number;
200
+ polledAt?: number;
201
+ failures: number;
202
+ /** When the next poll is due (backed off while it fails). */
203
+ nextAt: number;
204
+ /** The last good snapshot, kept whatever the state, with its age. */
205
+ snapshot?: QueueSnapshotRaw;
206
+ ageMs?: number;
207
+ }
208
+ export interface SourcePoller {
209
+ /** Start every poll that is due (never more than `maxConcurrent` in flight); resolves once those have settled. */
210
+ tick(): Promise<void>;
211
+ /** Tick every `everyMs` (unref'd). */
212
+ start(everyMs?: number): NodeJS.Timeout;
213
+ stop(): void;
214
+ snapshot(): SourceProbeView[];
215
+ }
216
+ export declare function createSourcePoller(deps: SourcePollerDeps): SourcePoller;
@@ -0,0 +1 @@
1
+ "use strict";var U=Object.defineProperty;var v=(e,o)=>U(e,"name",{value:o,configurable:!0});var b=Object.defineProperty,l=v((e,o)=>b(e,"name",{value:o,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sourceStaleAfterMs=exports.SOURCE_BACKOFF_MAX_MS=exports.SOURCE_DECLARATION_REPORT_MS=exports.SOURCE_NOT_RUNNING_REPORT_MS=exports.SOURCE_MAX_PER_SERVICE=exports.SOURCE_MAX_GROUPS=exports.SOURCE_MAX_BYTES=exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=exports.SOURCE_MIN_INTERVAL_SEC=exports.SOURCE_DEFAULT_INTERVAL_SEC=exports.QUEUE_WIRE_SCHEMA=exports.SOURCE_KINDS_READ=exports.SOURCE_KINDS=exports.sourcesConcurrency=exports.sourcesTimeoutMs=exports.sourcesEnabled=void 0,exports.sourceBackoffMs=sourceBackoffMs,exports.parseSourceDecls=parseSourceDecls,exports.parseQueueSnapshot=parseQueueSnapshot,exports.keepQueueSnapshot=keepQueueSnapshot,exports.buildSourcePollRaw=buildSourcePollRaw,exports.buildSourceDeclarationRaw=buildSourceDeclarationRaw,exports.sourcePollTitle=sourcePollTitle,exports.requestOverSocket=requestOverSocket,exports.createSourcePoller=createSourcePoller;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),discovery_1=require("./discovery"),readEnv=l(e=>process.env[e],"readEnv"),envInt=l((e,o,n)=>{const u=Number(readEnv(e));return Number.isInteger(u)&&u>=n?u:o},"envInt"),sourcesEnabled=l(()=>readEnv("LENSMCP_SOURCES")!=="0","sourcesEnabled");exports.sourcesEnabled=sourcesEnabled;const sourcesTimeoutMs=l(()=>envInt("LENSMCP_SOURCES_TIMEOUT_MS",3e3,100),"sourcesTimeoutMs");exports.sourcesTimeoutMs=sourcesTimeoutMs;const sourcesConcurrency=l(()=>envInt("LENSMCP_SOURCES_CONCURRENCY",4,1),"sourcesConcurrency");exports.sourcesConcurrency=sourcesConcurrency,exports.SOURCE_KINDS=["queue","loop","store","traffic"],exports.SOURCE_KINDS_READ=["queue"],exports.QUEUE_WIRE_SCHEMA="lensmcp.queue/v1",exports.SOURCE_DEFAULT_INTERVAL_SEC=10,exports.SOURCE_MIN_INTERVAL_SEC=2,exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=10,exports.SOURCE_MAX_BYTES=256*1024,exports.SOURCE_MAX_GROUPS=500,exports.SOURCE_MAX_PER_SERVICE=16,exports.SOURCE_NOT_RUNNING_REPORT_MS=6e4,exports.SOURCE_DECLARATION_REPORT_MS=5*6e4,exports.SOURCE_BACKOFF_MAX_MS=6e4;const sourceStaleAfterMs=l(e=>Math.max(3e4,3*Math.max(0,e)),"sourceStaleAfterMs");exports.sourceStaleAfterMs=sourceStaleAfterMs;function sourceBackoffMs(e,o){return o<=1?e:Math.min(e*2**Math.min(o-1,10),Math.max(e,exports.SOURCE_BACKOFF_MAX_MS))}v(sourceBackoffMs,"sourceBackoffMs"),l(sourceBackoffMs,"sourceBackoffMs");const ENTRY_KEYS=["kind","name","path","intervalSec","thresholds"],THRESHOLD_KEYS={queue:["oldestWaitMin","stalledMin"]},PATH_RE=/^\/(?!\/)[^\s#\u0000-\u001f\u007f]{0,511}$/,NAME_RE=/^[^\u0000-\u001f\u007f]{1,100}$/,isObj=l(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isObj"),minutes=l(e=>typeof e=="number"&&Number.isFinite(e)&&e>0&&e<=10080,"minutes");function parseSourceDecls(e){const o=[],n=[];return e===void 0?{sources:o,problems:n}:Array.isArray(e)?(e.forEach((u,p)=>{const E=`sources[${p}]`;if(!isObj(u)){n.push(`${E} is not an object \u2014 not read`);return}const f=typeof u.name=="string"&&u.name.trim()?`${E} "${u.name}"`:E,i=u.kind;if(typeof i!="string"||!exports.SOURCE_KINDS.includes(i)){n.push(`${f}: kind ${JSON.stringify(i??null)} is not a source kind (${exports.SOURCE_KINDS.join(", ")}) \u2014 not read`);return}if(!exports.SOURCE_KINDS_READ.includes(i)){n.push(`${f}: kind "${i}" is not read by this release (it reads ${exports.SOURCE_KINDS_READ.join(", ")}) \u2014 not read`);return}const r=i,h=Object.keys(u).filter(s=>!ENTRY_KEYS.includes(s));if(h.length){n.push(`${f}: unknown key${h.length===1?"":"s"} ${h.map(s=>JSON.stringify(s)).join(", ")} for a ${r} source (known: ${ENTRY_KEYS.join(", ")}) \u2014 not read`);return}const a=u.name;if(typeof a!="string"||!a.trim()||!NAME_RE.test(a)){n.push(`${E}: name ${JSON.stringify(a??null)} must be 1\u2013100 characters, not blank \u2014 not read`);return}if(o.some(s=>s.name===a)){n.push(`${f}: the name is declared twice on this service \u2014 read once`);return}const _=u.path;if(typeof _!="string"||!PATH_RE.test(_)||_.split("?")[0].split("/").includes("..")){n.push(`${f}: path ${JSON.stringify(_??null)} must be a route on the service, starting with "/" (e.g. "/api/dev/lens/backlog") \u2014 not read`);return}const m=u.intervalSec;if(m!==void 0&&!(typeof m=="number"&&Number.isInteger(m)&&m>=exports.SOURCE_MIN_INTERVAL_SEC&&m<=3600)){n.push(`${f}: intervalSec ${JSON.stringify(m)} must be a whole number of seconds from ${exports.SOURCE_MIN_INTERVAL_SEC} to 3600 \u2014 not read`);return}const S=u.thresholds;if(S!==void 0&&!isObj(S)){n.push(`${f}: thresholds must be an object \u2014 not read`);return}const O=isObj(S)?S:{},x=Object.keys(O).filter(s=>!THRESHOLD_KEYS[r].includes(s));if(x.length){n.push(`${f}: unknown threshold${x.length===1?"":"s"} ${x.map(s=>JSON.stringify(s)).join(", ")} for a ${r} source (known: ${THRESHOLD_KEYS[r].join(", ")}) \u2014 not read`);return}const t=THRESHOLD_KEYS[r].find(s=>O[s]!==void 0&&!minutes(O[s]));if(t){n.push(`${f}: thresholds.${t} ${JSON.stringify(O[t])} must be minutes, more than 0 and at most 10080 \u2014 not read`);return}if(o.length>=exports.SOURCE_MAX_PER_SERVICE){n.push(`${f}: more than ${exports.SOURCE_MAX_PER_SERVICE} sources on one service \u2014 not read`);return}o.push({kind:r,name:a,path:_,intervalSec:m??exports.SOURCE_DEFAULT_INTERVAL_SEC,thresholds:{oldestWaitMin:O.oldestWaitMin??exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN,...O.stalledMin!==void 0?{stalledMin:O.stalledMin}:{}}})}),{sources:o,problems:n}):{sources:o,problems:["`sources` is not a list of { kind, name, path } entries \u2014 no source is read"]}}v(parseSourceDecls,"parseSourceDecls"),l(parseSourceDecls,"parseSourceDecls");const ISO_RE=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})$/,isIso=l(e=>typeof e=="string"&&ISO_RE.test(e)&&!Number.isNaN(Date.parse(e)),"isIso"),isCount=l(e=>typeof e=="number"&&Number.isSafeInteger(e)&&e>=0,"isCount");function parseQueueSnapshot(e){const o=l(i=>({ok:!1,reason:i}),"fail");if(!isObj(e))return o("the snapshot is not a JSON object");if(e.schema!==exports.QUEUE_WIRE_SCHEMA)return o(`schema is ${e.schema===void 0?"absent":JSON.stringify(e.schema).slice(0,60)}, not "${exports.QUEUE_WIRE_SCHEMA}"`);const n=e.at;if(!isIso(n))return o("`at` is not an ISO time");const u=e.truncated;if(u!==void 0&&!(isObj(u)&&isCount(u.groups)))return o("`truncated` must be { groups: <how many were dropped> }, or absent");const p=e.groups;if(!Array.isArray(p))return o("`groups` is not a list");if(p.length>exports.SOURCE_MAX_GROUPS)return o(`${p.length} groups \u2014 at most ${exports.SOURCE_MAX_GROUPS}; cap them and say \`truncated: { groups: <dropped> }\``);const E=new Set,f=[];for(let i=0;i<p.length;i++){const r=p[i],h=`groups[${i}]`;if(!isObj(r))return o(`${h} is not an object`);const a=r.key;if(typeof a!="string"||a.length===0||a.length>200)return o(`${h}.key must be a string of 1\u2013200 characters`);if(E.has(a))return o(`${h}.key ${JSON.stringify(a)} appears twice`);if(E.add(a),!isCount(r.pending))return o(`${h}.pending must be a whole number \u2265 0`);for(const S of["inFlight","failed","completedInWindow"])if(r[S]!==void 0&&!isCount(r[S]))return o(`${h}.${S} must be a whole number \u2265 0, or absent`);const _=r.oldestPendingAt;if(_!=null&&!isIso(_))return o(`${h}.oldestPendingAt must be an ISO time, null, or absent`);const m=r.labels;if(m!==void 0){if(!isObj(m))return o(`${h}.labels must be an object of strings`);const S=Object.entries(m);if(S.length>16)return o(`${h}.labels has more than 16 entries`);for(const[O,x]of S)if(O.length===0||O.length>64||typeof x!="string"||x.length>200)return o(`${h}.labels.${O.slice(0,64)} must be a string of at most 200 characters`)}f.push({key:a,...m!==void 0?{labels:m}:{},pending:r.pending,...r.inFlight!==void 0?{inFlight:r.inFlight}:{},...r.failed!==void 0?{failed:r.failed}:{},..._!==void 0?{oldestPendingAt:_}:{},...r.completedInWindow!==void 0?{completedInWindow:r.completedInWindow}:{}})}return{ok:!0,at:n,groups:f,...u!==void 0?{truncated:{groups:u.groups}}:{}}}v(parseQueueSnapshot,"parseQueueSnapshot"),l(parseQueueSnapshot,"parseQueueSnapshot");const declRaw=l(e=>({sourceKind:e.kind,name:e.name,path:e.path,intervalSec:e.intervalSec,thresholds:{...e.thresholds}}),"declRaw"),oldestOf=l(e=>e.pending>0&&e.oldestPendingAt?Date.parse(e.oldestPendingAt):Number.POSITIVE_INFINITY,"oldestOf");function keepQueueSnapshot(e,o,n,u){const p={groups:o.length,pending:0,inFlight:0,failed:0};let E;for(const i of o)p.pending+=i.pending,p.inFlight+=i.inFlight??0,p.failed+=i.failed??0,i.completedInWindow!==void 0&&(E=(E??0)+i.completedInWindow);E!==void 0&&(p.completedInWindow=E);const f=[...o].sort((i,r)=>oldestOf(i)-oldestOf(r)||r.pending-i.pending||(i.key<r.key?-1:i.key>r.key?1:0));return{at:e,receivedAt:n,totals:p,groups:f.map(({key:i,...r})=>({group:i,...r})),...u?{truncated:{groups:u.groups}}:{}}}v(keepQueueSnapshot,"keepQueueSnapshot"),l(keepQueueSnapshot,"keepQueueSnapshot");function buildSourcePollRaw(e,o,n,u,p,E){return{kind:"source-poll",provenance:"reported",project:e,...declRaw(o),state:n.state,...n.detail!==void 0?{detail:n.detail}:{},...n.status!==void 0?{status:n.status}:{},polledAt:n.polledAt,...n.durationMs!==void 0?{durationMs:n.durationMs}:{},reportEveryMs:u,...p>0?{failures:p}:{},...E?{snapshot:E}:{}}}v(buildSourcePollRaw,"buildSourcePollRaw"),l(buildSourcePollRaw,"buildSourcePollRaw");function buildSourceDeclarationRaw(e,o){return{kind:"source-declaration",project:e,sources:o.sources.map(declRaw),problems:[...o.problems]}}v(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),l(buildSourceDeclarationRaw,"buildSourceDeclarationRaw");const span=l(e=>{const o=Math.round(e/6e4);return e<6e4?`${Math.max(0,Math.round(e/1e3))}s`:o<120?`${o}m`:`${Math.floor(o/60)}h${o%60?` ${o%60}m`:""}`},"span");function sourcePollTitle(e){const o=`${e.sourceKind} ${e.project}/${e.name} (reported)`;if(e.state!=="current"||!e.snapshot)return`${o}: ${e.state}${e.detail?` \u2014 ${e.detail}`:""}`;const n=e.snapshot,u=n.groups.find(E=>E.pending>0&&E.oldestPendingAt),p=u?.oldestPendingAt?Date.parse(n.at)-Date.parse(u.oldestPendingAt):void 0;return`${o}: ${n.totals.pending} pending in ${n.totals.groups} group${n.totals.groups===1?"":"s"}${p!==void 0?`, oldest waiting ${span(Math.max(0,p))}`:""}`}v(sourcePollTitle,"sourcePollTitle"),l(sourcePollTitle,"sourcePollTitle");function requestOverSocket(e,o,n,u,p=exports.SOURCE_MAX_BYTES){return new Promise(E=>{let f=!1,i;const r=l(a=>{f||(f=!0,clearTimeout(h),E(a))},"finish"),h=setTimeout(()=>{r({ok:!1,code:"ETIMEDOUT",message:`no answer within ${u} ms`}),i?.destroy()},u);h.unref?.();try{i=http.request({socketPath:e,path:o,method:"GET",agent:!1,headers:n},a=>{const _=[];let m=0;a.on("data",S=>{if(!f){if(m+=S.length,m>p){r({ok:!0,status:a.statusCode??0,body:"",truncated:!0}),a.destroy();return}_.push(S)}}),a.on("end",()=>r({ok:!0,status:a.statusCode??0,body:Buffer.concat(_).toString("utf8"),truncated:!1})),a.on("error",S=>r({ok:!1,code:S.code??"ERESPONSE",message:S.message}))}),i.on("error",a=>r({ok:!1,code:a.code??"EREQUEST",message:a.message})),i.end()}catch(a){r({ok:!1,code:a.code??"EREQUEST",message:a.message})}})}v(requestOverSocket,"requestOverSocket"),l(requestOverSocket,"requestOverSocket");const snippet=l(e=>e.replace(/\s+/g," ").trim().slice(0,160),"snippet");function createSourcePoller(e){const o=e.now??Date.now,n=e.timeoutMs??(0,exports.sourcesTimeoutMs)(),u=Math.max(1,e.maxConcurrent??(0,exports.sourcesConcurrency)()),p=e.socketOf??(t=>(0,discovery_1.pickSock)(t.pool,e.scanTtlMs??1500)),E=e.request??requestOverSocket,f=e.log??(t=>console.warn(t)),i=new WeakMap,r=new Map;let h=0,a;const _=l(t=>{const s=t.decl;let d=i.get(s);if(!d){d=parseSourceDecls(t.decl.sources),i.set(s,d);for(const c of d.problems)f(`[gateway] ${t.project}: cluster.sources \u2014 ${c}`)}const $=o();return(d.reportedAt===void 0||$-d.reportedAt>=exports.SOURCE_DECLARATION_REPORT_MS)&&(d.reportedAt=$,e.emit(t,d.problems.length?"warning":"info",d.problems.length?`sources ${t.project}: ${d.problems.length} cluster.sources entr${d.problems.length===1?"y":"ies"} not read \u2014 ${d.problems[0]}`:`sources ${t.project}: reads ${d.sources.map(c=>`${c.kind} "${c.name}"`).join(", ")}`,`source-declaration:${t.wsKey}:${t.project}`,buildSourceDeclarationRaw(t.project,d))),d},"declarationOf"),m=l(t=>{if(t.state==="starting")return{notRunning:"it is starting \u2014 read once it is up"};const s=p(t);return s?{socket:s}:{notRunning:t.state==="up"?"it has no live pod socket \u2014 no pod runs it now":"it is scaled to zero \u2014 the lens does not start a service to read a source"}},"reach"),S=l((t,s)=>{const d=t.last;if(!d)return;const $=o();if(!s&&t.reportedAt!==void 0&&$-t.reportedAt<t.reportEveryMs)return;t.reportedAt=$;const c=buildSourcePollRaw(t.svc.project,t.decl,d,t.reportEveryMs,t.failures,t.good),R=d.state==="unreachable"||d.state==="refused"||d.state==="malformed";e.emit(t.svc,R?"warning":"info",sourcePollTitle(c),`source:${t.svc.wsKey}:${t.svc.project}:${t.decl.name}`,c)},"report"),O=l(async t=>{const s=o(),d=t.decl.intervalSec*1e3;t.inFlight=!0,h+=1;const $=t.last?.state;try{const c=m(t.svc);if("notRunning"in c){t.last={state:"not-running",detail:c.notRunning,polledAt:s},t.failures=0,t.nextAt=s+d,t.reportEveryMs=exports.SOURCE_NOT_RUNNING_REPORT_MS,S(t,$!=="not-running");return}const R=e.serviceKey(t.svc),g=await E(c.socket,t.decl.path,{accept:"application/json","x-lensmcp-probe":`source:${t.decl.kind}`,...R?{"x-internal-token":R}:{}},n,exports.SOURCE_MAX_BYTES),M=Math.max(0,o()-s);if(g.ok===!1){const A=g.code==="ECONNREFUSED"||g.code==="ENOENT";t.last=A&&t.svc.state!=="up"?{state:"not-running",detail:`nothing listens on its pod socket \u2014 the service is ${t.svc.state}`,polledAt:s,durationMs:M}:{state:"unreachable",detail:g.code==="ETIMEDOUT"?g.message:`${g.code}: ${snippet(g.message)}`,polledAt:s,durationMs:M}}else if(g.status<200||g.status>299){const A=snippet(g.body);t.last={state:"refused",status:g.status,detail:`HTTP ${g.status}${A?`: ${A}`:""}`,polledAt:s,durationMs:M}}else if(g.truncated)t.last={state:"malformed",detail:`the snapshot is larger than ${exports.SOURCE_MAX_BYTES/1024} KiB`,polledAt:s,durationMs:M};else{let A;try{A=parseQueueSnapshot(JSON.parse(g.body))}catch{A={ok:!1,reason:`not JSON: ${snippet(g.body)||"(an empty body)"}`}}A.ok===!0?(t.good=keepQueueSnapshot(A.at,A.groups,o(),A.truncated),t.last={state:"current",polledAt:s,durationMs:M}):t.last={state:"malformed",detail:A.reason,polledAt:s,durationMs:M}}const y=t.last.state==="unreachable"||t.last.state==="refused"||t.last.state==="malformed";t.failures=y?t.failures+1:0;const C=y?sourceBackoffMs(d,t.failures):d;t.nextAt=s+C,t.reportEveryMs=t.last.state==="not-running"?exports.SOURCE_NOT_RUNNING_REPORT_MS:C,S(t,t.last.state!=="not-running"||$!=="not-running")}catch(c){t.last={state:"unreachable",detail:`the poll failed: ${snippet(c?.message??String(c))}`,polledAt:s},t.failures+=1,t.nextAt=s+sourceBackoffMs(d,t.failures),t.reportEveryMs=t.nextAt-s,S(t,!0)}finally{t.inFlight=!1,h-=1}},"pollOne"),x=l(async()=>{if(e.stopped?.())return;const t=new Set;for(const c of e.services())if(c.decl.sources!==void 0)for(const R of _(c).sources){const g=`${c.wsKey} ${c.project} ${R.name}`;t.add(g);const M=r.get(g);M?(M.svc!==c||M.decl!==R)&&(M.svc=c,M.decl=R):r.set(g,{svc:c,decl:R,nextAt:0,inFlight:!1,failures:0,reportEveryMs:R.intervalSec*1e3})}for(const[c,R]of[...r.entries()])!t.has(c)&&!R.inFlight&&r.delete(c);const s=o(),d=[...r.values()].filter(c=>!c.inFlight&&s>=c.nextAt).sort((c,R)=>c.nextAt-R.nextAt),$=[];for(const c of d){if(h>=u)break;$.push(O(c))}await Promise.all($)},"tick");return{tick:x,start(t=1e3){return a&&clearInterval(a),a=setInterval(()=>{x().catch(s=>console.error("[gateway] source poll error:",s))},t),a.unref(),a},stop(){a&&clearInterval(a),a=void 0},snapshot(){const t=o();return[...r.values()].map(s=>{const d=s.last!==void 0&&t-s.last.polledAt>(0,exports.sourceStaleAfterMs)(s.reportEveryMs);return{wsKey:s.svc.wsKey,project:s.svc.project,name:s.decl.name,kind:s.decl.kind,path:s.decl.path,state:s.last?d?"stale":s.last.state:"never-polled",...s.last?.detail!==void 0?{detail:s.last.detail}:{},...s.last?.status!==void 0?{status:s.last.status}:{},...s.last?{polledAt:s.last.polledAt}:{},failures:s.failures,nextAt:s.nextAt,...s.good?{snapshot:s.good,ageMs:Math.max(0,t-s.good.receivedAt)}:{}}})}}}v(createSourcePoller,"createSourcePoller"),l(createSourcePoller,"createSourcePoller");
@@ -90,6 +90,28 @@ export interface ClusterProjectDecl {
90
90
  * either way, so this is a tuning knob, not the safety mechanism.
91
91
  */
92
92
  firstByteMs?: number;
93
+ /**
94
+ * CUSTOM SOURCES (issues/open/foodguard-81): what this service REPORTS that the lens cannot observe — work it keeps
95
+ * waiting in its OWN storage (an outbox table), which no event can show. Each entry is a typed kind (`queue` · `loop` ·
96
+ * `store` · `traffic`; this release reads `queue`) with a dev route ON THE SERVICE that answers the kind's snapshot
97
+ * (`lensmcp.queue/v1`: `{ schema, at, truncated?, groups: [{ key, labels?, pending, inFlight?, failed?,
98
+ * oldestPendingAt?, completedInWindow? }] }`). The dev gateway reads it from a RUNNING pod every `intervalSec`
99
+ * (default 10, at least 2) over the pod's socket — never waking a service scaled to zero — and the lens shows it,
100
+ * labelled REPORTED (`observe.sources`, `source.get`, `process://sources`, Data › Queues), warning once a group's
101
+ * oldest pending item has waited longer than `thresholds.oldestWaitMin` (default 10). The lens never connects to the
102
+ * app's database; the prod gateway ignores the key. Pod (`service`) declarations only. Parsed by `source-poller.ts`
103
+ * `parseSourceDecls`: an unknown kind or a malformed entry is left out and said, never silently.
104
+ */
105
+ sources?: ReadonlyArray<{
106
+ kind: string;
107
+ name: string;
108
+ path: string;
109
+ intervalSec?: number;
110
+ thresholds?: {
111
+ oldestWaitMin?: number;
112
+ stalledMin?: number;
113
+ };
114
+ }>;
93
115
  /**
94
116
  * FRONTEND apps only: run this app UNDER the lens. When true, the gateway
95
117
  * SPAWNS the app's own Vite dev server with `@lensmcp/vite-plugin`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.25.5",
3
+ "version": "1.25.7",
4
4
  "description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -58,9 +58,9 @@
58
58
  }
59
59
  },
60
60
  "dependencies": {
61
- "@lensmcp/core": "1.25.5",
62
- "@lensmcp/node-instrumentation": "1.25.5",
63
- "@lensmcp/nx-plugin": "1.25.5",
61
+ "@lensmcp/core": "1.25.7",
62
+ "@lensmcp/node-instrumentation": "1.25.7",
63
+ "@lensmcp/nx-plugin": "1.25.7",
64
64
  "fastify": "^5.8.5",
65
65
  "@fastify/reply-from": "^12.6.2",
66
66
  "@typescript/native-preview": "^7.0.0-dev.20260707.2",