@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.
- package/executors/gateway/jwks-verify.js +1 -1
- package/executors/gateway/jwt-claims.d.ts +34 -0
- package/executors/gateway/jwt-claims.js +1 -0
- package/executors/gateway/main.prod-gateway.js +2 -2
- package/executors/gateway/prod-config.d.ts +27 -0
- package/executors/gateway/prod-config.js +1 -1
- package/executors/gateway/rate-limit.d.ts +9 -0
- package/executors/gateway/rate-limit.js +2 -2
- package/executors/gateway/runtime/auth.js +1 -1
- package/executors/gateway/runtime/chooser.d.ts +30 -9
- package/executors/gateway/runtime/chooser.js +9 -9
- package/executors/gateway/runtime/control.d.ts +17 -2
- package/executors/gateway/runtime/control.js +2 -2
- package/executors/gateway/runtime/edge.d.ts +29 -3
- package/executors/gateway/runtime/edge.js +1 -1
- package/executors/gateway/runtime/handler.js +1 -1
- package/executors/gateway/runtime/lens-children.js +1 -1
- package/executors/gateway/runtime/lifecycle.d.ts +21 -0
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/proxy.d.ts +25 -0
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/server.d.ts +90 -0
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/upgrade.js +1 -1
- package/executors/serve/serve.impl.js +1 -1
- package/executors/trust/hostname.d.ts +20 -0
- package/executors/trust/hostname.js +1 -0
- package/executors/trust/trust.impl.js +5 -3
- package/main.devserver.d.ts +50 -0
- package/main.devserver.js +7 -7
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
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
|
|
2
|
-
`;
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
13
|
-
|
|
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
|
|
2
|
-
<div class="key">${esc(
|
|
3
|
-
<div class="root">${esc(
|
|
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=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[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
|
|
6
|
-
${
|
|
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
|
-
${
|
|
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. ${
|
|
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
|
-
${
|
|
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>`}
|
|
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
|
-
/**
|
|
32
|
-
*
|
|
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
|
|
2
|
-
`)}catch{}return e.registry.register(
|
|
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
|
-
/**
|
|
40
|
-
|
|
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
|
|
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
|
|
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,
|
|
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");
|