@lensmcp/cluster 1.21.5 → 1.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/executors/gateway/jwks-verify.js +1 -1
  2. package/executors/gateway/jwt-claims.d.ts +34 -0
  3. package/executors/gateway/jwt-claims.js +1 -0
  4. package/executors/gateway/main.prod-gateway.js +2 -2
  5. package/executors/gateway/prod-config.d.ts +27 -0
  6. package/executors/gateway/prod-config.js +1 -1
  7. package/executors/gateway/rate-limit.d.ts +9 -0
  8. package/executors/gateway/rate-limit.js +2 -2
  9. package/executors/gateway/runtime/auth.js +1 -1
  10. package/executors/gateway/runtime/chooser.d.ts +30 -9
  11. package/executors/gateway/runtime/chooser.js +9 -9
  12. package/executors/gateway/runtime/control.d.ts +17 -2
  13. package/executors/gateway/runtime/control.js +2 -2
  14. package/executors/gateway/runtime/edge.d.ts +29 -3
  15. package/executors/gateway/runtime/edge.js +1 -1
  16. package/executors/gateway/runtime/handler.js +1 -1
  17. package/executors/gateway/runtime/lens-children.js +1 -1
  18. package/executors/gateway/runtime/lifecycle.d.ts +21 -0
  19. package/executors/gateway/runtime/lifecycle.js +3 -3
  20. package/executors/gateway/runtime/proxy.d.ts +25 -0
  21. package/executors/gateway/runtime/proxy.js +1 -1
  22. package/executors/gateway/runtime/server.d.ts +90 -0
  23. package/executors/gateway/runtime/server.js +2 -2
  24. package/executors/gateway/runtime/upgrade.js +1 -1
  25. package/executors/serve/serve.impl.js +1 -1
  26. package/executors/trust/hostname.d.ts +20 -0
  27. package/executors/trust/hostname.js +1 -0
  28. package/executors/trust/trust.impl.js +5 -3
  29. package/main.devserver.d.ts +50 -0
  30. package/main.devserver.js +7 -7
  31. package/package.json +6 -6
@@ -1 +1 @@
1
- "use strict";var J=Object.defineProperty;var d=(e,n)=>J(e,"name",{value:n,configurable:!0});var v=Object.defineProperty,c=d((e,n)=>v(e,"name",{value:n,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createJwksVerifier=createJwksVerifier;const node_crypto_1=require("node:crypto"),ALG_TO_HASH={RS256:"sha256",RS384:"sha384",RS512:"sha512"},b64urlJsonSafe=c(e=>JSON.parse(Buffer.from(e,"base64url").toString("utf8")),"b64urlJsonSafe");function createJwksVerifier(e){const n=(e.algorithms??["RS256"]).filter(r=>r in ALG_TO_HASH),_=e.cacheMaxAgeMs??6e5,b=e.missRefreshMs??3e4,h=e.clockToleranceSec??60,S=e.fetchImpl??globalThis.fetch,p=e.audience===void 0?void 0:Array.isArray(e.audience)?e.audience:[e.audience];let o=new Map,f,g=0;const m=c(async()=>{const r=await S(e.jwksUrl,{headers:{accept:"application/json"}});if(!r.ok)throw new Error(`JWKS ${e.jwksUrl} \u2192 HTTP ${r.status}`);const a=await r.json(),t=new Map;for(const s of a.keys??[])if(!(s.kty!=="RSA"||!s.n||!s.e)&&!(s.use&&s.use!=="sig"))try{const u=(0,node_crypto_1.createPublicKey)({key:s,format:"jwk"});t.set(s.kid??"__single__",u)}catch{}t.size>0&&(o=t)},"load"),l=c(()=>(f||(f=m().finally(()=>{f=void 0})),f),"refresh"),A=c(r=>{r-g<b||(g=r,l().catch(()=>{}))},"refreshOnMiss"),k=setInterval(()=>{l().catch(()=>{})},_);return k.unref?.(),{verify:c(r=>{const a=r.split(".");if(a.length!==3)return;let t;try{t=b64urlJsonSafe(a[0])}catch{return}if(!t.alg||!n.includes(t.alg))return;const s=t.kid!==void 0?o.get(t.kid):o.get("__single__")??(o.size===1?[...o.values()][0]:void 0);if(!s){A(Date.now());return}let u;try{u=(0,node_crypto_1.verify)(ALG_TO_HASH[t.alg],Buffer.from(`${a[0]}.${a[1]}`),s,Buffer.from(a[2],"base64url"))}catch{return}if(!u)return;let i;try{i=b64urlJsonSafe(a[1])}catch{return}const w=Date.now()/1e3;if(!(typeof i.exp=="number"&&w>i.exp+h)&&!(typeof i.nbf=="number"&&w<i.nbf-h)&&!(e.issuer&&i.iss!==e.issuer)){if(p){const y=i.aud;if(!(Array.isArray(y)?y:[y]).some(j=>p.includes(j)))return}return i}},"verify"),refresh:l,stop:c(()=>clearInterval(k),"stop")}}d(createJwksVerifier,"createJwksVerifier"),c(createJwksVerifier,"createJwksVerifier");
1
+ "use strict";var m=Object.defineProperty;var d=(e,c)=>m(e,"name",{value:c,configurable:!0});var j=Object.defineProperty,s=d((e,c)=>j(e,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createJwksVerifier=createJwksVerifier;const node_crypto_1=require("node:crypto"),jwt_claims_js_1=require("./jwt-claims.js"),ALG_TO_HASH={RS256:"sha256",RS384:"sha384",RS512:"sha512"},b64urlJsonSafe=s(e=>JSON.parse(Buffer.from(e,"base64url").toString("utf8")),"b64urlJsonSafe");function createJwksVerifier(e){const c=(e.algorithms??["RS256"]).filter(r=>r in ALG_TO_HASH),k=e.cacheMaxAgeMs??6e5,p=e.missRefreshMs??3e4,w=e.clockToleranceSec??60,g=e.fetchImpl??globalThis.fetch,h=e.audience===void 0?void 0:Array.isArray(e.audience)?e.audience:[e.audience];let n=new Map,o,y=0;const v=s(async()=>{const r=await g(e.jwksUrl,{headers:{accept:"application/json"}});if(!r.ok)throw new Error(`JWKS ${e.jwksUrl} \u2192 HTTP ${r.status}`);const i=await r.json(),t=new Map;for(const a of i.keys??[])if(!(a.kty!=="RSA"||!a.n||!a.e)&&!(a.use&&a.use!=="sig"))try{const l=(0,node_crypto_1.createPublicKey)({key:a,format:"jwk"});t.set(a.kid??"__single__",l)}catch{}t.size>0&&(n=t)},"load"),u=s(()=>(o||(o=v().finally(()=>{o=void 0})),o),"refresh"),S=s(r=>{r-y<p||(y=r,u().catch(()=>{}))},"refreshOnMiss"),_=setInterval(()=>{u().catch(()=>{})},k);return _.unref?.(),{verify:s(r=>{const i=r.split(".");if(i.length!==3)return;let t;try{t=b64urlJsonSafe(i[0])}catch{return}if(!t.alg||!c.includes(t.alg))return;const a=t.kid!==void 0?n.get(t.kid):n.get("__single__")??(n.size===1?[...n.values()][0]:void 0);if(!a){S(Date.now());return}let l;try{l=(0,node_crypto_1.verify)(ALG_TO_HASH[t.alg],Buffer.from(`${i[0]}.${i[1]}`),a,Buffer.from(i[2],"base64url"))}catch{return}if(!l)return;let f;try{f=b64urlJsonSafe(i[1])}catch{return}if((0,jwt_claims_js_1.checkStandardClaims)(f,{...e.issuer?{issuer:e.issuer}:{},...h?{audience:h}:{},clockToleranceS:w}))return f},"verify"),refresh:u,stop:s(()=>clearInterval(_),"stop")}}d(createJwksVerifier,"createJwksVerifier"),s(createJwksVerifier,"createJwksVerifier");
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The standard JWT claim checks, in ONE place, for BOTH verifiers.
3
+ *
4
+ * They used to exist only in `jwks-verify.ts`. The HS256 fallback in `main.prod-gateway.ts` checked
5
+ * the HMAC and `exp` and nothing else — no `nbf`, no `iss`, no `aud` — while
6
+ * `docs/lensmcp/concepts/prod-gateway-config.mdx` documents `LENSMCP_GW_JWT_ISS` and
7
+ * `LENSMCP_GW_JWT_AUD` as the required restrictions for the fallback it also documents. An operator
8
+ * following that table configured an identity control that did not exist, and a token signed with
9
+ * the same shared key by another issuer or for another audience was accepted, satisfied edge ABAC,
10
+ * and was stamped into gateway-owned `x-user-*` headers (issues/codex-security/01).
11
+ *
12
+ * Two verifiers that must agree about identity should not each carry their own copy of the rules.
13
+ */
14
+ /** Seconds of clock skew tolerated on `exp` and `nbf`. */
15
+ export declare const DEFAULT_CLOCK_TOLERANCE_S = 60;
16
+ export interface StandardClaimOptions {
17
+ /** Required `iss`. Unset ⇒ not enforced. */
18
+ issuer?: string;
19
+ /** Allowed `aud` values; a token matching ANY of them passes. Unset ⇒ not enforced. */
20
+ audience?: readonly string[];
21
+ /** Clock skew allowance in seconds. Defaults to {@link DEFAULT_CLOCK_TOLERANCE_S}. */
22
+ clockToleranceS?: number;
23
+ }
24
+ /**
25
+ * Are a verified token's registered claims acceptable?
26
+ *
27
+ * Call this only AFTER the signature has been verified — it says nothing about authenticity, only
28
+ * about whether an authentic token is one this gateway should accept.
29
+ *
30
+ * A configured restriction fails closed: with an `issuer` configured, a token carrying no `iss` is
31
+ * rejected rather than treated as unrestricted. A restriction the operator did not configure is not
32
+ * enforced, which is what keeps this backwards-compatible for a deployment that sets neither.
33
+ */
34
+ export declare function checkStandardClaims(payload: Record<string, unknown>, opts: StandardClaimOptions): boolean;
@@ -0,0 +1 @@
1
+ "use strict";var o=Object.defineProperty;var t=(e,n)=>o(e,"name",{value:n,configurable:!0});var c=Object.defineProperty,u=t((e,n)=>c(e,"name",{value:n,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DEFAULT_CLOCK_TOLERANCE_S=void 0,exports.checkStandardClaims=checkStandardClaims,exports.DEFAULT_CLOCK_TOLERANCE_S=60;function checkStandardClaims(e,n){const s=n.clockToleranceS??exports.DEFAULT_CLOCK_TOLERANCE_S,a=Date.now()/1e3;if(typeof e.exp=="number"&&a>e.exp+s||typeof e.nbf=="number"&&a<e.nbf-s||n.issuer&&e.iss!==n.issuer)return!1;if(n.audience&&n.audience.length>0){const r=e.aud;if(!(Array.isArray(r)?r:[r]).some(i=>typeof i=="string"&&n.audience.includes(i)))return!1}return!0}t(checkStandardClaims,"checkStandardClaims"),u(checkStandardClaims,"checkStandardClaims");
@@ -1,2 +1,2 @@
1
- "use strict";var ce=Object.defineProperty;var T=(e,r)=>ce(e,"name",{value:r,configurable:!0});var ie=Object.defineProperty,a=T((e,r)=>ie(e,"name",{value:r,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0});const tslib_1=require("tslib"),node_fs_1=require("node:fs"),node_crypto_1=require("node:crypto"),node_path_1=require("node:path"),node_os_1=require("node:os"),node_cluster_1=tslib_1.__importDefault(require("node:cluster")),prod_gateway_lib_1=require("./prod-gateway.lib"),jwks_verify_1=require("./jwks-verify"),otel_tracing_1=require("./otel-tracing"),health_check_1=require("./health-check"),rate_limit_1=require("./rate-limit"),metrics_1=require("./metrics"),edge_1=require("./prod-runtime/edge"),registry_source_1=require("./registry-source"),providers_prod_1=require("./providers-prod"),prod_config_1=require("./prod-config");function readJson(e,r){if(!e)return r;try{return JSON.parse((0,node_fs_1.readFileSync)(e,"utf8"))}catch(n){return console.error(`[gateway] could not read ${e}:`,n.message),r}}T(readJson,"readJson"),a(readJson,"readJson");function watchJsonFile(e,r,n=1e3){let u=-1;const E=setInterval(a(()=>{if(!(0,node_fs_1.existsSync)(e))return;let p;try{p=(0,node_fs_1.statSync)(e).mtimeMs}catch{return}if(p===u)return;let y;try{y=JSON.parse((0,node_fs_1.readFileSync)(e,"utf8"))}catch(_){console.error("[gateway] endpoints reload:",_.message);return}u=p,r(y)},"tick"),n);return E.unref?.(),()=>clearInterval(E)}T(watchJsonFile,"watchJsonFile"),a(watchJsonFile,"watchJsonFile");async function main(){const e=process.env,r=(0,prod_config_1.parseGatewayEnv)(e),n=r.ports;process.on("uncaughtException",o=>console.error("[gateway] uncaughtException:",o)),process.on("unhandledRejection",o=>console.error("[gateway] unhandledRejection:",o));const u=[];let E=!1;const p=a(async o=>{if(!E){E=!0,console.log(`[gateway] ${o} \u2014 draining\u2026`);for(const t of u)try{await t()}catch{}process.exit(0)}},"shutdown");for(const o of["SIGTERM","SIGINT"])process.on(o,()=>{p(o)});const y=e.LENSMCP_GW_REDIS_URL;let _,w,b,$;const O={};let f;if(y){const o=require("ioredis"),t=new o(y);f=t;const s=(0,registry_source_1.createRegistryProviders)((0,registry_source_1.redisRegistrySource)(t,e.LENSMCP_GW_REDIS_PREFIX?{prefix:e.LENSMCP_GW_REDIS_PREFIX}:{}),{onError:a(i=>console.error("[gateway] registry:",i instanceof Error?i.message:i),"onError")});await s.start(),_=s.manifests,w=s.pods,$=s,console.log(`[gateway] registry: Redis-backed, live (${_.list().length} service(s))`)}else{const o=e.LENSMCP_GW_MANIFESTS;o?_=(0,providers_prod_1.fileManifestProvider)(o,{onError:a(s=>console.error("[gateway] manifest:",s instanceof Error?s.message:s),"onError")}):(console.warn("[gateway] no LENSMCP_GW_MANIFESTS / LENSMCP_GW_REDIS_URL \u2014 starting with an EMPTY route table (all requests 404 until manifests arrive)."),_=(0,providers_prod_1.staticManifestProvider)([]));const t=e.LENSMCP_GW_ENDPOINTS;if(t){const s=a(S=>console.error("[gateway] endpoints:",S instanceof Error?S.message:S),"onEpError"),i=readJson(t,{}),d=Object.keys(i).length,l=(0,providers_prod_1.isRolloutShape)(i);w=l?(0,providers_prod_1.rolloutPodProvider)(i,{onError:s}):(0,providers_prod_1.endpointsPodProvider)(i,{onError:s}),console.log(d?`[gateway] endpoints: ${l?"ROLLOUT (weighted cohorts)":"flat"} \u2014 ${d} service(s)`:`[gateway] endpoints: ${(0,node_fs_1.existsSync)(t)?"present but empty/unreadable":"file not there yet"} \u2014 watching ${t}`),b=watchJsonFile(t,S=>{try{w?.update?.(S),O.current?.refreshUpstreams(),console.log("[gateway] endpoints reloaded")}catch(ae){console.error("[gateway] endpoints reload failed:",ae.message)}})}}const Z=readJson(e.LENSMCP_GW_KEYS,{}),A=e.LENSMCP_GW_TLS_KEY&&e.LENSMCP_GW_TLS_CERT?{key:(0,node_fs_1.readFileSync)(e.LENSMCP_GW_TLS_KEY),cert:(0,node_fs_1.readFileSync)(e.LENSMCP_GW_TLS_CERT)}:void 0,ee=r.jwt.presenceOnly,F=r.jwt.jwksUrl,M=r.jwt.secret,g=F?(0,jwks_verify_1.createJwksVerifier)({jwksUrl:F,...r.jwt.issuer?{issuer:r.jwt.issuer}:{},...r.jwt.audience?{audience:r.jwt.audience}:{}}):void 0,te=a(o=>{if(!M)return;const t=o.split(".");if(t.length===3)try{const s=(0,node_crypto_1.createHmac)("sha256",M).update(t[0]+"."+t[1]).digest("base64url"),i=Buffer.from(t[2]),d=Buffer.from(s);if(i.length!==d.length||!(0,node_crypto_1.timingSafeEqual)(i,d))return;const l=JSON.parse(Buffer.from(t[1],"base64url").toString());return typeof l.exp=="number"&&Date.now()/1e3>l.exp?void 0:l}catch{return}},"hs256"),j=a(o=>{const t=String(o.headers.authorization??"");if(!t.startsWith("Bearer "))return;const s=t.slice(7);return g?g.verify(s):te(s)},"bearerClaims"),q=g||M?j:void 0,re=a((o,t)=>{if(o==="jwt"){if(g||M){if(!j(t))throw new Error("invalid or missing token");return}if(!ee)throw new Error("jwt route but no verifier configured (set LENSMCP_GW_JWKS_URL or LENSMCP_GW_JWT_SECRET)");if(!String(t.headers.authorization??"").startsWith("Bearer "))throw new Error("missing bearer token")}},"authenticate"),{attributeHeaders:D,trustProxyHops:oe,trustProxyIp:x,uidHeader:U,cookie:J}=r,m=e.LENSMCP_EVENT_FILE,K=m?(()=>{try{return require("@lensmcp/node-instrumentation")}catch{return}})():void 0,H=K?.createEventFloodGuard({burst:30,perSec:5});let z=!1;const I=m?o=>{try{const t=H?H.admit(String(o.fingerprint??"")):{ok:!0,suppressed:0};if(!t.ok)return;z||((0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(m),{recursive:!0}),z=!0);const s=o.raw,i=JSON.stringify(t.suppressed>0?{...o,raw:{...s,floodSuppressed:t.suppressed}}:o)+`
2
- `;K?.maybeRotateEventFile(m,i.length),(0,node_fs_1.appendFileSync)(m,i)}catch{}}:void 0;if(g)try{await g.refresh(),console.log("[gateway] JWKS loaded")}catch(o){console.error("[gateway] initial JWKS load failed (will retry in background):",o.message)}const{accessLog:se,clientIpHeader:B,clientIpTrustedProxies:Y,undici:c}=r;if(r.mtlsFromRedis&&f){const o=e.LENSMCP_GW_REDIS_PREFIX??"lensmcp:gw",[t,s,i]=await f.mget(`${o}:mtls:cert`,`${o}:mtls:key`,`${o}:mtls:ca`);t&&(c.cert=t),s&&(c.key=s),i&&(c.ca=i),(t||s)&&console.log("[gateway] mTLS materials loaded from Redis")}else e.LENSMCP_GW_MTLS_CERT&&(c.cert=(0,node_fs_1.readFileSync)(e.LENSMCP_GW_MTLS_CERT)),e.LENSMCP_GW_MTLS_KEY&&(c.key=(0,node_fs_1.readFileSync)(e.LENSMCP_GW_MTLS_KEY)),e.LENSMCP_GW_MTLS_CA&&(c.ca=(0,node_fs_1.readFileSync)(e.LENSMCP_GW_MTLS_CA));(0,prod_config_1.finalizeUpstreamTls)(c).autoEnabled&&console.log("[gateway] upstream TLS verification ENABLED (a CA is configured; set LENSMCP_GW_UPSTREAM_TLS_VERIFY=0 to opt out)"),(0,prod_config_1.finalizeUpstreamH2)(c,e)&&console.log(`[gateway] mTLS to upstreams ON over ${c.allowH2?"h2":"h1.1"} (server validation ${c.rejectUnauthorized?"enabled":"OFF"})`);const{upstreamRetries:v,server:V}=r,P=(0,otel_tracing_1.setupTracing)({...e.OTEL_SERVICE_NAME?{serviceName:e.OTEL_SERVICE_NAME}:{},...e.LENSMCP_GW_OTEL_SAMPLE?{sampleRatio:Number(e.LENSMCP_GW_OTEL_SAMPLE)}:{}});P&&console.log(`[gateway] OTLP tracing on \u2192 ${(0,edge_1.safeUrl)(e.OTEL_EXPORTER_OTLP_ENDPOINT)}`);const{statusEnabled:R,statusToken:h}=r,X=(0,node_os_1.hostname)(),ne=r.statusTtlSec,Q=f,C=R&&Q?o=>{for(const t of o)Q.set(`status:${t.service}:${X}`,JSON.stringify({...t,instance:X,at:Date.now()}),"EX",ne).catch(()=>{})}:void 0;R&&!h&&console.warn("[gateway] LENSMCP_GW_STATUS=1 but no LENSMCP_GW_STATUS_TOKEN \u2014 /statusz stays DISABLED (404) until a token is set (fail-closed). The Redis status:* fan-out still runs."),R&&console.log(`[gateway] status on \u2014 /statusz${h?"":" (endpoint disabled: no token)"}${C?" + Redis status:* fan-out":""}`);const G=r.health?(0,health_check_1.createHealthChecker)(r.health):void 0;G&&console.log(`[gateway] active health-checking on (path ${r.health?.path??"/healthz"})`);let L,k;if(r.rateLimit){const{limit:o,windowMs:t,wantRedis:s,failClosed:i}=r.rateLimit;if(s){const d=r.rateLimit.dedicatedUrl;let l;if(d){const S=require("ioredis");k=new S(d),l=k}else f&&(l=f);l?(L=(0,rate_limit_1.createRedisRateLimiter)(l,{limit:o,windowMs:t,...r.rateLimit.timeoutMs!==void 0?{timeoutMs:r.rateLimit.timeoutMs}:{},...i?{failOpen:!1}:{}}),console.log(`[gateway] rate limit on \u2014 ${o} req / ${t}ms GLOBAL (Redis: ${d?"dedicated":"shared registry"}, fail-${i?"closed":"open"}) per client IP`)):console.warn("[gateway] LENSMCP_GW_RATE_LIMIT_REDIS set but no Redis configured (set LENSMCP_GW_REDIS_URL or LENSMCP_GW_RATE_LIMIT_REDIS_URL) \u2014 falling back to PER-INSTANCE in-memory limiting.")}L||(L=(0,rate_limit_1.createRateLimiter)({limit:o,windowMs:t}),console.log(`[gateway] rate limit on \u2014 ${o} req / ${t}ms per client IP (in-memory, per-instance)`))}const W=r.metricsEnabled?(0,metrics_1.createProcessMetrics)():void 0;W&&console.log(`[gateway] process metrics on \u2014 /metricsz${h?" (token-gated)":" (DISABLED: needs LENSMCP_GW_STATUS_TOKEN)"}`);const N=await(0,prod_gateway_lib_1.startProdGateway)({...n.length?{ports:n}:{},manifests:_,...w?{pods:w}:{},serviceKeys:Z,authenticate:re,...A?{tls:A}:{},...I?{emit:I}:{},...q?{identify:q}:{},...D.length?{attributeHeaders:D}:{},...x?{trustProxyIp:x,trustProxyHops:oe}:{},...J?{cookie:J}:{},...U?{uidHeader:U}:{},...se?{accessLog:!0}:{},...B?{clientIpHeader:B}:{},...Y.length?{clientIpTrustedProxies:Y}:{},...P?{tracing:P}:{},...R?{statusEndpoint:!0}:{},...h?{statusToken:h}:{},...C?{onStatus:C}:{},...G?{healthChecker:G}:{},...L?{rateLimiter:L}:{},...W?{metrics:W}:{},...r.trafficFlushMs!==void 0?{trafficFlushMs:r.trafficFlushMs}:{},...v!==void 0&&Number.isFinite(v)?{upstreamRetries:v}:{},...Object.keys(c).length?{undici:c}:{},...Object.keys(V).length?{server:V}:{}});O.current=N,console.log(`[gateway] production gateway up on ${N.ports.join(", ")} (${N.routes().length} routes${I?", lens ON":""})`),u.push(()=>_.stop?.(),()=>b?.(),()=>$?.stop(),()=>k?.quit(),()=>g?.stop(),()=>N.stop(),()=>P?.shutdown())}T(main,"main"),a(main,"main");const WORKERS=(0,prod_config_1.workerCount)(process.env);if(WORKERS>1&&node_cluster_1.default.isPrimary){console.log(`[gateway] primary ${process.pid}: forking ${WORKERS} workers (one per core)\u2026`),process.env.LENSMCP_GW_RATE_LIMIT&&!(process.env.LENSMCP_GW_RATE_LIMIT_REDIS==="1"||process.env.LENSMCP_GW_RATE_LIMIT_REDIS_URL)&&console.warn(`[gateway] WARNING: in-memory rate limiter + ${WORKERS} workers \u2192 the limit is enforced PER WORKER (effective \u2248 ${WORKERS}\xD7 ${process.env.LENSMCP_GW_RATE_LIMIT}/window). Set LENSMCP_GW_RATE_LIMIT_REDIS=1 for one shared global budget, or divide LENSMCP_GW_RATE_LIMIT by ${WORKERS}.`);let e=!1;for(let n=0;n<WORKERS;n++)node_cluster_1.default.fork();node_cluster_1.default.on("exit",(n,u,E)=>{e||(console.warn(`[gateway] worker ${n.process.pid} exited (code=${u} sig=${E??""}) \u2014 respawning.`),node_cluster_1.default.fork())});const r=a(n=>{if(!e){e=!0,console.log(`[gateway] primary: ${n} \u2014 stopping ${Object.keys(node_cluster_1.default.workers??{}).length} workers\u2026`);for(const u of Object.values(node_cluster_1.default.workers??{}))try{u?.kill(n)}catch{}setTimeout(()=>process.exit(0),12e3).unref()}},"stopAll");process.on("SIGTERM",()=>r("SIGTERM")),process.on("SIGINT",()=>r("SIGINT"))}else main().catch(e=>{console.error("[gateway] FATAL: could not start \u2014",e instanceof Error?e.stack??e.message:e),process.exit(1)});
1
+ "use strict";var ce=Object.defineProperty;var R=(e,r)=>ce(e,"name",{value:r,configurable:!0});var ie=Object.defineProperty,a=R((e,r)=>ie(e,"name",{value:r,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0});const tslib_1=require("tslib"),node_fs_1=require("node:fs"),node_crypto_1=require("node:crypto"),node_path_1=require("node:path"),node_os_1=require("node:os"),node_cluster_1=tslib_1.__importDefault(require("node:cluster")),prod_gateway_lib_1=require("./prod-gateway.lib"),jwt_claims_1=require("./jwt-claims"),jwks_verify_1=require("./jwks-verify"),otel_tracing_1=require("./otel-tracing"),health_check_1=require("./health-check"),rate_limit_1=require("./rate-limit"),metrics_1=require("./metrics"),edge_1=require("./prod-runtime/edge"),registry_source_1=require("./registry-source"),providers_prod_1=require("./providers-prod"),prod_config_1=require("./prod-config");function readJson(e,r){if(!e)return r;try{return JSON.parse((0,node_fs_1.readFileSync)(e,"utf8"))}catch(n){return console.error(`[gateway] could not read ${e}:`,n.message),r}}R(readJson,"readJson"),a(readJson,"readJson");function watchJsonFile(e,r,n=1e3){let u=-1;const _=setInterval(a(()=>{if(!(0,node_fs_1.existsSync)(e))return;let p;try{p=(0,node_fs_1.statSync)(e).mtimeMs}catch{return}if(p===u)return;let w;try{w=JSON.parse((0,node_fs_1.readFileSync)(e,"utf8"))}catch(g){console.error("[gateway] endpoints reload:",g.message);return}u=p,r(w)},"tick"),n);return _.unref?.(),()=>clearInterval(_)}R(watchJsonFile,"watchJsonFile"),a(watchJsonFile,"watchJsonFile");async function main(){const e=process.env,r=(0,prod_config_1.parseGatewayEnv)(e),n=r.ports;process.on("uncaughtException",s=>console.error("[gateway] uncaughtException:",s)),process.on("unhandledRejection",s=>console.error("[gateway] unhandledRejection:",s));const u=[];let _=!1;const p=a(async s=>{if(!_){_=!0,console.log(`[gateway] ${s} \u2014 draining\u2026`);for(const t of u)try{await t()}catch{}process.exit(0)}},"shutdown");for(const s of["SIGTERM","SIGINT"])process.on(s,()=>{p(s)});const w=e.LENSMCP_GW_REDIS_URL;let g,m,$,b;const O={};let f;if(w){const s=require("ioredis"),t=new s(w);f=t;const o=(0,registry_source_1.createRegistryProviders)((0,registry_source_1.redisRegistrySource)(t,e.LENSMCP_GW_REDIS_PREFIX?{prefix:e.LENSMCP_GW_REDIS_PREFIX}:{}),{onError:a(i=>console.error("[gateway] registry:",i instanceof Error?i.message:i),"onError")});await o.start(),g=o.manifests,m=o.pods,b=o,console.log(`[gateway] registry: Redis-backed, live (${g.list().length} service(s))`)}else{const s=e.LENSMCP_GW_MANIFESTS;s?g=(0,providers_prod_1.fileManifestProvider)(s,{onError:a(o=>console.error("[gateway] manifest:",o instanceof Error?o.message:o),"onError")}):(console.warn("[gateway] no LENSMCP_GW_MANIFESTS / LENSMCP_GW_REDIS_URL \u2014 starting with an EMPTY route table (all requests 404 until manifests arrive)."),g=(0,providers_prod_1.staticManifestProvider)([]));const t=e.LENSMCP_GW_ENDPOINTS;if(t){const o=a(E=>console.error("[gateway] endpoints:",E instanceof Error?E.message:E),"onEpError"),i=readJson(t,{}),c=Object.keys(i).length,l=(0,providers_prod_1.isRolloutShape)(i);m=l?(0,providers_prod_1.rolloutPodProvider)(i,{onError:o}):(0,providers_prod_1.endpointsPodProvider)(i,{onError:o}),console.log(c?`[gateway] endpoints: ${l?"ROLLOUT (weighted cohorts)":"flat"} \u2014 ${c} service(s)`:`[gateway] endpoints: ${(0,node_fs_1.existsSync)(t)?"present but empty/unreadable":"file not there yet"} \u2014 watching ${t}`),$=watchJsonFile(t,E=>{try{m?.update?.(E),O.current?.refreshUpstreams(),console.log("[gateway] endpoints reloaded")}catch(ae){console.error("[gateway] endpoints reload failed:",ae.message)}})}}const Z=readJson(e.LENSMCP_GW_KEYS,{}),A=e.LENSMCP_GW_TLS_KEY&&e.LENSMCP_GW_TLS_CERT?{key:(0,node_fs_1.readFileSync)(e.LENSMCP_GW_TLS_KEY),cert:(0,node_fs_1.readFileSync)(e.LENSMCP_GW_TLS_CERT)}:void 0,ee=r.jwt.presenceOnly,D=r.jwt.jwksUrl,M=r.jwt.secret,S=D?(0,jwks_verify_1.createJwksVerifier)({jwksUrl:D,...r.jwt.issuer?{issuer:r.jwt.issuer}:{},...r.jwt.audience?{audience:r.jwt.audience}:{}}):void 0,re=a(s=>{if(!M)return;const t=s.split(".");if(t.length===3)try{if(JSON.parse(Buffer.from(t[0],"base64url").toString()).alg!=="HS256")return;const o=(0,node_crypto_1.createHmac)("sha256",M).update(t[0]+"."+t[1]).digest("base64url"),i=Buffer.from(t[2]),c=Buffer.from(o);if(i.length!==c.length||!(0,node_crypto_1.timingSafeEqual)(i,c))return;const l=JSON.parse(Buffer.from(t[1],"base64url").toString());return(0,jwt_claims_1.checkStandardClaims)(l,{...r.jwt.issuer?{issuer:r.jwt.issuer}:{},...r.jwt.audience?.length?{audience:r.jwt.audience}:{}})?l:void 0}catch{return}},"hs256"),F=a(s=>{const t=String(s.headers.authorization??"");if(!t.startsWith("Bearer "))return;const o=t.slice(7);return S?S.verify(o):re(o)},"bearerClaims"),j=S||M?F:void 0,te=a((s,t)=>{if(s==="jwt"){if(S||M){if(!F(t))throw new Error("invalid or missing token");return}if(!ee)throw new Error("jwt route but no verifier configured (set LENSMCP_GW_JWKS_URL or LENSMCP_GW_JWT_SECRET)");if(!String(t.headers.authorization??"").startsWith("Bearer "))throw new Error("missing bearer token")}},"authenticate"),{attributeHeaders:q,trustProxyHops:se,trustProxyIp:U,uidHeader:x,cookie:J}=r,y=e.LENSMCP_EVENT_FILE,H=y?(()=>{try{return require("@lensmcp/node-instrumentation")}catch{return}})():void 0,K=H?.createEventFloodGuard({burst:30,perSec:5});let B=!1;const I=y?s=>{try{const t=K?K.admit(String(s.fingerprint??"")):{ok:!0,suppressed:0};if(!t.ok)return;B||((0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(y),{recursive:!0}),B=!0);const o=s.raw,i=JSON.stringify(t.suppressed>0?{...s,raw:{...o,floodSuppressed:t.suppressed}}:s)+`
2
+ `;H?.maybeRotateEventFile(y,i.length),(0,node_fs_1.appendFileSync)(y,i)}catch{}}:void 0;if(S)try{await S.refresh(),console.log("[gateway] JWKS loaded")}catch(s){console.error("[gateway] initial JWKS load failed (will retry in background):",s.message)}const{accessLog:oe,clientIpHeader:z,clientIpTrustedProxies:X,undici:d}=r;if(r.mtlsFromRedis&&f){const s=e.LENSMCP_GW_REDIS_PREFIX??"lensmcp:gw",[t,o,i]=await f.mget(`${s}:mtls:cert`,`${s}:mtls:key`,`${s}:mtls:ca`);t&&(d.cert=t),o&&(d.key=o),i&&(d.ca=i),(t||o)&&console.log("[gateway] mTLS materials loaded from Redis")}else e.LENSMCP_GW_MTLS_CERT&&(d.cert=(0,node_fs_1.readFileSync)(e.LENSMCP_GW_MTLS_CERT)),e.LENSMCP_GW_MTLS_KEY&&(d.key=(0,node_fs_1.readFileSync)(e.LENSMCP_GW_MTLS_KEY)),e.LENSMCP_GW_MTLS_CA&&(d.ca=(0,node_fs_1.readFileSync)(e.LENSMCP_GW_MTLS_CA));(0,prod_config_1.finalizeUpstreamTls)(d).autoEnabled&&console.log("[gateway] upstream TLS verification ENABLED (a CA is configured; set LENSMCP_GW_UPSTREAM_TLS_VERIFY=0 to opt out)"),(0,prod_config_1.finalizeUpstreamH2)(d,e)&&console.log(`[gateway] mTLS to upstreams ON over ${d.allowH2?"h2":"h1.1"} (server validation ${d.rejectUnauthorized?"enabled":"OFF"})`);const{upstreamRetries:C,server:Y}=r,T=(0,otel_tracing_1.setupTracing)({...e.OTEL_SERVICE_NAME?{serviceName:e.OTEL_SERVICE_NAME}:{},...e.LENSMCP_GW_OTEL_SAMPLE?{sampleRatio:Number(e.LENSMCP_GW_OTEL_SAMPLE)}:{}});T&&console.log(`[gateway] OTLP tracing on \u2192 ${(0,edge_1.safeUrl)(e.OTEL_EXPORTER_OTLP_ENDPOINT)}`);const{statusEnabled:P,statusToken:L}=r,V=(0,node_os_1.hostname)(),ne=r.statusTtlSec,Q=f,k=P&&Q?s=>{for(const t of s)Q.set(`status:${t.service}:${V}`,JSON.stringify({...t,instance:V,at:Date.now()}),"EX",ne).catch(()=>{})}:void 0;P&&!L&&console.warn("[gateway] LENSMCP_GW_STATUS=1 but no LENSMCP_GW_STATUS_TOKEN \u2014 /statusz stays DISABLED (404) until a token is set (fail-closed). The Redis status:* fan-out still runs."),P&&console.log(`[gateway] status on \u2014 /statusz${L?"":" (endpoint disabled: no token)"}${k?" + Redis status:* fan-out":""}`);const v=r.health?(0,health_check_1.createHealthChecker)(r.health):void 0;v&&console.log(`[gateway] active health-checking on (path ${r.health?.path??"/healthz"})`);let h,G;if(r.rateLimit){const{limit:s,windowMs:t,wantRedis:o,failClosed:i}=r.rateLimit;if(o){const c=r.rateLimit.dedicatedUrl;let l;if(c){const E=require("ioredis");G=new E(c),l=G}else f&&(l=f);l?(h=(0,rate_limit_1.createRedisRateLimiter)(l,{limit:s,windowMs:t,prefix:`${e.LENSMCP_GW_REDIS_PREFIX??"lensmcp:gw"}:rl:`,...r.rateLimit.timeoutMs!==void 0?{timeoutMs:r.rateLimit.timeoutMs}:{},...i?{failOpen:!1}:{},onFailure:a(E=>console.error(`[gateway] RATE LIMITER DEGRADED \u2014 Redis unusable (${E}). Requests are being ${i?"DENIED":"ALLOWED"} without a limit check. Check the ACL covers ${e.LENSMCP_GW_REDIS_PREFIX??"lensmcp:gw"}:rl:*`),"onFailure")}),console.log(`[gateway] rate limit on \u2014 ${s} req / ${t}ms GLOBAL (Redis: ${c?"dedicated":"shared registry"}, fail-${i?"closed":"open"}) per client IP`)):console.warn("[gateway] LENSMCP_GW_RATE_LIMIT_REDIS set but no Redis configured (set LENSMCP_GW_REDIS_URL or LENSMCP_GW_RATE_LIMIT_REDIS_URL) \u2014 falling back to PER-INSTANCE in-memory limiting.")}if(!h){const c=(0,prod_config_1.workerCount)(e),l=(0,prod_config_1.perWorkerLimit)(s,c);h=(0,rate_limit_1.createRateLimiter)({limit:l,windowMs:t}),console.log(`[gateway] rate limit on \u2014 ${s} req / ${t}ms per client IP (in-memory`+(c>1?`, ${l}/worker \xD7 ${c} workers`:", per-instance")+")")}}const W=r.metricsEnabled?(0,metrics_1.createProcessMetrics)():void 0;W&&console.log(`[gateway] process metrics on \u2014 /metricsz${L?" (token-gated)":" (DISABLED: needs LENSMCP_GW_STATUS_TOKEN)"}`);const N=await(0,prod_gateway_lib_1.startProdGateway)({...n.length?{ports:n}:{},manifests:g,...m?{pods:m}:{},serviceKeys:Z,authenticate:te,...A?{tls:A}:{},...I?{emit:I}:{},...j?{identify:j}:{},...q.length?{attributeHeaders:q}:{},...U?{trustProxyIp:U,trustProxyHops:se}:{},...J?{cookie:J}:{},...x?{uidHeader:x}:{},...oe?{accessLog:!0}:{},...z?{clientIpHeader:z}:{},...X.length?{clientIpTrustedProxies:X}:{},...T?{tracing:T}:{},...P?{statusEndpoint:!0}:{},...L?{statusToken:L}:{},...k?{onStatus:k}:{},...v?{healthChecker:v}:{},...h?{rateLimiter:h}:{},...W?{metrics:W}:{},...r.trafficFlushMs!==void 0?{trafficFlushMs:r.trafficFlushMs}:{},...C!==void 0&&Number.isFinite(C)?{upstreamRetries:C}:{},...Object.keys(d).length?{undici:d}:{},...Object.keys(Y).length?{server:Y}:{}});O.current=N,console.log(`[gateway] production gateway up on ${N.ports.join(", ")} (${N.routes().length} routes${I?", lens ON":""})`),u.push(()=>g.stop?.(),()=>$?.(),()=>b?.stop(),()=>G?.quit(),()=>S?.stop(),()=>N.stop(),()=>T?.shutdown())}R(main,"main"),a(main,"main");const WORKERS=(0,prod_config_1.workerCount)(process.env);if(WORKERS>1&&node_cluster_1.default.isPrimary){if(console.log(`[gateway] primary ${process.pid}: forking ${WORKERS} workers (one per core)\u2026`),process.env.LENSMCP_GW_RATE_LIMIT&&!(0,prod_config_1.redisLimiterConfigured)(process.env)){const n=process.env.LENSMCP_GW_RATE_LIMIT_REDIS==="1",u=Number(process.env.LENSMCP_GW_RATE_LIMIT),_=(0,prod_config_1.perWorkerLimit)(u,WORKERS);console.warn(`[gateway] WARNING: ${n?"LENSMCP_GW_RATE_LIMIT_REDIS=1 is set but no Redis URL is \u2014 each worker will fall back to":"using"} the in-memory rate limiter with ${WORKERS} workers. Each worker now enforces ${_} req/window so the total stays \u2248 ${u}; a single client's burst can still land unevenly across workers. Set ${n?"LENSMCP_GW_REDIS_URL (or LENSMCP_GW_RATE_LIMIT_REDIS_URL)":"LENSMCP_GW_RATE_LIMIT_REDIS=1 and a Redis URL"} for one shared global budget.`)}let e=!1;for(let n=0;n<WORKERS;n++)node_cluster_1.default.fork();node_cluster_1.default.on("exit",(n,u,_)=>{e||(console.warn(`[gateway] worker ${n.process.pid} exited (code=${u} sig=${_??""}) \u2014 respawning.`),node_cluster_1.default.fork())});const r=a(n=>{if(!e){e=!0,console.log(`[gateway] primary: ${n} \u2014 stopping ${Object.keys(node_cluster_1.default.workers??{}).length} workers\u2026`);for(const u of Object.values(node_cluster_1.default.workers??{}))try{u?.kill(n)}catch{}setTimeout(()=>process.exit(0),12e3).unref()}},"stopAll");process.on("SIGTERM",()=>r("SIGTERM")),process.on("SIGINT",()=>r("SIGINT"))}else main().catch(e=>{console.error("[gateway] FATAL: could not start \u2014",e instanceof Error?e.stack??e.message:e),process.exit(1)});
@@ -57,6 +57,33 @@ export declare function parseGatewayEnv(env: NodeJS.ProcessEnv): GatewayEnvConfi
57
57
  * Worker processes for multi-core. `auto`/`max`/`0` ⇒ one per core; anything unparseable or
58
58
  * non-positive ⇒ 1, so a typo runs a single healthy gateway rather than none.
59
59
  */
60
+ /**
61
+ * Will the Redis-backed rate limiter actually be used — as opposed to merely asked for?
62
+ *
63
+ * issues/codex-security/31: the primary's multi-worker warning tested the DECLARED INTENT
64
+ * (`LENSMCP_GW_RATE_LIMIT_REDIS=1` or a dedicated URL) and suppressed itself whenever either was
65
+ * present. Whether Redis is reachable at all is decided later, per worker, where the limiter falls
66
+ * back to the per-process in-memory one and logs a line that says nothing about multiplication. So
67
+ * the configuration that most needs the warning — *"I asked for Redis and did not get it"* — is
68
+ * exactly the one that did not print it, and the operator instead saw N copies of a fallback notice
69
+ * that reads like a detail.
70
+ *
71
+ * The primary can settle this before forking, because the answer is entirely in the environment: a
72
+ * dedicated limiter URL, or the shared registry URL that `main()` builds its client from. It cannot
73
+ * know whether that Redis will ANSWER — the limiter's own `onFailure` covers a live outage — but
74
+ * "configured" is the half the warning got wrong.
75
+ */
76
+ export declare function redisLimiterConfigured(env: NodeJS.ProcessEnv): boolean;
77
+ /**
78
+ * The per-process budget for the in-memory limiter, so the CONFIGURED limit is the ENFORCED one.
79
+ *
80
+ * With N workers each owning its own bucket map, Node's cluster scheduler spreads one client's
81
+ * connections across processes and `LENSMCP_GW_RATE_LIMIT` became roughly `N ×` itself. Warning
82
+ * about that is documentation; dividing is the fix. Floors at 1 — a budget of zero would deny every
83
+ * request, which is a worse failure than a slightly generous limit, and `Math.floor` on a limit
84
+ * smaller than the worker count would otherwise reach it.
85
+ */
86
+ export declare function perWorkerLimit(limit: number, workers: number): number;
60
87
  export declare function workerCount(env: NodeJS.ProcessEnv): number;
61
88
  /**
62
89
  * The last word on upstream TLS, applied AFTER the mTLS materials are loaded (which is
@@ -1 +1 @@
1
- "use strict";var w=Object.defineProperty;var o=(e,E)=>w(e,"name",{value:E,configurable:!0});var k=Object.defineProperty,i=o((e,E)=>k(e,"name",{value:E,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.parseGatewayEnv=parseGatewayEnv,exports.workerCount=workerCount,exports.finalizeUpstreamTls=finalizeUpstreamTls,exports.finalizeUpstreamH2=finalizeUpstreamH2;const node_os_1=require("node:os"),num=i(e=>{if(e===void 0||e.trim()==="")return;const E=Number(e);return Number.isFinite(E)?E:void 0},"num"),csv=i(e=>(e??"").split(",").map(E=>E.trim()).filter(Boolean),"csv"),flag=i(e=>e==="1","flag");function parseGatewayEnv(e){const E=csv(e.LENSMCP_GW_PORTS??e.PORT).map(Number).filter(a=>Number.isFinite(a)&&a>=0&&a<=65535),S=Math.max(0,Math.floor(num(e.LENSMCP_GW_TRUST_PROXY)??0)),t={},N=e.LENSMCP_GW_UPSTREAM_TLS_VERIFY;N==="1"?t.rejectUnauthorized=!0:N==="0"&&(t.rejectUnauthorized=!1),flag(e.LENSMCP_GW_UPSTREAM_H2)&&(t.allowH2=!0);const L=num(e.LENSMCP_GW_UNDICI_CONNECTIONS);L!==void 0&&(t.connections=L);const T=num(e.LENSMCP_GW_UNDICI_BODY_TIMEOUT_MS);T!==void 0&&(t.bodyTimeout=T);const P=num(e.LENSMCP_GW_UNDICI_PIPELINING);P!==void 0&&(t.pipelining=P);const W=num(e.LENSMCP_GW_UNDICI_HEADERS_TIMEOUT_MS);W!==void 0&&(t.headersTimeout=W);const I=num(e.LENSMCP_GW_UNDICI_KEEPALIVE_MS);I!==void 0&&(t.keepAliveTimeout=I);const _={},G=num(e.LENSMCP_GW_KEEPALIVE_MS);G!==void 0&&(_.keepAliveTimeout=G);const d=num(e.LENSMCP_GW_REQUEST_TIMEOUT_MS);d!==void 0&&(_.requestTimeout=d);const u=num(e.LENSMCP_GW_HEADERS_TIMEOUT_MS);u!==void 0&&(_.headersTimeout=u);const c=num(e.LENSMCP_GW_MAX_HEADERS);c!==void 0&&(_.maxHeadersCount=c);const l=num(e.LENSMCP_GW_CONN_CHECK_MS);l!==void 0&&(_.connectionsCheckingInterval=l);const R=num(e.LENSMCP_GW_MAX_CONNECTIONS);R!==void 0&&(_.maxConnections=R);const A=num(e.LENSMCP_GW_BODY_STALL_MS);A!==void 0&&(_.bodyStallMs=A);const M=e.LENSMCP_GW_COOKIE_SECRET,r=e.LENSMCP_GW_COOKIE_NAME,n=e.LENSMCP_GW_COOKIE_DOMAIN,m=num(e.LENSMCP_GW_COOKIE_MAXAGE),U=M||r||n?{...r?{name:r}:{},...M?{secret:M}:{},...n?{domain:n}:{},...m!==void 0?{maxAge:m}:{}}:void 0,v=num(e.LENSMCP_GW_RATE_LIMIT),s=e.LENSMCP_GW_RATE_LIMIT_REDIS_URL,O=num(e.LENSMCP_GW_RATE_LIMIT_TIMEOUT_MS),p=v===void 0?void 0:{limit:v,windowMs:num(e.LENSMCP_GW_RATE_WINDOW_MS)??6e4,wantRedis:flag(e.LENSMCP_GW_RATE_LIMIT_REDIS)||!!s,...s?{dedicatedUrl:s}:{},...O!==void 0?{timeoutMs:O}:{},failClosed:flag(e.LENSMCP_GW_RATE_LIMIT_FAIL_CLOSED)},H=e.LENSMCP_GW_HEALTH_PATH,D=num(e.LENSMCP_GW_HEALTH_INTERVAL_MS),f=num(e.LENSMCP_GW_HEALTH_TIMEOUT_MS),h=flag(e.LENSMCP_GW_HEALTH_CHECK),b=h?{...H?{path:H}:{},...D!==void 0?{intervalMs:D}:{},...f!==void 0?{timeoutMs:f}:{}}:void 0,C=num(e.LENSMCP_GW_TRAFFIC_FLUSH_MS),x=csv(e.LENSMCP_GW_JWT_AUD);return{ports:E,workers:workerCount(e),undici:t,server:_,...num(e.LENSMCP_GW_UPSTREAM_RETRIES)!==void 0?{upstreamRetries:num(e.LENSMCP_GW_UPSTREAM_RETRIES)}:{},trustProxyIp:S>0,trustProxyHops:S,...U?{cookie:U}:{},attributeHeaders:csv(e.LENSMCP_GW_ATTR_HEADERS),...e.LENSMCP_GW_CLIENT_IP_HEADER?{clientIpHeader:e.LENSMCP_GW_CLIENT_IP_HEADER}:{},clientIpTrustedProxies:csv(e.LENSMCP_GW_CLIENT_IP_TRUSTED_PROXIES),...e.LENSMCP_GW_UID_HEADER?{uidHeader:e.LENSMCP_GW_UID_HEADER}:{},accessLog:flag(e.LENSMCP_GW_ACCESS_LOG),statusEnabled:flag(e.LENSMCP_GW_STATUS),...e.LENSMCP_GW_STATUS_TOKEN?{statusToken:e.LENSMCP_GW_STATUS_TOKEN}:{},metricsEnabled:flag(e.LENSMCP_GW_METRICS),statusTtlSec:Math.max(3,Math.ceil((C??5e3)/1e3*3)),...C!==void 0?{trafficFlushMs:C}:{},...p?{rateLimit:p}:{},...b?{health:b}:{},healthCheckEnabled:h,jwt:{...e.LENSMCP_GW_JWKS_URL?{jwksUrl:e.LENSMCP_GW_JWKS_URL}:{},...e.LENSMCP_GW_JWT_SECRET?{secret:e.LENSMCP_GW_JWT_SECRET}:{},...e.LENSMCP_GW_JWT_ISS?{issuer:e.LENSMCP_GW_JWT_ISS}:{},...x.length?{audience:x}:{},presenceOnly:flag(e.LENSMCP_GW_JWT_PRESENCE_ONLY)},mtlsFromRedis:flag(e.LENSMCP_GW_MTLS_FROM_REDIS)}}o(parseGatewayEnv,"parseGatewayEnv"),i(parseGatewayEnv,"parseGatewayEnv");function workerCount(e){const E=(e.LENSMCP_GW_WORKERS??"1").trim().toLowerCase();if(E==="auto"||E==="max"||E==="0")return Math.max(1,(0,node_os_1.cpus)().length);const S=Math.floor(Number(E));return Number.isFinite(S)&&S>0?S:1}o(workerCount,"workerCount"),i(workerCount,"workerCount");function finalizeUpstreamTls(e){return e.ca&&e.rejectUnauthorized===void 0?(e.rejectUnauthorized=!0,{autoEnabled:!0}):{autoEnabled:!1}}o(finalizeUpstreamTls,"finalizeUpstreamTls"),i(finalizeUpstreamTls,"finalizeUpstreamTls");function finalizeUpstreamH2(e,E){return e.cert&&e.key?(E.LENSMCP_GW_UPSTREAM_H2!=="0"&&(e.allowH2=!0),!0):!1}o(finalizeUpstreamH2,"finalizeUpstreamH2"),i(finalizeUpstreamH2,"finalizeUpstreamH2");
1
+ "use strict";var w=Object.defineProperty;var S=(_,E)=>w(_,"name",{value:E,configurable:!0});var k=Object.defineProperty,t=S((_,E)=>k(_,"name",{value:E,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.parseGatewayEnv=parseGatewayEnv,exports.redisLimiterConfigured=redisLimiterConfigured,exports.perWorkerLimit=perWorkerLimit,exports.workerCount=workerCount,exports.finalizeUpstreamTls=finalizeUpstreamTls,exports.finalizeUpstreamH2=finalizeUpstreamH2;const node_os_1=require("node:os"),num=t(_=>{if(_===void 0||_.trim()==="")return;const E=Number(_);return Number.isFinite(E)?E:void 0},"num"),csv=t(_=>(_??"").split(",").map(E=>E.trim()).filter(Boolean),"csv"),flag=t(_=>_==="1","flag");function parseGatewayEnv(_){const E=csv(_.LENSMCP_GW_PORTS??_.PORT).map(Number).filter(L=>Number.isFinite(L)&&L>=0&&L<=65535),r=Math.max(0,Math.floor(num(_.LENSMCP_GW_TRUST_PROXY)??0)),i={},N=_.LENSMCP_GW_UPSTREAM_TLS_VERIFY;N==="1"?i.rejectUnauthorized=!0:N==="0"&&(i.rejectUnauthorized=!1),flag(_.LENSMCP_GW_UPSTREAM_H2)&&(i.allowH2=!0);const a=num(_.LENSMCP_GW_UNDICI_CONNECTIONS);a!==void 0&&(i.connections=a);const T=num(_.LENSMCP_GW_UNDICI_BODY_TIMEOUT_MS);T!==void 0&&(i.bodyTimeout=T);const P=num(_.LENSMCP_GW_UNDICI_PIPELINING);P!==void 0&&(i.pipelining=P);const W=num(_.LENSMCP_GW_UNDICI_HEADERS_TIMEOUT_MS);W!==void 0&&(i.headersTimeout=W);const I=num(_.LENSMCP_GW_UNDICI_KEEPALIVE_MS);I!==void 0&&(i.keepAliveTimeout=I);const o={},u=num(_.LENSMCP_GW_KEEPALIVE_MS);u!==void 0&&(o.keepAliveTimeout=u);const d=num(_.LENSMCP_GW_REQUEST_TIMEOUT_MS);d!==void 0&&(o.requestTimeout=d);const G=num(_.LENSMCP_GW_HEADERS_TIMEOUT_MS);G!==void 0&&(o.headersTimeout=G);const c=num(_.LENSMCP_GW_MAX_HEADERS);c!==void 0&&(o.maxHeadersCount=c);const R=num(_.LENSMCP_GW_CONN_CHECK_MS);R!==void 0&&(o.connectionsCheckingInterval=R);const l=num(_.LENSMCP_GW_MAX_CONNECTIONS);l!==void 0&&(o.maxConnections=l);const m=num(_.LENSMCP_GW_BODY_STALL_MS);m!==void 0&&(o.bodyStallMs=m);const e=_.LENSMCP_GW_COOKIE_SECRET,M=_.LENSMCP_GW_COOKIE_NAME,n=_.LENSMCP_GW_COOKIE_DOMAIN,A=num(_.LENSMCP_GW_COOKIE_MAXAGE),U=e||M||n?{...M?{name:M}:{},...e?{secret:e}:{},...n?{domain:n}:{},...A!==void 0?{maxAge:A}:{}}:void 0,v=num(_.LENSMCP_GW_RATE_LIMIT),s=_.LENSMCP_GW_RATE_LIMIT_REDIS_URL,p=num(_.LENSMCP_GW_RATE_LIMIT_TIMEOUT_MS),O=v===void 0?void 0:{limit:v,windowMs:num(_.LENSMCP_GW_RATE_WINDOW_MS)??6e4,wantRedis:flag(_.LENSMCP_GW_RATE_LIMIT_REDIS)||!!s,...s?{dedicatedUrl:s}:{},...p!==void 0?{timeoutMs:p}:{},failClosed:flag(_.LENSMCP_GW_RATE_LIMIT_FAIL_CLOSED)},f=_.LENSMCP_GW_HEALTH_PATH,H=num(_.LENSMCP_GW_HEALTH_INTERVAL_MS),D=num(_.LENSMCP_GW_HEALTH_TIMEOUT_MS),h=flag(_.LENSMCP_GW_HEALTH_CHECK),b=h?{...f?{path:f}:{},...H!==void 0?{intervalMs:H}:{},...D!==void 0?{timeoutMs:D}:{}}:void 0,C=num(_.LENSMCP_GW_TRAFFIC_FLUSH_MS),x=csv(_.LENSMCP_GW_JWT_AUD);return{ports:E,workers:workerCount(_),undici:i,server:o,...num(_.LENSMCP_GW_UPSTREAM_RETRIES)!==void 0?{upstreamRetries:num(_.LENSMCP_GW_UPSTREAM_RETRIES)}:{},trustProxyIp:r>0,trustProxyHops:r,...U?{cookie:U}:{},attributeHeaders:csv(_.LENSMCP_GW_ATTR_HEADERS),..._.LENSMCP_GW_CLIENT_IP_HEADER?{clientIpHeader:_.LENSMCP_GW_CLIENT_IP_HEADER}:{},clientIpTrustedProxies:csv(_.LENSMCP_GW_CLIENT_IP_TRUSTED_PROXIES),..._.LENSMCP_GW_UID_HEADER?{uidHeader:_.LENSMCP_GW_UID_HEADER}:{},accessLog:flag(_.LENSMCP_GW_ACCESS_LOG),statusEnabled:flag(_.LENSMCP_GW_STATUS),..._.LENSMCP_GW_STATUS_TOKEN?{statusToken:_.LENSMCP_GW_STATUS_TOKEN}:{},metricsEnabled:flag(_.LENSMCP_GW_METRICS),statusTtlSec:Math.max(3,Math.ceil((C??5e3)/1e3*3)),...C!==void 0?{trafficFlushMs:C}:{},...O?{rateLimit:O}:{},...b?{health:b}:{},healthCheckEnabled:h,jwt:{..._.LENSMCP_GW_JWKS_URL?{jwksUrl:_.LENSMCP_GW_JWKS_URL}:{},..._.LENSMCP_GW_JWT_SECRET?{secret:_.LENSMCP_GW_JWT_SECRET}:{},..._.LENSMCP_GW_JWT_ISS?{issuer:_.LENSMCP_GW_JWT_ISS}:{},...x.length?{audience:x}:{},presenceOnly:flag(_.LENSMCP_GW_JWT_PRESENCE_ONLY)},mtlsFromRedis:flag(_.LENSMCP_GW_MTLS_FROM_REDIS)}}S(parseGatewayEnv,"parseGatewayEnv"),t(parseGatewayEnv,"parseGatewayEnv");function redisLimiterConfigured(_){return _.LENSMCP_GW_RATE_LIMIT_REDIS_URL?!0:flag(_.LENSMCP_GW_RATE_LIMIT_REDIS)&&!!_.LENSMCP_GW_REDIS_URL}S(redisLimiterConfigured,"redisLimiterConfigured"),t(redisLimiterConfigured,"redisLimiterConfigured");function perWorkerLimit(_,E){if(!Number.isFinite(_)||_<=0)return _;const r=Number.isFinite(E)&&E>1?Math.floor(E):1;return Math.max(1,Math.floor(_/r))}S(perWorkerLimit,"perWorkerLimit"),t(perWorkerLimit,"perWorkerLimit");function workerCount(_){const E=(_.LENSMCP_GW_WORKERS??"1").trim().toLowerCase();if(E==="auto"||E==="max"||E==="0")return Math.max(1,(0,node_os_1.cpus)().length);const r=Math.floor(Number(E));return Number.isFinite(r)&&r>0?r:1}S(workerCount,"workerCount"),t(workerCount,"workerCount");function finalizeUpstreamTls(_){return _.ca&&_.rejectUnauthorized===void 0?(_.rejectUnauthorized=!0,{autoEnabled:!0}):{autoEnabled:!1}}S(finalizeUpstreamTls,"finalizeUpstreamTls"),t(finalizeUpstreamTls,"finalizeUpstreamTls");function finalizeUpstreamH2(_,E){return _.cert&&_.key?(E.LENSMCP_GW_UPSTREAM_H2!=="0"&&(_.allowH2=!0),!0):!1}S(finalizeUpstreamH2,"finalizeUpstreamH2"),t(finalizeUpstreamH2,"finalizeUpstreamH2");
@@ -64,6 +64,15 @@ export interface RedisRateLimiterOptions {
64
64
  timeoutMs?: number;
65
65
  /** On Redis error/timeout: true → allow (default), false → deny. */
66
66
  failOpen?: boolean;
67
+ /**
68
+ * Called ONCE, the first time the limiter falls back to its fail policy.
69
+ *
70
+ * A limiter that fails open answers "allowed" to everything and says nothing, while startup has
71
+ * already logged that GLOBAL rate limiting is on — so a namespace/ACL mismatch looks exactly like
72
+ * a working limiter (issues/codex-security/34). Once, not per request: the point is to be noticed,
73
+ * not to become the flood.
74
+ */
75
+ onFailure?: (reason: string) => void;
67
76
  }
68
77
  /** The single ioredis method the limiter needs — an atomic script eval. */
69
78
  export interface RateLimitRedis {
@@ -1,4 +1,4 @@
1
- "use strict";var A=Object.defineProperty;var u=(r,e)=>A(r,"name",{value:e,configurable:!0});var R=Object.defineProperty,a=u((r,e)=>R(r,"name",{value:e,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRateLimiter=createRateLimiter,exports.createRedisRateLimiter=createRedisRateLimiter;function createRateLimiter(r){const e=Math.max(1,r.limit),i=Math.max(1,r.windowMs),n=r.now??Date.now,c=e/i,s=new Map,m=a((l,o)=>Math.min(e,l.tokens+(o-l.last)*c),"refilled"),f=setInterval(()=>{const l=n();for(const[o,t]of s)m(t,l)>=e&&s.delete(o)},i);return f.unref?.(),{check(l){const o=n();let t=s.get(l);return t||(t={tokens:e,last:o},s.set(l,t)),t.tokens=m(t,o),t.last=o,t.tokens>=1?(t.tokens-=1,{allowed:!0,remaining:Math.floor(t.tokens),retryAfterMs:0}):{allowed:!1,remaining:0,retryAfterMs:Math.ceil((1-t.tokens)/c)}},stop(){clearInterval(f)}}}u(createRateLimiter,"createRateLimiter"),a(createRateLimiter,"createRateLimiter");const BUCKET_LUA=`
1
+ "use strict";var E=Object.defineProperty;var k=(a,e)=>E(a,"name",{value:e,configurable:!0});var A=Object.defineProperty,c=k((a,e)=>A(a,"name",{value:e,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRateLimiter=createRateLimiter,exports.createRedisRateLimiter=createRedisRateLimiter;function createRateLimiter(a){const e=Math.max(1,a.limit),s=Math.max(1,a.windowMs),n=a.now??Date.now,m=e/s,o=new Map,f=c((l,r)=>Math.min(e,l.tokens+(r-l.last)*m),"refilled"),u=setInterval(()=>{const l=n();for(const[r,t]of o)f(t,l)>=e&&o.delete(r)},s);return u.unref?.(),{check(l){const r=n();let t=o.get(l);return t||(t={tokens:e,last:r},o.set(l,t)),t.tokens=f(t,r),t.last=r,t.tokens>=1?(t.tokens-=1,{allowed:!0,remaining:Math.floor(t.tokens),retryAfterMs:0}):{allowed:!1,remaining:0,retryAfterMs:Math.ceil((1-t.tokens)/m)}},stop(){clearInterval(u)}}}k(createRateLimiter,"createRateLimiter"),c(createRateLimiter,"createRateLimiter");const BUCKET_LUA=`
2
2
  local b = redis.call('HMGET', KEYS[1], 't', 's')
3
3
  local cap = tonumber(ARGV[1])
4
4
  local refill = tonumber(ARGV[2])
@@ -15,4 +15,4 @@ redis.call('HSET', KEYS[1], 't', tokens, 's', now)
15
15
  redis.call('PEXPIRE', KEYS[1], ttl)
16
16
  local retry = 0
17
17
  if ok == 0 then retry = math.ceil((1 - tokens) / refill) end
18
- return {ok, math.floor(tokens), retry}`,RL_TIMEOUT=Symbol("rl-timeout"),RL_FAIL=Symbol("rl-fail");function createRedisRateLimiter(r,e){const i=Math.max(1,e.limit),n=Math.max(1,e.windowMs),c=e.prefix??"lensmcp:gw:rl:",s=Math.max(1,e.timeoutMs??100),m=e.failOpen!==!1,f=i/n,l=Math.ceil(n)+1e3,o=a(()=>m?{allowed:!0,remaining:i,retryAfterMs:0}:{allowed:!1,remaining:0,retryAfterMs:n},"onFail");return{async check(t){let M;const w=Promise.resolve(r.eval(BUCKET_LUA,1,`${c}${t}`,i,f,l)).then(d=>d,()=>RL_FAIL),h=new Promise(d=>{M=setTimeout(()=>d(RL_TIMEOUT),s)}),k=await Promise.race([w,h]);if(clearTimeout(M),k===RL_TIMEOUT||k===RL_FAIL)return o();const[b,y,p]=k;return b===1?{allowed:!0,remaining:Number(y)||0,retryAfterMs:0}:{allowed:!1,remaining:0,retryAfterMs:Number(p)||n}},stop(){}}}u(createRedisRateLimiter,"createRedisRateLimiter"),a(createRedisRateLimiter,"createRedisRateLimiter");
18
+ return {ok, math.floor(tokens), retry}`,RL_TIMEOUT=Symbol("rl-timeout"),RL_FAIL=Symbol("rl-fail");function createRedisRateLimiter(a,e){const s=Math.max(1,e.limit),n=Math.max(1,e.windowMs),m=e.prefix??"lensmcp:gw:rl:",o=Math.max(1,e.timeoutMs??100),f=e.failOpen!==!1,u=s/n,l=Math.ceil(n)+1e3;let r=!1;const t=c(M=>(r||(r=!0,e.onFailure?.(M)),f?{allowed:!0,remaining:s,retryAfterMs:0}:{allowed:!1,remaining:0,retryAfterMs:n}),"onFail");return{async check(M){let w,h="redis unavailable";const b=Promise.resolve(a.eval(BUCKET_LUA,1,`${m}${M}`,s,u,l)).then(i=>i,i=>(h=String(i?.message??i),RL_FAIL)),R=new Promise(i=>{w=setTimeout(()=>i(RL_TIMEOUT),o)}),d=await Promise.race([b,R]);if(clearTimeout(w),d===RL_TIMEOUT||d===RL_FAIL)return t(d===RL_TIMEOUT?`redis timed out after ${o}ms`:h);const[y,p,g]=d;return y===1?{allowed:!0,remaining:Number(p)||0,retryAfterMs:0}:{allowed:!1,remaining:0,retryAfterMs:Number(g)||n}},stop(){}}}k(createRedisRateLimiter,"createRedisRateLimiter"),c(createRedisRateLimiter,"createRedisRateLimiter");
@@ -1 +1 @@
1
- "use strict";var y=Object.defineProperty;var c=(i,n)=>y(i,"name",{value:n,configurable:!0});var f=Object.defineProperty,l=c((i,n)=>f(i,"name",{value:n,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),dev_auth_1=require("./dev-auth"),edge_1=require("./edge");function createEdgeAuth(i,n,k){const{traceStep:a}=n;return{internal:l((e,h,t)=>{const u=String(e.headers["x-api-key"]??""),r=i.keyToProject.get(u);if(!r)return a(t,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};e.headers["x-api-key-id"]=r;const o=i.keyToWorkspace?.get(u);return o?e.headers["x-api-key-workspace"]=o:delete e.headers["x-api-key-workspace"],e.__lensmcpCaller=r,delete e.headers["x-api-key"],a(t,"auth",{mode:"x-api-key",ok:!0,caller:r,...o?{callerWorkspace:o}:{}}),{ok:!0,caller:r}},"internal"),public:l((e,h,t,u)=>{const r=h.auth;if(r){const o=u??(0,edge_1.canonicalPath)(e.url??"/")??(e.url??"/").split("?")[0],p=(e.method??"GET").toUpperCase(),s=(0,manifest_1.authRuleFor)(r,o,p);if(s.mode==="jwt"){const m=String(e.headers.host??"").split(":")[0].toLowerCase(),d=(0,dev_auth_1.verifyDevJwt)(e,k?.for(m));return d?s.rule&&!(0,manifest_1.evalRule)(s.rule,(0,dev_auth_1.devAttrs)(e,d,o))?(a(t,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):((0,dev_auth_1.stampIdentity)(e,d),a(t,"auth",{mode:"jwt",ok:!0}),{ok:!0,claims:d}):(a(t,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"})}return a(t,"auth",{mode:s.mode}),{ok:!0}}return a(t,"auth",{mode:"public",unconfigured:!0}),{ok:!0}},"publicAuth")}}c(createEdgeAuth,"createEdgeAuth"),l(createEdgeAuth,"createEdgeAuth");
1
+ "use strict";var y=Object.defineProperty;var p=(i,s)=>y(i,"name",{value:s,configurable:!0});var f=Object.defineProperty,d=p((i,s)=>f(i,"name",{value:s,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),dev_auth_1=require("./dev-auth"),edge_1=require("./edge");function createEdgeAuth(i,s,h){const{traceStep:o}=s;return{internal:d((e,k,r)=>{const u=String(e.headers["x-api-key"]??""),a=i.keyToProject.get(u);if(!a)return o(r,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};const t=i.keyToWorkspace?.get(u),n=k.svc?.wsKey;return t&&n&&t!==n?(o(r,"auth",{mode:"x-api-key",ok:!1,reason:"cross-workspace",caller:a,callerWorkspace:t,routeWorkspace:n}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."}):(e.headers["x-api-key-id"]=a,t?e.headers["x-api-key-workspace"]=t:delete e.headers["x-api-key-workspace"],e.__lensmcpCaller=a,delete e.headers["x-api-key"],o(r,"auth",{mode:"x-api-key",ok:!0,caller:a,...t?{callerWorkspace:t}:{}}),{ok:!0,caller:a})},"internal"),public:d((e,k,r,u)=>{const a=k.auth;if(a){const t=u??(0,edge_1.canonicalPath)(e.url??"/")??(e.url??"/").split("?")[0],n=(e.method??"GET").toUpperCase(),l=(0,manifest_1.authRuleFor)(a,t,n);if(l.mode==="jwt"){const m=String(e.headers.host??"").split(":")[0].toLowerCase(),c=(0,dev_auth_1.verifyDevJwt)(e,h?.for(m));return c?l.rule&&!(0,manifest_1.evalRule)(l.rule,(0,dev_auth_1.devAttrs)(e,c,t))?(o(r,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):((0,dev_auth_1.stampIdentity)(e,c),o(r,"auth",{mode:"jwt",ok:!0}),{ok:!0,claims:c}):(o(r,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"})}return o(r,"auth",{mode:l.mode}),{ok:!0}}return o(r,"auth",{mode:"public",unconfigured:!0}),{ok:!0}},"publicAuth")}}p(createEdgeAuth,"createEdgeAuth"),d(createEdgeAuth,"createEdgeAuth");
@@ -1,13 +1,34 @@
1
- /**
2
- * The multi-workspace LANDING page — served at `lensmcp.local/` (the dashboard host root, which otherwise
3
- * 404s because the dashboard itself mounts at `/<key>`). It lists every workspace in the global registry
4
- * (`workspace-registry.ts`), marks which are LIVE (their gateway process is alive) and which one is running
5
- * HERE, and links each live one to its dashboard. The visible half of the shared multi-workspace gateway
6
- * (planning/multi-workspace-gateway.md).
7
- */
8
1
  import type * as http from 'node:http';
9
2
  import { type WorkspaceEntry } from './workspace-registry';
3
+ /**
4
+ * Is this connection from the machine the gateway runs on?
5
+ *
6
+ * The SOCKET peer, deliberately — not the resolved client IP. `client-ip.ts` honours
7
+ * `x-forwarded-for` because a real edge sits in front of the prod gateway, and a header is exactly
8
+ * what an attacker controls. For "may this caller see the local topology", the only trustworthy
9
+ * answer is who actually opened the TCP connection.
10
+ */
11
+ export declare function isLoopbackRequest(req: Pick<http.IncomingMessage, 'socket'>): boolean;
12
+ /**
13
+ * What the chooser shows instead of the absolute workspace root.
14
+ *
15
+ * The registry stores absolute paths because the gateway needs them; the LANDING PAGE needs only
16
+ * enough to tell two workspaces apart. An absolute root carries the developer's username and the
17
+ * machine's directory layout, and this page used to render every registered one — including
18
+ * workspaces whose gateways are not even running (issues/codex-security/26).
19
+ *
20
+ * Home-relative, last two segments: `/Users/dana/work/acme/web` → `~/…/acme/web`.
21
+ */
22
+ export declare function displayRoot(root: string, home?: string): string;
10
23
  /** Render the chooser HTML from the registry entries (+ which key is running in THIS gateway). */
11
24
  export declare function renderChooserPage(entries: WorkspaceEntry[], currentKey: string): string;
12
- /** Serve the chooser at the dashboard-host root. Reads the registry fresh so live status is current. */
13
- export declare function serveChooser(res: http.ServerResponse, currentKey: string): void;
25
+ /**
26
+ * Serve the chooser at the dashboard-host root. Reads the registry fresh so live status is current.
27
+ *
28
+ * LOOPBACK ONLY. This is a listing of every workspace the developer has ever registered — names,
29
+ * roots and whether each gateway is running — and it used to be served as an *unrouted* request,
30
+ * which means before route authentication and before the configured after-auth middleware
31
+ * (issues/codex-security/26). A caller from elsewhere gets the same 404 any other unrouted path
32
+ * gets: a landing page that announces itself to the network is not a 404 page.
33
+ */
34
+ export declare function serveChooser(res: http.ServerResponse, currentKey: string, req?: Pick<http.IncomingMessage, 'socket'>): void;
@@ -1,11 +1,11 @@
1
- "use strict";var l=Object.defineProperty;var n=(a,o)=>l(a,"name",{value:o,configurable:!0});var p=Object.defineProperty,t=n((a,o)=>p(a,"name",{value:o,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.renderChooserPage=renderChooserPage,exports.serveChooser=serveChooser;const workspace_registry_1=require("./workspace-registry"),esc=t(a=>a.replace(/[&<>"']/g,o=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[o]),"esc");function renderChooserPage(a,o){const r=a.map(e=>({...e,live:e.key===o||(0,workspace_registry_1.isWorkspaceLive)(e)})).sort((e,s)=>Number(s.live)-Number(e.live)||e.key.localeCompare(s.key)),c=r.length===0?'<p class="empty">No workspaces registered yet. Start a gateway in a workspace (<code>lensmcp gateway start</code>) and it will appear here.</p>':r.map(e=>{const s=e.key===o?'<span class="badge here">running here</span>':e.live?'<span class="badge live">live</span>':'<span class="badge off">offline</span>',d=e.live?'<span class="open">Open dashboard \u2192</span>':'<span class="hint">start its gateway to open</span>',i=`<div class="card-main">
2
- <div class="key">${esc(e.key)} ${s}</div>
3
- <div class="root">${esc(e.root)}</div>
1
+ "use strict";var l=Object.defineProperty;var i=(o,e)=>l(o,"name",{value:e,configurable:!0});var p=Object.defineProperty,n=i((o,e)=>p(o,"name",{value:e,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isLoopbackRequest=isLoopbackRequest,exports.displayRoot=displayRoot,exports.renderChooserPage=renderChooserPage,exports.serveChooser=serveChooser;const node_os_1=require("node:os"),workspace_registry_1=require("./workspace-registry"),esc=n(o=>o.replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e]),"esc");function isLoopbackRequest(o){const e=o.socket?.remoteAddress;if(!e)return!1;const t=e.startsWith("::ffff:")?e.slice(7):e;return t==="::1"||t==="127.0.0.1"||/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(t)}i(isLoopbackRequest,"isLoopbackRequest"),n(isLoopbackRequest,"isLoopbackRequest");function displayRoot(o,e=(0,node_os_1.homedir)()){const t=o.replace(/\/+$/,"").split("/").filter(Boolean),r=t.slice(-2).join("/");if(!r)return"/";const a=e!==""&&(o===e||o.startsWith(`${e}/`)),s=t.length>2;return a?s?`~/\u2026/${r}`:`~/${r}`:s?`\u2026/${r}`:`/${r}`}i(displayRoot,"displayRoot"),n(displayRoot,"displayRoot");function renderChooserPage(o,e){const t=o.map(a=>({...a,live:a.key===e||(0,workspace_registry_1.isWorkspaceLive)(a)})).sort((a,s)=>Number(s.live)-Number(a.live)||a.key.localeCompare(s.key)),r=t.length===0?'<p class="empty">No workspaces registered yet. Start a gateway in a workspace (<code>lensmcp gateway start</code>) and it will appear here.</p>':t.map(a=>{const s=a.key===e?'<span class="badge here">running here</span>':a.live?'<span class="badge live">live</span>':'<span class="badge off">offline</span>',d=a.live?'<span class="open">Open dashboard \u2192</span>':'<span class="hint">start its gateway to open</span>',c=`<div class="card-main">
2
+ <div class="key">${esc(a.key)} ${s}</div>
3
+ <div class="root">${esc(displayRoot(a.root))}</div>
4
4
  </div>
5
- <div class="card-action">${d}</div>`;return e.live?`<a class="card is-live" href="${esc(e.base)}/">
6
- ${i}
5
+ <div class="card-action">${d}</div>`;return a.live?`<a class="card is-live" href="${esc(a.base)}/">
6
+ ${c}
7
7
  </a>`:`<div class="card is-off">
8
- ${i}
8
+ ${c}
9
9
  </div>`}).join(`
10
10
  `);return`<!doctype html>
11
11
  <html lang="en"><head>
@@ -42,10 +42,10 @@
42
42
  <body><div class="wrap">
43
43
  <header>
44
44
  <h1>LensMCP workspaces</h1>
45
- <p class="sub">Pick a workspace to open its dashboard. ${r.filter(e=>e.live).length} live \xB7 ${r.length} known.</p>
45
+ <p class="sub">Pick a workspace to open its dashboard. ${t.filter(a=>a.live).length} live \xB7 ${t.length} known.</p>
46
46
  </header>
47
47
  <div class="list">
48
- ${c}
48
+ ${r}
49
49
  </div>
50
50
  <footer>Served by the gateway on <code>lensmcp.local</code>. This list updates as gateways start and stop.</footer>
51
- </div></body></html>`}n(renderChooserPage,"renderChooserPage"),t(renderChooserPage,"renderChooserPage");function serveChooser(a,o){const r=renderChooserPage((0,workspace_registry_1.readWorkspaces)(),o);a.writeHead(200,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),a.end(r)}n(serveChooser,"serveChooser"),t(serveChooser,"serveChooser");
51
+ </div></body></html>`}i(renderChooserPage,"renderChooserPage"),n(renderChooserPage,"renderChooserPage");function serveChooser(o,e,t){if(t&&!isLoopbackRequest(t)){o.writeHead(404,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),o.end("not found");return}const r=renderChooserPage((0,workspace_registry_1.readWorkspaces)(),e);o.writeHead(200,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),o.end(r)}i(serveChooser,"serveChooser"),n(serveChooser,"serveChooser");
@@ -28,6 +28,21 @@ export interface ControlServer {
28
28
  * resolves nothing and reports nothing — absent means "cannot vouch", never a guess.
29
29
  */
30
30
  export declare function clusterVersion(): string | undefined;
31
- /** Start the control listener. The socket is loopback-only (a filesystem socket), so no IP allow-listing
32
- * is needed — anything that can open it is already on this machine as this user. */
31
+ /**
32
+ * Make `dir` exist and be readable/writable by its owner ONLY.
33
+ *
34
+ * A filesystem socket is unreachable off-host, which is what the header below used to lean on — but
35
+ * it is perfectly reachable by other USERS, and the project's threat model does not trust them.
36
+ * Nothing enforced owner-only access: `mkdirSync` supplied no mode, an existing directory was
37
+ * accepted whatever its permissions, and `LENSMCP_HOME` can point the whole thing somewhere
38
+ * permissive. Under a cooperative `0002` umask that left a group-writable control plane
39
+ * (issues/codex-security/25).
40
+ *
41
+ * Returns false when the path exists and is not a directory — the caller decides what to do rather
42
+ * than proceeding on a bad assumption.
43
+ */
44
+ export declare function ensureOwnerOnlyDir(dir: string): boolean;
45
+ /** Start the control listener. A filesystem socket is unreachable off-host, but NOT unreachable by
46
+ * other users on this machine — so the home is owner-only and the socket is 0600
47
+ * (issues/codex-security/25). */
33
48
  export declare function startControlServer(deps: ControlServerDeps): ControlServer;
@@ -1,2 +1,2 @@
1
- "use strict";var h=Object.defineProperty;var p=(s,e)=>h(s,"name",{value:e,configurable:!0});var f=Object.defineProperty,l=p((s,e)=>f(s,"name",{value:e,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.controlSocketPath=controlSocketPath,exports.clusterVersion=clusterVersion,exports.startControlServer=startControlServer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log");function controlSocketPath(){const s=process.env.LENSMCP_HOME||os.homedir();return path.join(s,".lensmcp","control.sock")}p(controlSocketPath,"controlSocketPath"),l(controlSocketPath,"controlSocketPath");function clusterVersion(){let s=__dirname;for(let e=0;e<6;e++){const n=path.join(s,"package.json");if(fs.existsSync(n))try{const c=JSON.parse(fs.readFileSync(n,"utf8"));if(c.name==="@lensmcp/cluster"&&typeof c.version=="string")return c.version}catch{}const o=path.dirname(s);if(o===s)break;s=o}}p(clusterVersion,"clusterVersion"),l(clusterVersion,"clusterVersion");function readJsonBody(s){return new Promise((e,n)=>{let o="";s.on("data",c=>{o+=c,o.length>1e6&&n(new Error("body too large"))}),s.on("end",()=>{try{e(o?JSON.parse(o):{})}catch(c){n(c)}}),s.on("error",n)})}p(readJsonBody,"readJsonBody"),l(readJsonBody,"readJsonBody");function startControlServer(s){const{rt:e}=s,n=controlSocketPath();fs.mkdirSync(path.dirname(n),{recursive:!0});try{fs.unlinkSync(n)}catch{}const o=l((i,a,d)=>{i.writeHead(a,{"content-type":"application/json"}),i.end(JSON.stringify(d))},"send"),c=http.createServer((i,a)=>{(async()=>{try{const d=(i.url??"").split("?")[0];if(i.method==="GET"&&d==="/list")return o(a,200,{workspaces:e.registry.keys().map(r=>{const t=e.registry.get(r);return{key:r,root:t?.root,routes:t?.routes.length??0,services:t?.services.length??0}})});if(i.method==="GET"&&d==="/status"){const r=clusterVersion();return o(a,200,{daemon:{pid:process.pid,wsKey:e.wsKey,...r?{version:r}:{}},workspaces:e.registry.keys().map(t=>{const y=e.registry.get(t);return{key:t,root:y?.root,routes:y?.routes.length??0,services:y?.services.length??0,isDaemon:t===e.wsKey}}),services:e.registry.allServices().map(t=>({project:t.project,wsKey:t.wsKey,host:t.decl.host,state:t.state}))})}if(i.method==="POST"&&d==="/register"){const r=await readJsonBody(i);if(!r.wsKey||!r.root)return o(a,400,{error:"wsKey and root are required"});const t=r.projects??{},y=(0,discovery_1.projectSourceRoots)(r.root,t,r.projectDependencies),{routes:w,services:u}=(0,discovery_1.discoverRoutes)(r.root,t,void 0,r.wsKey,y),v={wsKey:r.wsKey,root:r.root,routes:w,services:u};try{const g=(0,service_keys_1.adoptServiceKeys)(e,r.root,u,r.wsKey);g.added.length>0&&console.log(`[gateway] adopted east-west keys for ${r.wsKey}: ${g.added.join(", ")}`)}catch(g){console.error(`[gateway] could not adopt east-west keys for ${r.wsKey}: ${g.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(r.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${r.wsKey}') is served by the daemon at ${e.root} (pid ${process.pid}); its gateway-level log is ${path.join(e.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
2
- `)}catch{}return e.registry.register(v),await s.onRegister?.(v),e.rebuildRoutes(),o(a,200,{ok:!0,wsKey:r.wsKey,routes:w.length,services:u.length,workspaces:e.registry.keys()})}if(i.method==="POST"&&d==="/unregister"){const r=await readJsonBody(i);if(!r.wsKey)return o(a,400,{error:"wsKey is required"});const t=e.registry.unregister(r.wsKey);if(t){for(const y of t.services){const w=(0,service_keys_1.serviceKeyId)(y.wsKey??r.wsKey,y.project),u=e.serviceKeys[w];u&&(delete e.serviceKeys[w],e.keyToProject.delete(u),e.keyToWorkspace?.delete(u))}for(const y of t.services)s.killService(y,"workspace unregistered");s.onUnregister?.(t)}return e.rebuildRoutes(),o(a,200,{ok:!0,removed:!!t,workspaces:e.registry.keys()})}o(a,404,{error:"not found"})}catch(d){o(a,500,{error:d.message})}})()});return c.listen(n),{socketPath:n,close:l(()=>{c.close();try{fs.unlinkSync(n)}catch{}},"close")}}p(startControlServer,"startControlServer"),l(startControlServer,"startControlServer");
1
+ "use strict";var f=Object.defineProperty;var p=(t,e)=>f(t,"name",{value:e,configurable:!0});var h=Object.defineProperty,u=p((t,e)=>h(t,"name",{value:e,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.controlSocketPath=controlSocketPath,exports.clusterVersion=clusterVersion,exports.ensureOwnerOnlyDir=ensureOwnerOnlyDir,exports.startControlServer=startControlServer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log");function controlSocketPath(){const t=process.env.LENSMCP_HOME||os.homedir();return path.join(t,".lensmcp","control.sock")}p(controlSocketPath,"controlSocketPath"),u(controlSocketPath,"controlSocketPath");function clusterVersion(){let t=__dirname;for(let e=0;e<6;e++){const n=path.join(t,"package.json");if(fs.existsSync(n))try{const s=JSON.parse(fs.readFileSync(n,"utf8"));if(s.name==="@lensmcp/cluster"&&typeof s.version=="string")return s.version}catch{}const c=path.dirname(t);if(c===t)break;t=c}}p(clusterVersion,"clusterVersion"),u(clusterVersion,"clusterVersion");function readJsonBody(t){return new Promise((e,n)=>{let c="";t.on("data",s=>{c+=s,c.length>1e6&&n(new Error("body too large"))}),t.on("end",()=>{try{e(c?JSON.parse(c):{})}catch(s){n(s)}}),t.on("error",n)})}p(readJsonBody,"readJsonBody"),u(readJsonBody,"readJsonBody");function ensureOwnerOnlyDir(t){try{fs.mkdirSync(t,{recursive:!0,mode:448})}catch{}try{const e=fs.lstatSync(t);return e.isDirectory()?((e.mode&63)!==0&&fs.chmodSync(t,448),!0):!1}catch{return!1}}p(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),u(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(t){const{rt:e}=t,n=controlSocketPath(),c=path.dirname(n);ensureOwnerOnlyDir(c)||console.warn(`[gateway] control home ${c} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(n)}catch{}const s=u((i,a,l)=>{i.writeHead(a,{"content-type":"application/json"}),i.end(JSON.stringify(l))},"send"),v=http.createServer((i,a)=>{(async()=>{try{const l=(i.url??"").split("?")[0];if(i.method==="GET"&&l==="/list")return s(a,200,{workspaces:e.registry.keys().map(r=>{const o=e.registry.get(r);return{key:r,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0}})});if(i.method==="GET"&&l==="/status"){const r=clusterVersion();return s(a,200,{daemon:{pid:process.pid,wsKey:e.wsKey,...r?{version:r}:{}},workspaces:e.registry.keys().map(o=>{const y=e.registry.get(o);return{key:o,root:y?.root,routes:y?.routes.length??0,services:y?.services.length??0,isDaemon:o===e.wsKey}}),services:e.registry.allServices().map(o=>({project:o.project,wsKey:o.wsKey,host:o.decl.host,state:o.state}))})}if(i.method==="POST"&&l==="/register"){const r=await readJsonBody(i);if(!r.wsKey||!r.root)return s(a,400,{error:"wsKey and root are required"});const o=r.projects??{},y=(0,discovery_1.projectSourceRoots)(r.root,o,r.projectDependencies),{routes:w,services:d}=(0,discovery_1.discoverRoutes)(r.root,o,void 0,r.wsKey,y),k={wsKey:r.wsKey,root:r.root,routes:w,services:d};try{const g=(0,service_keys_1.adoptServiceKeys)(e,r.root,d,r.wsKey);g.added.length>0&&console.log(`[gateway] adopted east-west keys for ${r.wsKey}: ${g.added.join(", ")}`)}catch(g){console.error(`[gateway] could not adopt east-west keys for ${r.wsKey}: ${g.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(r.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${r.wsKey}') is served by the daemon at ${e.root} (pid ${process.pid}); its gateway-level log is ${path.join(e.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
2
+ `)}catch{}return e.registry.register(k),await t.onRegister?.(k),e.rebuildRoutes(),s(a,200,{ok:!0,wsKey:r.wsKey,routes:w.length,services:d.length,workspaces:e.registry.keys()})}if(i.method==="POST"&&l==="/unregister"){const r=await readJsonBody(i);if(!r.wsKey)return s(a,400,{error:"wsKey is required"});const o=e.registry.unregister(r.wsKey);if(o){for(const y of o.services){const w=(0,service_keys_1.serviceKeyId)(y.wsKey??r.wsKey,y.project),d=e.serviceKeys[w];d&&(delete e.serviceKeys[w],e.keyToProject.delete(d),e.keyToWorkspace?.delete(d))}for(const y of o.services)t.killService(y,"workspace unregistered");t.onUnregister?.(o)}return e.rebuildRoutes(),s(a,200,{ok:!0,removed:!!o,workspaces:e.registry.keys()})}s(a,404,{error:"not found"})}catch(l){s(a,500,{error:l.message})}})()});return v.listen(n,()=>{try{fs.chmodSync(n,384)}catch(i){console.warn(`[gateway] could not restrict ${n} to 0600:`,i.message)}}),{socketPath:n,close:u(()=>{v.close();try{fs.unlinkSync(n)}catch{}},"close")}}p(startControlServer,"startControlServer"),u(startControlServer,"startControlServer");
@@ -36,14 +36,40 @@ export declare const sendError: (res: http.ServerResponse, req: http.IncomingMes
36
36
  * BOTH the HTTP handler and the WS upgrade.
37
37
  */
38
38
  export declare const GATEWAY_OWNED_HEADERS: string[];
39
- /** Reflect the request Origin back with credentials — the gateway is the edge CORS authority. */
40
- export declare const cors: (req: http.IncomingMessage, res: http.ServerResponse) => void;
39
+ /**
40
+ * Is this `Origin` one of the hosts THIS gateway routes?
41
+ *
42
+ * The allowlist needs no configuration, because the gateway already knows the answer: the case the
43
+ * CORS helper exists for is `app.<domain>` calling `api.<domain>`, and both are in its own route
44
+ * table. Anything else — including a look-alike like `foodguard.local.evil.example` — is a site the
45
+ * gateway has no relationship with (issues/codex-security/55).
46
+ */
47
+ export declare const isRoutedOrigin: (origin: string, routes: readonly {
48
+ host?: string;
49
+ }[]) => boolean;
50
+ /**
51
+ * Answer CORS for a gateway-OWNED response.
52
+ *
53
+ * This used to reflect any Origin with `credentials: true`. Before it existed a browser could still
54
+ * send these requests; the same-origin policy stopped an unrelated site READING the answers, and
55
+ * reflecting the origin removed that — so a page the developer visited could read `/health`,
56
+ * `/.well-known/services` and the informative 404s (issues/codex-security/55). The prod gateway
57
+ * already restricts credentials to an allowlist (`prod-runtime/cors.ts`); this is the dev twin.
58
+ *
59
+ * An untrusted origin gets NO `access-control-allow-origin`, so the browser refuses the read. `Vary`
60
+ * is still set, because the answer depends on `Origin` and a cache must not reuse it across origins.
61
+ */
62
+ export declare const cors: (req: http.IncomingMessage, res: http.ServerResponse, routes: readonly {
63
+ host?: string;
64
+ }[]) => void;
41
65
  /**
42
66
  * Answer a CORS preflight (OPTIONS) at the edge: reflect CORS + 204 with the allowed `methods` and the
43
67
  * requested headers echoed back. `methods` differs by caller — the no-route host-root uses the narrow
44
68
  * `GET,HEAD,OPTIONS`; a routed path uses the wide verb list.
45
69
  */
46
- export declare const corsPreflight: (req: http.IncomingMessage, res: http.ServerResponse, methods: string) => void;
70
+ export declare const corsPreflight: (req: http.IncomingMessage, res: http.ServerResponse, methods: string, routes: readonly {
71
+ host?: string;
72
+ }[]) => void;
47
73
  /** Allowed-methods lists for the two preflight sites. */
48
74
  export declare const PREFLIGHT_METHODS_NOROUTE = "GET,HEAD,OPTIONS";
49
75
  export declare const PREFLIGHT_METHODS_ROUTED = "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS";
@@ -1 +1 @@
1
- "use strict";var l=Object.defineProperty;var c=(r,e)=>l(r,"name",{value:e,configurable:!0});var p=Object.defineProperty,t=c((r,e)=>p(r,"name",{value:e,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.PREFLIGHT_METHODS_ROUTED=exports.PREFLIGHT_METHODS_NOROUTE=exports.corsPreflight=exports.cors=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;const gateway_errors_1=require("../gateway-errors"),client_ip_1=require("../client-ip");var canonical_path_1=require("../canonical-path");Object.defineProperty(exports,"ENCODED_STRUCTURE",{enumerable:!0,get:t(function(){return canonical_path_1.ENCODED_STRUCTURE},"get")}),Object.defineProperty(exports,"canonicalize",{enumerable:!0,get:t(function(){return canonical_path_1.canonicalize},"get")}),Object.defineProperty(exports,"canonicalPath",{enumerable:!0,get:t(function(){return canonical_path_1.canonicalPath},"get")}),Object.defineProperty(exports,"RAW_TARGET",{enumerable:!0,get:t(function(){return canonical_path_1.RAW_TARGET},"get")}),Object.defineProperty(exports,"rawTargetOf",{enumerable:!0,get:t(function(){return canonical_path_1.rawTargetOf},"get")}),Object.defineProperty(exports,"rememberRawTarget",{enumerable:!0,get:t(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=t(r=>r.clientIp??peerOnly,"resolverOf"),peerOf=t(r=>r.socket?.remoteAddress??"","peerOf"),resolveClientIp=t((r,e)=>{const o=resolverOf(r).clientIpOf(e,peerOf(e));return e[exports.CLIENT_IP]=o,o},"resolveClientIp");exports.resolveClientIp=resolveClientIp;const clientIpOf=t(r=>r[exports.CLIENT_IP]??peerOf(r),"clientIpOf");exports.clientIpOf=clientIpOf;const stampForwardingHeaders=t((r,e)=>{const o=e;if(o[FORWARDED])return;o[FORWARDED]=!0;const n=peerOf(e);e.headers["x-forwarded-for"]=resolverOf(r).forwardedFor(e,n),e.headers["x-forwarded-proto"]=r.https?"https":"http";const s=e.headers.host;(resolverOf(r).trustProxyHops===0||!e.headers["x-forwarded-host"])&&(s?e.headers["x-forwarded-host"]=s:delete e.headers["x-forwarded-host"])},"stampForwardingHeaders");exports.stampForwardingHeaders=stampForwardingHeaders;const sendError=t((r,e,o,n,s,i,d)=>{const a=(0,gateway_errors_1.ensureRequestId)(e);return r.headersSent||r.writeHead(o,{"content-type":gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE,"x-request-id":a,...d??{}}),r.end((0,gateway_errors_1.gatewayErrorBody)(o,n,a,s,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"];const cors=t((r,e)=>{const o=String(r.headers.origin??"*");e.setHeader("access-control-allow-origin",o),e.setHeader("access-control-allow-credentials","true"),e.setHeader("vary","Origin")},"cors");exports.cors=cors;const corsPreflight=t((r,e,o)=>{(0,exports.cors)(r,e),e.writeHead(204,{"access-control-allow-methods":o,"access-control-allow-headers":String(r.headers["access-control-request-headers"]??"*")}),e.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 l=Object.defineProperty;var i=(e,r)=>l(e,"name",{value:r,configurable:!0});var p=Object.defineProperty,o=i((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.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;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,c,d)=>{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,...d??{}}),e.end((0,gateway_errors_1.gatewayErrorBody)(t,s,a,n,c)),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"];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 +1 @@
1
- "use strict";var O=Object.defineProperty;var w=(p,d)=>O(p,"name",{value:d,configurable:!0});var E=Object.defineProperty,h=w((p,d)=>E(p,"name",{value:d,configurable:!0}),"h");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHandler=createHandler;const manifest_1=require("../manifest"),service_keys_1=require("./service-keys"),gateway_errors_1=require("../gateway-errors"),discovery_1=require("./discovery"),edge_1=require("./edge"),observability_1=require("./observability"),chooser_1=require("./chooser");function createHandler(p){const{rt:d,obs:u,auth:y,proxy:m,hooks:i}=p,g=h(e=>(e.headers.host||"").split(":")[0],"hostOf"),_=h((e,r)=>{if((0,edge_1.cors)(e,r),e.method==="OPTIONS"){r.writeHead(204,{"access-control-allow-methods":edge_1.PREFLIGHT_METHODS_NOROUTE,"access-control-allow-headers":String(e.headers["access-control-request-headers"]??"*")}),r.end();return}const s=(e.headers.host||"").split(":")[0],t=d.routes.filter(o=>o.host&&(0,manifest_1.hostMatches)(o.host,s)&&o.prefix),n=(e.url??"/").split("?")[0];if((n==="/"||n==="/index.html"||n==="")&&d.routes.some(o=>o.host&&(0,manifest_1.hostMatches)(o.host,s)&&o.project===observability_1.LENS_DASHBOARD_PROJECT)){(0,chooser_1.serveChooser)(r,d.wsKey);return}if(t.length>0&&(n==="/health"||n==="/healthz")){const o=t.map(l=>({project:l.project,prefix:l.prefix,pods:l.pool?((0,discovery_1.pickSock)(l.pool,0),l.pool.socks.length):void 0,state:l.svc?.state??(l.target?"up":"unknown")})),f=o.every(l=>l.state!=="down"||!0);r.writeHead(f?200:503,{"content-type":"application/json"}),r.end(JSON.stringify({status:"ok",host:s,services:o},null,2));return}if(t.length>0&&n==="/.well-known/services"){r.writeHead(200,{"content-type":"application/json"}),r.end(JSON.stringify({host:s,services:t.map(o=>({prefix:o.prefix,project:o.project,internal:!!o.internal}))},null,2));return}(0,edge_1.sendError)(r,e,404,"not_found",t.length>0?`No service mounted at ${n}. Mounted on ${s}: ${[...new Set(t.map(o=>o.prefix))].join(", ")} (+ /health, /.well-known/services)`:"No gateway route for this hostname.")},"handleNoRoute"),S=h(async(e,r)=>{if(!e.headers.host){const n=e.authority??e.headers[":authority"];n&&(e.headers.host=n)}for(const n of edge_1.GATEWAY_OWNED_HEADERS)delete e.headers[n];(0,gateway_errors_1.ensureRequestId)(e);const s=u.beginTrace(e);s&&(u.traceStep(s,"received",{host:s.host,method:s.method,url:s.url}),e.__lensmcpTrace=s);const t=i.httpContext(e,r,s);try{const n=e.url??"/",o=(0,edge_1.canonicalize)(n);if(o===void 0){u.traceStep(s,"bad-path",{}),u.finishTrace(s,"gateway",400),(0,edge_1.sendError)(r,e,400,"bad_request","Bad Request");return}const f=o.policy,l=o.path+o.query;if(l!==n&&(0,edge_1.rememberRawTarget)(e,n),e.url=l,(0,edge_1.resolveClientIp)(d,e),i.has("onRequest")&&(await i.run("onRequest",t),t.responded()))return;const a=(0,manifest_1.matchRoute)(d.routes,g(e),f+o.query);if(!a){_(e,r);return}if(t.route=a,u.traceStep(s,"route-match",{project:a.project,...a.prefix?{prefix:a.prefix}:{},...a.host?{routeHost:a.host}:{},...a.internal?{internal:!0}:{}}),e.method==="OPTIONS"){(0,edge_1.cors)(e,r),r.writeHead(204,{"access-control-allow-methods":edge_1.PREFLIGHT_METHODS_ROUTED,"access-control-allow-headers":String(e.headers["access-control-request-headers"]??"*")}),r.end();return}if((0,edge_1.cors)(e,r),i.has("beforeAuth")&&(await i.run("beforeAuth",t),t.responded()))return;if(a.internal){const c=y.internal(e,a,s);if(!c.ok){u.finishTrace(s,a.project,c.status),(0,edge_1.sendError)(r,e,c.status,c.reason,c.detail);return}t.caller=c.caller}else{const c=y.public(e,a,s,f);if(!c.ok){u.finishTrace(s,a.project,c.status),(0,edge_1.sendError)(r,e,c.status,c.reason,c.detail);return}if(t.claims=c.claims,i.has("afterAuth")&&(await i.run("afterAuth",t),t.responded()))return}const v=a.pool?d.serviceKeys[(0,service_keys_1.serviceKeyId)(a.svc?.wsKey,a.project)]:void 0;if(v&&(e.headers["x-internal-token"]=v),(0,edge_1.stampForwardingHeaders)(d,e),i.has("beforeForward")&&(await i.run("beforeForward",t),t.responded()))return;i.has("onResponse")&&r.on("close",()=>{t.status=r.statusCode??0,i.run("onResponse",t)}),m.forward(a,e,r)}catch(n){t.error=n,console.error(`[gateway] ${new Date().toISOString()} pipeline error traceId=${(0,gateway_errors_1.ensureRequestId)(e)}: ${n?.stack??String(n)}`),i.has("onError")&&await i.run("onError",t),t.responded()||(0,edge_1.sendError)(r,e,500,"internal","Internal gateway error.")}},"runPipeline");return(e,r)=>{S(e,r)}}w(createHandler,"createHandler"),h(createHandler,"createHandler");
1
+ "use strict";var O=Object.defineProperty;var w=(p,i)=>O(p,"name",{value:i,configurable:!0});var E=Object.defineProperty,h=w((p,i)=>E(p,"name",{value:i,configurable:!0}),"h");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHandler=createHandler;const manifest_1=require("../manifest"),service_keys_1=require("./service-keys"),gateway_errors_1=require("../gateway-errors"),discovery_1=require("./discovery"),edge_1=require("./edge"),observability_1=require("./observability"),chooser_1=require("./chooser");function createHandler(p){const{rt:i,obs:d,auth:y,proxy:m,hooks:c}=p,g=h(e=>(e.headers.host||"").split(":")[0],"hostOf"),_=h((e,r)=>{if((0,edge_1.cors)(e,r,i.routes),e.method==="OPTIONS"){r.writeHead(204,{"access-control-allow-methods":edge_1.PREFLIGHT_METHODS_NOROUTE,"access-control-allow-headers":String(e.headers["access-control-request-headers"]??"*")}),r.end();return}const s=(e.headers.host||"").split(":")[0],t=i.routes.filter(o=>o.host&&(0,manifest_1.hostMatches)(o.host,s)&&o.prefix),n=(e.url??"/").split("?")[0];if((n==="/"||n==="/index.html"||n==="")&&i.routes.some(o=>o.host&&(0,manifest_1.hostMatches)(o.host,s)&&o.project===observability_1.LENS_DASHBOARD_PROJECT)){(0,chooser_1.serveChooser)(r,i.wsKey,e);return}if(t.length>0&&(n==="/health"||n==="/healthz")){const o=t.map(l=>({project:l.project,prefix:l.prefix,pods:l.pool?((0,discovery_1.pickSock)(l.pool,0),l.pool.socks.length):void 0,state:l.svc?.state??(l.target?"up":"unknown")})),f=o.every(l=>l.state!=="down"||!0);r.writeHead(f?200:503,{"content-type":"application/json"}),r.end(JSON.stringify({status:"ok",host:s,services:o},null,2));return}if(t.length>0&&n==="/.well-known/services"){r.writeHead(200,{"content-type":"application/json"}),r.end(JSON.stringify({host:s,services:t.map(o=>({prefix:o.prefix,project:o.project,internal:!!o.internal}))},null,2));return}(0,edge_1.sendError)(r,e,404,"not_found",t.length>0?`No service mounted at ${n}. Mounted on ${s}: ${[...new Set(t.map(o=>o.prefix))].join(", ")} (+ /health, /.well-known/services)`:"No gateway route for this hostname.")},"handleNoRoute"),S=h(async(e,r)=>{if(!e.headers.host){const n=e.authority??e.headers[":authority"];n&&(e.headers.host=n)}for(const n of edge_1.GATEWAY_OWNED_HEADERS)delete e.headers[n];(0,gateway_errors_1.ensureRequestId)(e);const s=d.beginTrace(e);s&&(d.traceStep(s,"received",{host:s.host,method:s.method,url:s.url}),e.__lensmcpTrace=s);const t=c.httpContext(e,r,s);try{const n=e.url??"/",o=(0,edge_1.canonicalize)(n);if(o===void 0){d.traceStep(s,"bad-path",{}),d.finishTrace(s,"gateway",400),(0,edge_1.sendError)(r,e,400,"bad_request","Bad Request");return}const f=o.policy,l=o.path+o.query;if(l!==n&&(0,edge_1.rememberRawTarget)(e,n),e.url=l,(0,edge_1.resolveClientIp)(i,e),c.has("onRequest")&&(await c.run("onRequest",t),t.responded()))return;const a=(0,manifest_1.matchRoute)(i.routes,g(e),f+o.query);if(!a){_(e,r);return}if(t.route=a,d.traceStep(s,"route-match",{project:a.project,...a.prefix?{prefix:a.prefix}:{},...a.host?{routeHost:a.host}:{},...a.internal?{internal:!0}:{}}),e.method==="OPTIONS"){(0,edge_1.cors)(e,r,i.routes),r.writeHead(204,{"access-control-allow-methods":edge_1.PREFLIGHT_METHODS_ROUTED,"access-control-allow-headers":String(e.headers["access-control-request-headers"]??"*")}),r.end();return}if((0,edge_1.cors)(e,r,i.routes),c.has("beforeAuth")&&(await c.run("beforeAuth",t),t.responded()))return;if(a.internal){const u=y.internal(e,a,s);if(!u.ok){d.finishTrace(s,a.project,u.status),(0,edge_1.sendError)(r,e,u.status,u.reason,u.detail);return}t.caller=u.caller}else{const u=y.public(e,a,s,f);if(!u.ok){d.finishTrace(s,a.project,u.status),(0,edge_1.sendError)(r,e,u.status,u.reason,u.detail);return}if(t.claims=u.claims,c.has("afterAuth")&&(await c.run("afterAuth",t),t.responded()))return}const v=a.pool?i.serviceKeys[(0,service_keys_1.serviceKeyId)(a.svc?.wsKey,a.project)]:void 0;if(v&&(e.headers["x-internal-token"]=v),(0,edge_1.stampForwardingHeaders)(i,e),c.has("beforeForward")&&(await c.run("beforeForward",t),t.responded()))return;c.has("onResponse")&&r.on("close",()=>{t.status=r.statusCode??0,c.run("onResponse",t)}),m.forward(a,e,r)}catch(n){t.error=n,console.error(`[gateway] ${new Date().toISOString()} pipeline error traceId=${(0,gateway_errors_1.ensureRequestId)(e)}: ${n?.stack??String(n)}`),c.has("onError")&&await c.run("onError",t),t.responded()||(0,edge_1.sendError)(r,e,500,"internal","Internal gateway error.")}},"runPipeline");return(e,r)=>{S(e,r)}}w(createHandler,"createHandler"),h(createHandler,"createHandler");