@lensmcp/cluster 1.18.3 → 1.18.6

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 (190) hide show
  1. package/basic-ssl.d.ts +0 -1
  2. package/basic-ssl.js +1 -241
  3. package/build-scope-patterns.d.ts +0 -1
  4. package/build-scope-patterns.js +1 -40
  5. package/create-webpack-dev.d.ts +0 -1
  6. package/create-webpack-dev.js +1 -186
  7. package/create-webpack-prod.d.ts +0 -1
  8. package/create-webpack-prod.js +1 -169
  9. package/executors/build/build.impl.d.ts +0 -1
  10. package/executors/build/build.impl.js +1 -98
  11. package/executors/gateway/gateway-errors.d.ts +0 -1
  12. package/executors/gateway/gateway-errors.js +1 -43
  13. package/executors/gateway/gateway.impl.d.ts +0 -1
  14. package/executors/gateway/gateway.impl.js +1 -53
  15. package/executors/gateway/gateway.lib.d.ts +0 -1
  16. package/executors/gateway/gateway.lib.js +1 -29
  17. package/executors/gateway/health-check.d.ts +0 -1
  18. package/executors/gateway/health-check.js +1 -66
  19. package/executors/gateway/jwks-verify.d.ts +0 -1
  20. package/executors/gateway/jwks-verify.js +1 -121
  21. package/executors/gateway/main.prod-gateway.d.ts +0 -1
  22. package/executors/gateway/main.prod-gateway.js +2 -573
  23. package/executors/gateway/main.rollout.d.ts +0 -1
  24. package/executors/gateway/main.rollout.js +11 -117
  25. package/executors/gateway/manifest.d.ts +0 -1
  26. package/executors/gateway/manifest.js +1 -374
  27. package/executors/gateway/metrics.d.ts +0 -1
  28. package/executors/gateway/metrics.js +1 -56
  29. package/executors/gateway/otel-tracing.d.ts +0 -1
  30. package/executors/gateway/otel-tracing.js +1 -74
  31. package/executors/gateway/prod-gateway.lib.d.ts +0 -1
  32. package/executors/gateway/prod-gateway.lib.js +1 -22
  33. package/executors/gateway/prod-runtime/access-log.d.ts +0 -1
  34. package/executors/gateway/prod-runtime/access-log.js +1 -24
  35. package/executors/gateway/prod-runtime/app.d.ts +0 -1
  36. package/executors/gateway/prod-runtime/app.js +1 -123
  37. package/executors/gateway/prod-runtime/auth.d.ts +0 -1
  38. package/executors/gateway/prod-runtime/auth.js +1 -51
  39. package/executors/gateway/prod-runtime/cors.d.ts +0 -1
  40. package/executors/gateway/prod-runtime/cors.js +1 -40
  41. package/executors/gateway/prod-runtime/edge.d.ts +0 -1
  42. package/executors/gateway/prod-runtime/edge.js +1 -65
  43. package/executors/gateway/prod-runtime/handler.d.ts +0 -1
  44. package/executors/gateway/prod-runtime/handler.js +1 -226
  45. package/executors/gateway/prod-runtime/hooks.d.ts +0 -1
  46. package/executors/gateway/prod-runtime/hooks.js +1 -42
  47. package/executors/gateway/prod-runtime/observability.d.ts +0 -1
  48. package/executors/gateway/prod-runtime/observability.js +1 -125
  49. package/executors/gateway/prod-runtime/rollout.d.ts +0 -1
  50. package/executors/gateway/prod-runtime/rollout.js +1 -103
  51. package/executors/gateway/prod-runtime/routing.d.ts +0 -1
  52. package/executors/gateway/prod-runtime/routing.js +1 -40
  53. package/executors/gateway/prod-runtime/server.d.ts +0 -1
  54. package/executors/gateway/prod-runtime/server.js +1 -79
  55. package/executors/gateway/prod-runtime/trust.d.ts +0 -1
  56. package/executors/gateway/prod-runtime/trust.js +1 -32
  57. package/executors/gateway/prod-runtime/types.d.ts +0 -1
  58. package/executors/gateway/prod-runtime/types.js +1 -2
  59. package/executors/gateway/prod-runtime/upgrade.d.ts +0 -1
  60. package/executors/gateway/prod-runtime/upgrade.js +4 -116
  61. package/executors/gateway/prod-runtime/upstream.d.ts +0 -1
  62. package/executors/gateway/prod-runtime/upstream.js +1 -21
  63. package/executors/gateway/providers-prod.d.ts +0 -1
  64. package/executors/gateway/providers-prod.js +1 -232
  65. package/executors/gateway/rate-limit.d.ts +0 -1
  66. package/executors/gateway/rate-limit.js +2 -75
  67. package/executors/gateway/registry-source.d.ts +0 -1
  68. package/executors/gateway/registry-source.js +1 -131
  69. package/executors/gateway/rollout-ops.d.ts +0 -1
  70. package/executors/gateway/rollout-ops.js +2 -167
  71. package/executors/gateway/runtime/auth.d.ts +0 -1
  72. package/executors/gateway/runtime/auth.js +1 -64
  73. package/executors/gateway/runtime/chooser.d.ts +0 -1
  74. package/executors/gateway/runtime/chooser.js +12 -45
  75. package/executors/gateway/runtime/control.d.ts +0 -1
  76. package/executors/gateway/runtime/control.js +1 -128
  77. package/executors/gateway/runtime/dev-auth.d.ts +0 -1
  78. package/executors/gateway/runtime/dev-auth.js +1 -108
  79. package/executors/gateway/runtime/discovery.d.ts +0 -1
  80. package/executors/gateway/runtime/discovery.js +1 -123
  81. package/executors/gateway/runtime/edge.d.ts +0 -1
  82. package/executors/gateway/runtime/edge.js +1 -47
  83. package/executors/gateway/runtime/handler.d.ts +0 -1
  84. package/executors/gateway/runtime/handler.js +1 -183
  85. package/executors/gateway/runtime/hooks.d.ts +0 -1
  86. package/executors/gateway/runtime/hooks.js +1 -55
  87. package/executors/gateway/runtime/lens-children.d.ts +0 -1
  88. package/executors/gateway/runtime/lens-children.js +1 -651
  89. package/executors/gateway/runtime/lifecycle.d.ts +0 -1
  90. package/executors/gateway/runtime/lifecycle.js +3 -842
  91. package/executors/gateway/runtime/observability.d.ts +0 -1
  92. package/executors/gateway/runtime/observability.js +2 -148
  93. package/executors/gateway/runtime/pod-env.d.ts +0 -1
  94. package/executors/gateway/runtime/pod-env.js +2 -89
  95. package/executors/gateway/runtime/proxy.d.ts +0 -1
  96. package/executors/gateway/runtime/proxy.js +1 -457
  97. package/executors/gateway/runtime/route-registry.d.ts +0 -1
  98. package/executors/gateway/runtime/route-registry.js +1 -72
  99. package/executors/gateway/runtime/scope.d.ts +0 -1
  100. package/executors/gateway/runtime/scope.js +1 -117
  101. package/executors/gateway/runtime/server.d.ts +0 -1
  102. package/executors/gateway/runtime/server.js +3 -487
  103. package/executors/gateway/runtime/service-keys.d.ts +0 -1
  104. package/executors/gateway/runtime/service-keys.js +1 -49
  105. package/executors/gateway/runtime/types.d.ts +0 -1
  106. package/executors/gateway/runtime/types.js +1 -151
  107. package/executors/gateway/runtime/upgrade.d.ts +0 -1
  108. package/executors/gateway/runtime/upgrade.js +1 -71
  109. package/executors/gateway/runtime/workspace-registry.d.ts +0 -1
  110. package/executors/gateway/runtime/workspace-registry.js +1 -99
  111. package/executors/gateway/ssrf-guard.d.ts +0 -1
  112. package/executors/gateway/ssrf-guard.js +1 -190
  113. package/executors/serve/serve.impl.d.ts +0 -1
  114. package/executors/serve/serve.impl.js +1 -280
  115. package/executors/trust/trust.impl.d.ts +0 -1
  116. package/executors/trust/trust.impl.js +4 -162
  117. package/gateway.d.ts +0 -1
  118. package/gateway.js +1 -35
  119. package/index.d.ts +0 -1
  120. package/index.js +1 -16
  121. package/main.devserver.d.ts +0 -1
  122. package/main.devserver.js +10 -1117
  123. package/package.json +4 -3
  124. package/tsgo-check-plugin.d.ts +0 -1
  125. package/tsgo-check-plugin.js +4 -364
  126. package/typecheck-bus.d.ts +0 -1
  127. package/typecheck-bus.js +4 -256
  128. package/basic-ssl.d.ts.map +0 -1
  129. package/build-scope-patterns.d.ts.map +0 -1
  130. package/create-webpack-dev.d.ts.map +0 -1
  131. package/create-webpack-prod.d.ts.map +0 -1
  132. package/executors/build/build.impl.d.ts.map +0 -1
  133. package/executors/gateway/gateway-errors.d.ts.map +0 -1
  134. package/executors/gateway/gateway.impl.d.ts.map +0 -1
  135. package/executors/gateway/gateway.lib.d.ts.map +0 -1
  136. package/executors/gateway/health-check.d.ts.map +0 -1
  137. package/executors/gateway/jwks-verify.d.ts.map +0 -1
  138. package/executors/gateway/main.prod-gateway.d.ts.map +0 -1
  139. package/executors/gateway/main.rollout.d.ts.map +0 -1
  140. package/executors/gateway/manifest.d.ts.map +0 -1
  141. package/executors/gateway/metrics.d.ts.map +0 -1
  142. package/executors/gateway/otel-tracing.d.ts.map +0 -1
  143. package/executors/gateway/prod-gateway.lib.d.ts.map +0 -1
  144. package/executors/gateway/prod-runtime/access-log.d.ts.map +0 -1
  145. package/executors/gateway/prod-runtime/app.d.ts.map +0 -1
  146. package/executors/gateway/prod-runtime/auth.d.ts.map +0 -1
  147. package/executors/gateway/prod-runtime/cors.d.ts.map +0 -1
  148. package/executors/gateway/prod-runtime/edge.d.ts.map +0 -1
  149. package/executors/gateway/prod-runtime/handler.d.ts.map +0 -1
  150. package/executors/gateway/prod-runtime/hooks.d.ts.map +0 -1
  151. package/executors/gateway/prod-runtime/observability.d.ts.map +0 -1
  152. package/executors/gateway/prod-runtime/rollout.d.ts.map +0 -1
  153. package/executors/gateway/prod-runtime/routing.d.ts.map +0 -1
  154. package/executors/gateway/prod-runtime/server.d.ts.map +0 -1
  155. package/executors/gateway/prod-runtime/trust.d.ts.map +0 -1
  156. package/executors/gateway/prod-runtime/types.d.ts.map +0 -1
  157. package/executors/gateway/prod-runtime/upgrade.d.ts.map +0 -1
  158. package/executors/gateway/prod-runtime/upstream.d.ts.map +0 -1
  159. package/executors/gateway/providers-prod.d.ts.map +0 -1
  160. package/executors/gateway/rate-limit.d.ts.map +0 -1
  161. package/executors/gateway/registry-source.d.ts.map +0 -1
  162. package/executors/gateway/rollout-ops.d.ts.map +0 -1
  163. package/executors/gateway/runtime/auth.d.ts.map +0 -1
  164. package/executors/gateway/runtime/chooser.d.ts.map +0 -1
  165. package/executors/gateway/runtime/control.d.ts.map +0 -1
  166. package/executors/gateway/runtime/dev-auth.d.ts.map +0 -1
  167. package/executors/gateway/runtime/discovery.d.ts.map +0 -1
  168. package/executors/gateway/runtime/edge.d.ts.map +0 -1
  169. package/executors/gateway/runtime/handler.d.ts.map +0 -1
  170. package/executors/gateway/runtime/hooks.d.ts.map +0 -1
  171. package/executors/gateway/runtime/lens-children.d.ts.map +0 -1
  172. package/executors/gateway/runtime/lifecycle.d.ts.map +0 -1
  173. package/executors/gateway/runtime/observability.d.ts.map +0 -1
  174. package/executors/gateway/runtime/pod-env.d.ts.map +0 -1
  175. package/executors/gateway/runtime/proxy.d.ts.map +0 -1
  176. package/executors/gateway/runtime/route-registry.d.ts.map +0 -1
  177. package/executors/gateway/runtime/scope.d.ts.map +0 -1
  178. package/executors/gateway/runtime/server.d.ts.map +0 -1
  179. package/executors/gateway/runtime/service-keys.d.ts.map +0 -1
  180. package/executors/gateway/runtime/types.d.ts.map +0 -1
  181. package/executors/gateway/runtime/upgrade.d.ts.map +0 -1
  182. package/executors/gateway/runtime/workspace-registry.d.ts.map +0 -1
  183. package/executors/gateway/ssrf-guard.d.ts.map +0 -1
  184. package/executors/serve/serve.impl.d.ts.map +0 -1
  185. package/executors/trust/trust.impl.d.ts.map +0 -1
  186. package/gateway.d.ts.map +0 -1
  187. package/index.d.ts.map +0 -1
  188. package/main.devserver.d.ts.map +0 -1
  189. package/tsgo-check-plugin.d.ts.map +0 -1
  190. package/typecheck-bus.d.ts.map +0 -1
@@ -1,49 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.gatewayCacheDir = gatewayCacheDir;
4
- exports.loadServiceKeys = loadServiceKeys;
5
- const tslib_1 = require("tslib");
6
- /**
7
- * Per-service api keys (be-security: the gateway is the trust boundary).
8
- * Every pod service gets a stable key in `{cacheDir}/service-keys.json`.
9
- * - Public routes: the gateway injects the TARGET's key as `x-internal-token` —
10
- * pods are only reachable through it (unix sockets aren't on the network).
11
- * - `internal.<host>` routes: the CALLER must present its own key as `x-api-key`;
12
- * the gateway validates it, stamps `x-api-key-id` (caller identity) and
13
- * forwards with the target's `x-internal-token`.
14
- */
15
- const node_crypto_1 = require("node:crypto");
16
- const fs = tslib_1.__importStar(require("node:fs"));
17
- const path = tslib_1.__importStar(require("node:path"));
18
- /** The gateway's on-disk cache root — shared by service keys AND the TLS cert cache. */
19
- function gatewayCacheDir(root) {
20
- return path.join(root, 'node_modules', '.cache', 'davnx-webpack');
21
- }
22
- /** Load (or generate + persist) a stable key per pod service, plus the inverse
23
- * key→project map used to authenticate internal callers. Tolerant of a
24
- * read-only fs (keys then live only for this process). */
25
- function loadServiceKeys(root, services) {
26
- const keysDir = gatewayCacheDir(root);
27
- const keysPath = path.join(keysDir, 'service-keys.json');
28
- let serviceKeys = {};
29
- try {
30
- serviceKeys = JSON.parse(fs.readFileSync(keysPath, 'utf8'));
31
- }
32
- catch { /* first run */ }
33
- let dirty = false;
34
- for (const svc of services) {
35
- if (!serviceKeys[svc.project]) {
36
- serviceKeys[svc.project] = (0, node_crypto_1.randomBytes)(16).toString('hex');
37
- dirty = true;
38
- }
39
- }
40
- if (dirty) {
41
- try {
42
- fs.mkdirSync(keysDir, { recursive: true });
43
- fs.writeFileSync(keysPath, JSON.stringify(serviceKeys, null, 2));
44
- }
45
- catch { /* ro fs */ }
46
- }
47
- const keyToProject = new Map(Object.entries(serviceKeys).map(([p, k]) => [k, p]));
48
- return { serviceKeys, keyToProject };
49
- }
1
+ "use strict";var d=Object.defineProperty;var i=(e,t)=>d(e,"name",{value:t,configurable:!0});var u=Object.defineProperty,c=i((e,t)=>u(e,"name",{value:t,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.gatewayCacheDir=gatewayCacheDir,exports.loadServiceKeys=loadServiceKeys;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));function gatewayCacheDir(e){return path.join(e,"node_modules",".cache","davnx-webpack")}i(gatewayCacheDir,"gatewayCacheDir"),c(gatewayCacheDir,"gatewayCacheDir");function loadServiceKeys(e,t){const a=gatewayCacheDir(e),n=path.join(a,"service-keys.json");let r={};try{r=JSON.parse(fs.readFileSync(n,"utf8"))}catch{}let s=!1;for(const o of t)r[o.project]||(r[o.project]=(0,node_crypto_1.randomBytes)(16).toString("hex"),s=!0);if(s)try{fs.mkdirSync(a,{recursive:!0}),fs.writeFileSync(n,JSON.stringify(r,null,2))}catch{}const y=new Map(Object.entries(r).map(([o,p])=>[p,o]));return{serviceKeys:r,keyToProject:y}}i(loadServiceKeys,"loadServiceKeys"),c(loadServiceKeys,"loadServiceKeys");
@@ -424,4 +424,3 @@ export interface AuthVerdict {
424
424
  reason?: GatewayErrorReason;
425
425
  detail?: string;
426
426
  }
427
- //# sourceMappingURL=types.d.ts.map
@@ -1,151 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GATEWAY_KEEPALIVE_TIMEOUT_MS = exports.MAX_PORT_CONFLICT_RETRIES = exports.CHILD_START_FAIL_MS = exports.PORT_FREE_POLL_MS = exports.PORT_FREE_WAIT_MS = exports.LENS_PROBE_COOLDOWN_MS = exports.LENS_PROBE_TIMEOUT_MS = exports.LENS_PROBE_FAILS = exports.LENS_PROBE_GRACE_MS = exports.LENS_PROBE_ENABLED = exports.POD_STALE_RECYCLE_COOLDOWN_MS = exports.POD_STALE_SCAN_MS = exports.POD_RECYCLE_SETTLE_MS = exports.POD_STALE_GRACE_MS = exports.POD_MAX_AGE_MS = exports.POD_RECYCLE_ENABLED = exports.WEDGE_RECYCLE_GRACE_MS = exports.POD_RESPONSE_TIMEOUT_MS = exports.UPSTREAM_HEAL_STEP_MS = exports.UPSTREAM_HEAL_WINDOW_MS = exports.SERVICE_RESTART_WINDOW_MS = exports.SERVICE_MAX_RESTARTS = exports.SERVICE_ERROR_THROTTLE_MS = void 0;
4
- /** Parse a positive-integer env override; fall back to `def` on missing/NaN/negative. `allowZero` lets a
5
- * `0` through (used as a "disable" sentinel, e.g. {@link POD_MAX_AGE_MS}); otherwise `0` also falls back. */
6
- function envInt(name, def, allowZero = false) {
7
- const n = Number(process.env[name]);
8
- if (!Number.isFinite(n) || n < 0)
9
- return def;
10
- if (n === 0)
11
- return allowZero ? 0 : def;
12
- return n;
13
- }
14
- /** Min interval (ms) between repeat 'service error' emits for the SAME service + same error line. */
15
- exports.SERVICE_ERROR_THROTTLE_MS = 5000;
16
- /** Self-heal: an unexpectedly-exited pod (crash, not a clean exit or a build error) is auto-respawned
17
- * after a backoff, capped at {@link SERVICE_MAX_RESTARTS} within {@link SERVICE_RESTART_WINDOW_MS} so a
18
- * genuinely-broken service doesn't hot-loop the CPU. */
19
- exports.SERVICE_MAX_RESTARTS = 5;
20
- exports.SERVICE_RESTART_WINDOW_MS = 60_000;
21
- /** Self-heal: how long the proxy retries a CONNECTION-class upstream error (a Vite dev server restarting
22
- * its internal server on a config change → the port refuses for ~1-3s; a pod warming up) before it
23
- * finally 502s. Retried only BEFORE any response byte is written, and only for a connection error. */
24
- exports.UPSTREAM_HEAL_WINDOW_MS = 8000;
25
- exports.UPSTREAM_HEAL_STEP_MS = 300;
26
- /** Self-heal: how long the proxy waits for a POD to start responding (first byte / headers) before it
27
- * treats the pod as WEDGED — a child that ACCEPTS its unix socket but never replies (e.g. a stale socket
28
- * left by an HMR hot-swap). A connection error is caught by http-proxy's error cb immediately, but an
29
- * accepted-then-silent socket would otherwise pin the request until {@link http-proxy}'s `proxyTimeout`
30
- * (120s) — the "request pending 2+ minutes" symptom. On this deadline the proxy EVICTS the pod (drops its
31
- * socket + unlinks the ghost file) so the lifecycle sweeper / scale-from-zero respawns it, and 502s fast.
32
- * Only fires BEFORE any response byte (a legit long request streams AFTER headers, unaffected).
33
- * Override with `LENSMCP_POD_RESPONSE_TIMEOUT_MS`. */
34
- exports.POD_RESPONSE_TIMEOUT_MS = (() => {
35
- const n = Number(process.env['LENSMCP_POD_RESPONSE_TIMEOUT_MS']);
36
- return Number.isFinite(n) && n > 0 ? n : 30_000;
37
- })();
38
- /**
39
- * Self-heal: how long a service may sit WEDGED — its gateway-spawned parent process ALIVE but with ZERO
40
- * live pods (an HMR hot-swap evicted the child's socket, and `ensureUp` won't respawn because its
41
- * `!svc.proc` guard sees the alive parent) — before the sweeper force-RECYCLES it (kill the wedged tree →
42
- * respawn a fresh pod). This closes the evict→respawn gap: `evictPod` (proxy) empties the pool but never
43
- * touches `svc.proc`, so a "next request scale-from-zero" is structurally blocked. The grace absorbs a
44
- * legit in-process hot-swap (the socket is briefly gone, then re-bound). A code (build) error is left to
45
- * the user's fix but still recycled under the crash-loop cap. Override with `LENSMCP_WEDGE_RECYCLE_GRACE_MS`. */
46
- exports.WEDGE_RECYCLE_GRACE_MS = (() => {
47
- const n = Number(process.env['LENSMCP_WEDGE_RECYCLE_GRACE_MS']);
48
- return Number.isFinite(n) && n > 0 ? n : 15_000;
49
- })();
50
- /**
51
- * ZOMBIE-POD RECYCLE (dev only). The exit-respawn (crashed pod) and wedge-recycle (live parent, 0 live
52
- * pods) self-heals both assume a DEAD or SOCKET-LESS pod. This one catches the third kind: a pod that is
53
- * ALIVE and reachable (a live socket, answers requests) yet serves a STALE FILESYSTEM VIEW — its dev-server
54
- * file-watcher degraded (macOS fsevents after long uptime / a sleep-wake) and MISSED files created AFTER it
55
- * spawned, so it 500s `Failed to resolve import "./x"` for a file that EXISTS on disk (the reported
56
- * incident: a 17.7h-old vite pod that predated new files by ~6h). The sweeper detects it two ways and
57
- * recycles the pod (kill → scale-from-zero → a fresh pod re-scans the FS) — but ONLY when the pod is idle
58
- * (no in-flight request + a brief lull), so an active pod is never interrupted.
59
- */
60
- /** Master switch — set `LENSMCP_POD_RECYCLE=0` to disable zombie-pod recycling entirely. Default ON (dev). */
61
- exports.POD_RECYCLE_ENABLED = process.env['LENSMCP_POD_RECYCLE'] !== '0';
62
- /** Backstop: recycle a gateway-spawned pod once it is older than this, even with no detected source change
63
- * (a long-lived pod is the one whose watcher degrades). `0` disables the age backstop. Override
64
- * `LENSMCP_POD_MAX_AGE_MS`. Default 4h. */
65
- exports.POD_MAX_AGE_MS = envInt('LENSMCP_POD_MAX_AGE_MS', 4 * 60 * 60_000, true);
66
- /** A pod is "stale" when the source file SET changed (a file added/removed — NOT a content change, so
67
- * ordinary saves never recycle) more than this AFTER the pod spawned. The grace absorbs clock skew + the
68
- * pod's own first-compile writes. Override `LENSMCP_POD_STALE_GRACE_MS`. Default 10s. */
69
- exports.POD_STALE_GRACE_MS = envInt('LENSMCP_POD_STALE_GRACE_MS', 10_000);
70
- /** Recycle only after the pod has been quiet this long (0 in-flight AND lastUsed older than this) — a busy
71
- * pod is never interrupted; the recycle lands in an inter-request lull. Override
72
- * `LENSMCP_POD_RECYCLE_SETTLE_MS`. Default 8s. */
73
- exports.POD_RECYCLE_SETTLE_MS = envInt('LENSMCP_POD_RECYCLE_SETTLE_MS', 8_000);
74
- /** How often the sweeper rescans the workspace source file SET signature (a bounded, async, non-blocking
75
- * walk). Override `LENSMCP_POD_STALE_SCAN_MS`. Default 30s. */
76
- exports.POD_STALE_SCAN_MS = envInt('LENSMCP_POD_STALE_SCAN_MS', 30_000);
77
- /** Per-service cooldown between staleness recycles — stops a source-churn burst (codegen) thrash-recycling
78
- * the same pod. Override `LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS`. Default 60s. */
79
- exports.POD_STALE_RECYCLE_COOLDOWN_MS = envInt('LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS', 60_000);
80
- /**
81
- * LENS-CHILD LIVENESS PROBE (dev only). The FOURTH failure mode, and the one that wedges an app in a
82
- * PERMANENT 502 with nothing in the log: a gateway-spawned vite that is ALIVE but NOT LISTENING. Child
83
- * health was modelled ONLY as "has the process exited", so a vite whose HTTP listener closes/faults while
84
- * its event loop stays held open (by the lensmcp plugin's WS server + the bridge connections) is INVISIBLE:
85
- * `viteChildren` still holds it as live, no `lens child vite exited` line is ever logged, and the
86
- * exit-driven auto-heal never fires. Worked failure (foodguard, 2026-07-29): two 43-minute-old vite children
87
- * (pids 89518/89519) that had both printed `VITE ready in 1005 ms` + `Local: http://localhost:4200/`, yet
88
- * `lsof -nP -p 89519` showed ZERO LISTEN sockets and `lsof -iTCP:4200 -sTCP:LISTEN` was empty —
89
- * `https://app.foodguard.local` 502'd indefinitely until the pids were hand-killed (the exit handler then
90
- * respawned them and the app returned 200 within 8s). So the sweeper now TCP-probes each live child's pinned
91
- * port and SIGKILLs a confirmed wedge, letting the proven exit→auto-heal path do the rest.
92
- */
93
- /** Master switch for the liveness probe. Deliberately INDEPENDENT of {@link POD_RECYCLE_ENABLED}: a user who
94
- * sets `LENSMCP_POD_RECYCLE=0` to stop staleness churn still wants a wedged 502 healed. Set
95
- * `LENSMCP_LENS_PROBE=0` to disable. Default ON (dev). */
96
- exports.LENS_PROBE_ENABLED = process.env['LENSMCP_LENS_PROBE'] !== '0';
97
- /** Spawn grace: a child is never probed (and so never recycled) until it has had this long to BIND its port
98
- * — a cold vite/nx boot in a big monorepo takes many seconds, and killing a still-booting dev server would
99
- * be a self-inflicted outage. Override `LENSMCP_LENS_PROBE_GRACE_MS`. Default 45s. */
100
- exports.LENS_PROBE_GRACE_MS = envInt('LENSMCP_LENS_PROBE_GRACE_MS', 45_000);
101
- /** CONSECUTIVE probe failures required before a recycle — one blip (a momentary accept backlog, a laptop
102
- * sleep-wake, a GC pause) must never recycle a healthy dev server. Floored at 2 by
103
- * {@link lensChildWedged}. Override `LENSMCP_LENS_PROBE_FAILS`. Default 2. */
104
- exports.LENS_PROBE_FAILS = envInt('LENSMCP_LENS_PROBE_FAILS', 2);
105
- /** Per-probe TCP connect timeout. Short: a listening socket accepts a loopback connect in microseconds, so
106
- * anything slower is indistinguishable from absent. Override `LENSMCP_LENS_PROBE_TIMEOUT_MS`. Default 1s. */
107
- exports.LENS_PROBE_TIMEOUT_MS = envInt('LENSMCP_LENS_PROBE_TIMEOUT_MS', 1_000);
108
- /**
109
- * Anti-thrash window for a WEDGE heal — deliberately SEPARATE from
110
- * {@link POD_STALE_RECYCLE_COOLDOWN_MS}, and short.
111
- *
112
- * The two cooldowns bound opposite risks. A STALENESS recycle is an optimization (the pod still serves;
113
- * its fs view may be behind), so throttling it hard is free — foodguard sets that override to 15min to bound
114
- * the recycle churn a busy monorepo generates. A WEDGE is a hard outage: the pod is alive but has NO
115
- * listener, so every request is a `gateway.unavailable` 502 until something kills it. Recycling a wedged pod
116
- * can never be "unnecessary" — the only thing a long cooldown buys there is a longer outage.
117
- *
118
- * Worked failure (foodguard, 2026-07-30): the wedge probe shipped reusing the staleness cooldown, so that
119
- * workspace's 15min override silently gated it too — the dashboard sat at 502 with the probe correctly
120
- * detecting the wedge and then declining to act, and a human killed the pod by hand (three times in one
121
- * session). Override `LENSMCP_LENS_PROBE_COOLDOWN_MS`. Default 30s — long enough that a pod which wedges
122
- * immediately on respawn backs off instead of spinning, short enough that a wedge is a blip, not an outage.
123
- */
124
- exports.LENS_PROBE_COOLDOWN_MS = envInt('LENSMCP_LENS_PROBE_COOLDOWN_MS', 30_000);
125
- /**
126
- * RESPAWN-INTO-A-HELD-PORT (the EADDRINUSE self-inflicted crash). The auto-heal respawned after a FIXED
127
- * backoff while the previous child still held the pinned port — vite's `--strictPort` then exits code=1 on
128
- * bind, burning a heal attempt and EXTENDING the 502 window. Observed in the gateway log:
129
- * `Error: listen EADDRINUSE: address already in use 127.0.0.1:50059` immediately followed by
130
- * `lens child vite exited (code=1 sig=null); auto-healing in 3s (attempt 2).` (3 such events in one log).
131
- * Two fixes: WAIT for the port to become bindable before spawning, and classify a bind-race exit as
132
- * RETRYABLE so it does not consume the crash-loop budget a real code fault does.
133
- */
134
- /** How long a respawn polls for its pinned port to become bindable before spawning anyway (past the budget
135
- * the normal heal backoff takes over). Override `LENSMCP_PORT_FREE_WAIT_MS`. Default 10s. */
136
- exports.PORT_FREE_WAIT_MS = envInt('LENSMCP_PORT_FREE_WAIT_MS', 10_000);
137
- /** Poll interval while waiting for a port to free. Override `LENSMCP_PORT_FREE_POLL_MS`. Default 250ms. */
138
- exports.PORT_FREE_POLL_MS = envInt('LENSMCP_PORT_FREE_POLL_MS', 250);
139
- /** A non-zero exit sooner than this is a START failure (it never bound); later means the child RAN and so
140
- * had the port, which cannot be a bind race. Override `LENSMCP_CHILD_START_FAIL_MS`. Default 10s. */
141
- exports.CHILD_START_FAIL_MS = envInt('LENSMCP_CHILD_START_FAIL_MS', 10_000);
142
- /** Ultimate backstop: after this many CONSECUTIVE bind-race exits for one label, stop forgiving them and
143
- * fall back to the normal exponential backoff (so a permanently-squatted port can't 10s-loop forever).
144
- * Override `LENSMCP_MAX_PORT_CONFLICT_RETRIES`. Default 5. */
145
- exports.MAX_PORT_CONFLICT_RETRIES = envInt('LENSMCP_MAX_PORT_CONFLICT_RETRIES', 5);
146
- /** Inbound keep-alive idle window for the front door. MUST exceed every client's pooled-socket idle timeout
147
- * so the SERVER never closes a keep-alive socket a client is about to reuse — otherwise both sides race the
148
- * same deadline and the truncated request surfaces as a spurious front-door `400` (see
149
- * `classifyClientError`). Node's server default is 5s and a Node client agent's is ~5s, i.e. exactly tied.
150
- * Override `LENSMCP_KEEPALIVE_TIMEOUT_MS`. Default 76s. */
151
- exports.GATEWAY_KEEPALIVE_TIMEOUT_MS = envInt('LENSMCP_KEEPALIVE_TIMEOUT_MS', 76_000);
1
+ "use strict";var R=Object.defineProperty;var e=(_,E)=>R(_,"name",{value:E,configurable:!0});var O=Object.defineProperty,L=e((_,E)=>O(_,"name",{value:E,configurable:!0}),"L");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=exports.MAX_PORT_CONFLICT_RETRIES=exports.CHILD_START_FAIL_MS=exports.PORT_FREE_POLL_MS=exports.PORT_FREE_WAIT_MS=exports.LENS_PROBE_COOLDOWN_MS=exports.LENS_PROBE_TIMEOUT_MS=exports.LENS_PROBE_FAILS=exports.LENS_PROBE_GRACE_MS=exports.LENS_PROBE_ENABLED=exports.POD_STALE_RECYCLE_COOLDOWN_MS=exports.POD_STALE_SCAN_MS=exports.POD_RECYCLE_SETTLE_MS=exports.POD_STALE_GRACE_MS=exports.POD_MAX_AGE_MS=exports.POD_RECYCLE_ENABLED=exports.WEDGE_RECYCLE_GRACE_MS=exports.POD_RESPONSE_TIMEOUT_MS=exports.UPSTREAM_HEAL_STEP_MS=exports.UPSTREAM_HEAL_WINDOW_MS=exports.SERVICE_RESTART_WINDOW_MS=exports.SERVICE_MAX_RESTARTS=exports.SERVICE_ERROR_THROTTLE_MS=void 0;function envInt(_,E,M=!1){const S=Number(process.env[_]);return!Number.isFinite(S)||S<0?E:S===0?M?0:E:S}e(envInt,"envInt"),L(envInt,"envInt"),exports.SERVICE_ERROR_THROTTLE_MS=5e3,exports.SERVICE_MAX_RESTARTS=5,exports.SERVICE_RESTART_WINDOW_MS=6e4,exports.UPSTREAM_HEAL_WINDOW_MS=8e3,exports.UPSTREAM_HEAL_STEP_MS=300,exports.POD_RESPONSE_TIMEOUT_MS=(()=>{const _=Number(process.env.LENSMCP_POD_RESPONSE_TIMEOUT_MS);return Number.isFinite(_)&&_>0?_:3e4})(),exports.WEDGE_RECYCLE_GRACE_MS=(()=>{const _=Number(process.env.LENSMCP_WEDGE_RECYCLE_GRACE_MS);return Number.isFinite(_)&&_>0?_:15e3})(),exports.POD_RECYCLE_ENABLED=process.env.LENSMCP_POD_RECYCLE!=="0",exports.POD_MAX_AGE_MS=envInt("LENSMCP_POD_MAX_AGE_MS",240*6e4,!0),exports.POD_STALE_GRACE_MS=envInt("LENSMCP_POD_STALE_GRACE_MS",1e4),exports.POD_RECYCLE_SETTLE_MS=envInt("LENSMCP_POD_RECYCLE_SETTLE_MS",8e3),exports.POD_STALE_SCAN_MS=envInt("LENSMCP_POD_STALE_SCAN_MS",3e4),exports.POD_STALE_RECYCLE_COOLDOWN_MS=envInt("LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS",6e4),exports.LENS_PROBE_ENABLED=process.env.LENSMCP_LENS_PROBE!=="0",exports.LENS_PROBE_GRACE_MS=envInt("LENSMCP_LENS_PROBE_GRACE_MS",45e3),exports.LENS_PROBE_FAILS=envInt("LENSMCP_LENS_PROBE_FAILS",2),exports.LENS_PROBE_TIMEOUT_MS=envInt("LENSMCP_LENS_PROBE_TIMEOUT_MS",1e3),exports.LENS_PROBE_COOLDOWN_MS=envInt("LENSMCP_LENS_PROBE_COOLDOWN_MS",3e4),exports.PORT_FREE_WAIT_MS=envInt("LENSMCP_PORT_FREE_WAIT_MS",1e4),exports.PORT_FREE_POLL_MS=envInt("LENSMCP_PORT_FREE_POLL_MS",250),exports.CHILD_START_FAIL_MS=envInt("LENSMCP_CHILD_START_FAIL_MS",1e4),exports.MAX_PORT_CONFLICT_RETRIES=envInt("LENSMCP_MAX_PORT_CONFLICT_RETRIES",5),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=envInt("LENSMCP_KEEPALIVE_TIMEOUT_MS",76e3);
@@ -19,4 +19,3 @@ export interface UpgradeDeps {
19
19
  }
20
20
  export type UpgradeListener = (req: http.IncomingMessage, socket: stream.Duplex, head: Buffer) => void;
21
21
  export declare function createUpgrade(deps: UpgradeDeps): UpgradeListener;
22
- //# sourceMappingURL=upgrade.d.ts.map
@@ -1,71 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createUpgrade = createUpgrade;
4
- const manifest_1 = require("../manifest");
5
- const discovery_1 = require("./discovery");
6
- const edge_1 = require("./edge");
7
- function createUpgrade(deps) {
8
- const { rt, auth, proxy, hooks } = deps;
9
- const match = (req) => (0, manifest_1.matchRoute)(rt.routes, (req.headers.host || '').split(':')[0], req.url ?? '/');
10
- const run = async (req, socket, head) => {
11
- for (const h of edge_1.GATEWAY_OWNED_HEADERS)
12
- delete req.headers[h];
13
- const route = match(req);
14
- if (!route) {
15
- socket.destroy();
16
- return;
17
- }
18
- const ctx = hooks.upgradeContext(req, socket, undefined);
19
- ctx.route = route;
20
- if (route.internal) {
21
- const v = auth.internal(req, route, undefined);
22
- if (!v.ok) {
23
- socket.destroy();
24
- return;
25
- } // no valid key → no internal WS
26
- ctx.caller = v.caller;
27
- }
28
- else {
29
- const v = auth.public(req, route, undefined);
30
- if (!v.ok) {
31
- socket.destroy();
32
- return;
33
- }
34
- ctx.claims = v.claims;
35
- // afterAuth (= builtin middleware first, then user hooks) — public branch only
36
- if (hooks.has('afterAuth')) {
37
- await hooks.run('afterAuth', ctx);
38
- if (ctx.responded())
39
- return;
40
- }
41
- }
42
- if (route.pool && rt.serviceKeys[route.project]) {
43
- req.headers['x-internal-token'] = rt.serviceKeys[route.project];
44
- }
45
- if (route.prependPrefix && !req.url?.startsWith(route.prependPrefix)) {
46
- req.url = route.prependPrefix + (req.url ?? '/');
47
- }
48
- if (hooks.has('onUpgrade')) {
49
- await hooks.run('onUpgrade', ctx);
50
- if (ctx.responded())
51
- return;
52
- }
53
- const target = route.pool
54
- ? (() => { const s = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs); return s ? { socketPath: s } : undefined; })()
55
- : route.target;
56
- if (!target) {
57
- socket.destroy();
58
- return;
59
- }
60
- proxy.proxy.ws(req, socket, head, { target, changeOrigin: !route.pool, agent: proxy.agentForTarget(target) }, () => socket.destroy());
61
- };
62
- return (req, socket, head) => {
63
- // Guard the handed-off socket IMMEDIATELY: on 'upgrade' the http layer removes its OWN listeners,
64
- // so until proxy.ws attaches handlers (an `await` away in `run`) a client abort (ECONNRESET — an
65
- // HMR socket torn mid-upgrade) would be an unhandled 'error' event → the whole daemon dies. And a
66
- // rejection out of `run` (a throwing auth/hook) must likewise cost the ONE socket, never the
67
- // process — a floating promise rejection is fatal by default.
68
- socket.on('error', () => socket.destroy());
69
- run(req, socket, head).catch(() => socket.destroy());
70
- };
71
- }
1
+ "use strict";var v=Object.defineProperty;var p=(i,n)=>v(i,"name",{value:n,configurable:!0});var y=Object.defineProperty,a=p((i,n)=>y(i,"name",{value:n,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpgrade=createUpgrade;const manifest_1=require("../manifest"),discovery_1=require("./discovery"),edge_1=require("./edge");function createUpgrade(i){const{rt:n,auth:l,proxy:f,hooks:c}=i,h=a(e=>(0,manifest_1.matchRoute)(n.routes,(e.headers.host||"").split(":")[0],e.url??"/"),"match"),g=a(async(e,t,d)=>{for(const o of edge_1.GATEWAY_OWNED_HEADERS)delete e.headers[o];const r=h(e);if(!r){t.destroy();return}const s=c.upgradeContext(e,t,void 0);if(s.route=r,r.internal){const o=l.internal(e,r,void 0);if(!o.ok){t.destroy();return}s.caller=o.caller}else{const o=l.public(e,r,void 0);if(!o.ok){t.destroy();return}if(s.claims=o.claims,c.has("afterAuth")&&(await c.run("afterAuth",s),s.responded()))return}if(r.pool&&n.serviceKeys[r.project]&&(e.headers["x-internal-token"]=n.serviceKeys[r.project]),r.prependPrefix&&!e.url?.startsWith(r.prependPrefix)&&(e.url=r.prependPrefix+(e.url??"/")),c.has("onUpgrade")&&(await c.run("onUpgrade",s),s.responded()))return;const u=r.pool?(()=>{const o=(0,discovery_1.pickSock)(r.pool,n.scanTtlMs);return o?{socketPath:o}:void 0})():r.target;if(!u){t.destroy();return}f.proxy.ws(e,t,d,{target:u,changeOrigin:!r.pool,agent:f.agentForTarget(u)},()=>t.destroy())},"run");return(e,t,d)=>{t.on("error",()=>t.destroy()),g(e,t,d).catch(()=>t.destroy())}}p(createUpgrade,"createUpgrade"),a(createUpgrade,"createUpgrade");
@@ -37,4 +37,3 @@ export declare function unregisterWorkspace(key: string, file?: string): void;
37
37
  * (recycled pids fail the start-time comparison instead of impersonating a live gateway).
38
38
  */
39
39
  export declare function isWorkspaceLive(entry: Pick<WorkspaceEntry, 'pid' | 'procStamp'>): boolean;
40
- //# sourceMappingURL=workspace-registry.d.ts.map
@@ -1,99 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processStamp = processStamp;
4
- exports.registryPath = registryPath;
5
- exports.readWorkspaces = readWorkspaces;
6
- exports.registerWorkspace = registerWorkspace;
7
- exports.unregisterWorkspace = unregisterWorkspace;
8
- exports.isWorkspaceLive = isWorkspaceLive;
9
- const tslib_1 = require("tslib");
10
- /**
11
- * The GLOBAL workspace registry — `~/.lensmcp/workspaces.json`. Each gateway upserts its own workspace
12
- * on start and drops it on a graceful stop, so the shared landing page at `lensmcp.local/` can list EVERY
13
- * workspace LensMCP knows about (not just the one whose gateway currently owns :443). It's the persistence
14
- * seam the multi-workspace daemon (planning/multi-workspace-gateway.md) builds on: today it powers the
15
- * root chooser; the daemon will read the same file to decide which workspaces to host.
16
- *
17
- * Concurrency: read-modify-write, best-effort. Writes are rare (start/stop only) and the file is tiny, so
18
- * a lost update at most drops one entry from the listing until the next start — never a correctness issue.
19
- */
20
- const node_child_process_1 = require("node:child_process");
21
- const fs = tslib_1.__importStar(require("node:fs"));
22
- const os = tslib_1.__importStar(require("node:os"));
23
- const path = tslib_1.__importStar(require("node:path"));
24
- /**
25
- * The start-time identity stamp for a pid (`''` when `ps` is unavailable — Windows, restricted
26
- * environments — or the process is gone). Start time is immutable for the life of a process and
27
- * survives in no recycled successor, which makes it the cheapest cross-platform-ish identity proof.
28
- */
29
- function processStamp(pid) {
30
- if (!pid || pid <= 0 || process.platform === 'win32')
31
- return '';
32
- try {
33
- return (0, node_child_process_1.execFileSync)('ps', ['-p', String(pid), '-o', 'lstart='], {
34
- encoding: 'utf8',
35
- timeout: 2_000,
36
- stdio: ['ignore', 'pipe', 'ignore'],
37
- }).trim();
38
- }
39
- catch {
40
- return '';
41
- }
42
- }
43
- /** `~/.lensmcp/workspaces.json` — override the home dir via `LENSMCP_HOME` (tests). */
44
- function registryPath() {
45
- const home = process.env['LENSMCP_HOME'] || os.homedir();
46
- return path.join(home, '.lensmcp', 'workspaces.json');
47
- }
48
- function readWorkspaces(file = registryPath()) {
49
- try {
50
- const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
51
- if (!Array.isArray(raw))
52
- return [];
53
- // tolerate partial/corrupt entries — keep only well-formed ones.
54
- return raw.filter((e) => !!e && typeof e.key === 'string' && typeof e.base === 'string');
55
- }
56
- catch {
57
- return [];
58
- }
59
- }
60
- function writeWorkspaces(list, file = registryPath()) {
61
- try {
62
- fs.mkdirSync(path.dirname(file), { recursive: true });
63
- fs.writeFileSync(file, JSON.stringify(list, null, 2));
64
- }
65
- catch {
66
- /* best-effort — a read-only home or a race must never crash the gateway */
67
- }
68
- }
69
- /** Upsert this workspace (keyed by `key`) — called on gateway start. */
70
- function registerWorkspace(entry, file = registryPath()) {
71
- const list = readWorkspaces(file).filter((e) => e.key !== entry.key);
72
- list.push(entry);
73
- list.sort((a, b) => a.key.localeCompare(b.key));
74
- writeWorkspaces(list, file);
75
- }
76
- /** Drop this workspace — called on a graceful gateway stop. */
77
- function unregisterWorkspace(key, file = registryPath()) {
78
- writeWorkspaces(readWorkspaces(file).filter((e) => e.key !== key), file);
79
- }
80
- /**
81
- * Is the gateway that registered this workspace still alive? `process.kill(pid, 0)` throws ESRCH if
82
- * not — and when the entry carries a `procStamp`, the pid must ALSO still be the SAME process
83
- * (recycled pids fail the start-time comparison instead of impersonating a live gateway).
84
- */
85
- function isWorkspaceLive(entry) {
86
- if (!entry.pid || entry.pid <= 0)
87
- return false;
88
- try {
89
- process.kill(entry.pid, 0);
90
- }
91
- catch (e) {
92
- if (e.code !== 'EPERM')
93
- return false; // EPERM = exists but not signalable; ESRCH = gone
94
- }
95
- if (!entry.procStamp)
96
- return true; // legacy entry / no-ps platform — pid liveness is all we have
97
- const now = processStamp(entry.pid);
98
- return now === '' ? true : now === entry.procStamp; // '' = ps unavailable here — don't false-negative
99
- }
1
+ "use strict";var p=Object.defineProperty;var i=(e,r)=>p(e,"name",{value:r,configurable:!0});var c=Object.defineProperty,s=i((e,r)=>c(e,"name",{value:r,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.processStamp=processStamp,exports.registryPath=registryPath,exports.readWorkspaces=readWorkspaces,exports.registerWorkspace=registerWorkspace,exports.unregisterWorkspace=unregisterWorkspace,exports.isWorkspaceLive=isWorkspaceLive;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path"));function processStamp(e){if(!e||e<=0||process.platform==="win32")return"";try{return(0,node_child_process_1.execFileSync)("ps",["-p",String(e),"-o","lstart="],{encoding:"utf8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}catch{return""}}i(processStamp,"processStamp"),s(processStamp,"processStamp");function registryPath(){const e=process.env.LENSMCP_HOME||os.homedir();return path.join(e,".lensmcp","workspaces.json")}i(registryPath,"registryPath"),s(registryPath,"registryPath");function readWorkspaces(e=registryPath()){try{const r=JSON.parse(fs.readFileSync(e,"utf8"));return Array.isArray(r)?r.filter(t=>!!t&&typeof t.key=="string"&&typeof t.base=="string"):[]}catch{return[]}}i(readWorkspaces,"readWorkspaces"),s(readWorkspaces,"readWorkspaces");function writeWorkspaces(e,r=registryPath()){try{fs.mkdirSync(path.dirname(r),{recursive:!0}),fs.writeFileSync(r,JSON.stringify(e,null,2))}catch{}}i(writeWorkspaces,"writeWorkspaces"),s(writeWorkspaces,"writeWorkspaces");function registerWorkspace(e,r=registryPath()){const t=readWorkspaces(r).filter(o=>o.key!==e.key);t.push(e),t.sort((o,n)=>o.key.localeCompare(n.key)),writeWorkspaces(t,r)}i(registerWorkspace,"registerWorkspace"),s(registerWorkspace,"registerWorkspace");function unregisterWorkspace(e,r=registryPath()){writeWorkspaces(readWorkspaces(r).filter(t=>t.key!==e),r)}i(unregisterWorkspace,"unregisterWorkspace"),s(unregisterWorkspace,"unregisterWorkspace");function isWorkspaceLive(e){if(!e.pid||e.pid<=0)return!1;try{process.kill(e.pid,0)}catch(t){if(t.code!=="EPERM")return!1}if(!e.procStamp)return!0;const r=processStamp(e.pid);return r===""?!0:r===e.procStamp}i(isWorkspaceLive,"isWorkspaceLive"),s(isWorkspaceLive,"isWorkspaceLive");
@@ -94,4 +94,3 @@ type LookupCallback = (err: NodeJS.ErrnoException | null, address?: string | Loo
94
94
  */
95
95
  export declare function pinnedLookup(addresses: readonly string[]): (hostname: string, options: unknown, callback?: LookupCallback) => void;
96
96
  export {};
97
- //# sourceMappingURL=ssrf-guard.d.ts.map
@@ -1,190 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SsrfBlockedError = void 0;
4
- exports.isBlockedAddress = isBlockedAddress;
5
- exports.assertFetchableUrl = assertFetchableUrl;
6
- exports.safeFetch = safeFetch;
7
- exports.pinnedLookup = pinnedLookup;
8
- /**
9
- * SSRF guard for OUTBOUND fetches of a request- or config-supplied URL.
10
- *
11
- * The prod gateway today never fetches an attacker-controlled URL — upstreams
12
- * are pinned in the manifest, the JWKS URL is operator env, and reply-from /
13
- * undici do not chase redirects. This module exists so that the MOMENT a sink
14
- * IS added (a remote-MCP / OpenAPI schema import, a webhook callback, a
15
- * dynamically-discovered upstream — anything that fetches a URL it did not
16
- * author) there is a ready, tested guard to wrap it with, instead of a naive
17
- * `if (host === '127.0.0.1')` string check that the classic bypasses defeat.
18
- *
19
- * It defends the three canonical bypasses:
20
- * 1. DNS name that RESOLVES to an internal IP (`127.0.0.1.nip.io`, rebinding)
21
- * → we resolve the host and check the RESOLVED addresses, not the string.
22
- * 2. Redirect chase (`/redirect` 3xx → loopback) → `safeFetch` re-validates
23
- * EVERY hop with `redirect:'manual'`; it never blindly follows a Location.
24
- * 3. IPv4-mapped IPv6 loopback (`[::ffff:127.0.0.1]`, `[::ffff:7f00:1]`) →
25
- * `net.BlockList` normalizes mapped forms against the IPv4 rules.
26
- *
27
- * Residual TOCTOU: between the resolve-and-check and the socket connect, a
28
- * rebinding resolver could flip the record. Close it by pinning the connection
29
- * to the validated address(es) with `pinnedLookup()` (pass to
30
- * `new http.Agent({ lookup })` or an undici `connect:{ lookup }` dispatcher).
31
- *
32
- * Defaults are operator-overridable but FAIL CLOSED: anything unparseable, any
33
- * non-http(s) scheme, embedded credentials, an empty host, or a host that
34
- * resolves to no records is rejected.
35
- */
36
- const node_net_1 = require("node:net");
37
- const promises_1 = require("node:dns/promises");
38
- /** Thrown for every blocked URL — carries a machine-readable `reason` and, when
39
- * the block was an address, the offending `address` (the resolved IP, not the
40
- * hostname, so logs show what the name actually pointed at). */
41
- class SsrfBlockedError extends Error {
42
- constructor(reason, url, address, cause) {
43
- super(`SSRF guard blocked ${url ?? 'request'}: ${reason}${address ? ` (${address})` : ''}`, cause !== undefined ? { cause } : undefined);
44
- this.name = 'SsrfBlockedError';
45
- this.reason = reason;
46
- this.url = url;
47
- this.address = address;
48
- }
49
- }
50
- exports.SsrfBlockedError = SsrfBlockedError;
51
- /** The internal / non-routable ranges an outbound fetch must never reach.
52
- * Mapped IPv4-in-IPv6 (`::ffff:a.b.c.d`) is matched by the IPv4 rules — verified
53
- * in the spec — so we do NOT blanket-block `::ffff:0:0/96` (mapped *public*
54
- * IPv4 is legitimate). */
55
- function buildDefaultBlockList() {
56
- const b = new node_net_1.BlockList();
57
- // ── IPv4 ──
58
- b.addSubnet('0.0.0.0', 8, 'ipv4'); // "this host" / 0.0.0.0
59
- b.addSubnet('10.0.0.0', 8, 'ipv4'); // RFC1918 private
60
- b.addSubnet('100.64.0.0', 10, 'ipv4'); // RFC6598 CGNAT
61
- b.addSubnet('127.0.0.0', 8, 'ipv4'); // loopback
62
- b.addSubnet('169.254.0.0', 16, 'ipv4'); // link-local incl. cloud metadata 169.254.169.254
63
- b.addSubnet('172.16.0.0', 12, 'ipv4'); // RFC1918 private (172.16–172.31)
64
- b.addSubnet('192.0.0.0', 24, 'ipv4'); // IETF protocol assignments
65
- b.addSubnet('192.168.0.0', 16, 'ipv4'); // RFC1918 private
66
- b.addSubnet('198.18.0.0', 15, 'ipv4'); // benchmarking
67
- b.addSubnet('240.0.0.0', 4, 'ipv4'); // reserved + 255.255.255.255 broadcast
68
- // ── IPv6 ──
69
- b.addAddress('::', 'ipv6'); // unspecified
70
- b.addAddress('::1', 'ipv6'); // loopback
71
- b.addSubnet('fc00::', 7, 'ipv6'); // unique-local (ULA)
72
- b.addSubnet('fe80::', 10, 'ipv6'); // link-local
73
- return b;
74
- }
75
- const DEFAULT_BLOCK_LIST = buildDefaultBlockList();
76
- /** True if `ip` (a literal address) falls in a blocked range. Mapped IPv6 is
77
- * normalized by BlockList; an unparseable string is treated as blocked. */
78
- function isBlockedAddress(ip, blockList = DEFAULT_BLOCK_LIST) {
79
- const fam = (0, node_net_1.isIP)(ip);
80
- if (fam === 0)
81
- return true; // not an IP → fail closed
82
- return blockList.check(ip, fam === 6 ? 'ipv6' : 'ipv4');
83
- }
84
- const stripBrackets = (host) => host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
85
- const defaultResolve = async (host) => (await (0, promises_1.lookup)(host, { all: true, verbatim: true })).map((r) => r.address);
86
- /**
87
- * Validate that `rawUrl` is safe to fetch. Rejects (throws `SsrfBlockedError`)
88
- * a malformed URL, a disallowed scheme, embedded credentials, a missing host,
89
- * a literal internal IP, or a host that resolves to ANY internal address.
90
- * Returns the parsed URL and the validated address set on success.
91
- */
92
- async function assertFetchableUrl(rawUrl, opts = {}) {
93
- const protocols = opts.allowedProtocols ?? ['http:', 'https:'];
94
- let url;
95
- try {
96
- url = new URL(rawUrl);
97
- }
98
- catch {
99
- throw new SsrfBlockedError('malformed-url', String(rawUrl));
100
- }
101
- if (!protocols.includes(url.protocol)) {
102
- throw new SsrfBlockedError(`scheme-not-allowed:${url.protocol}`, url.href);
103
- }
104
- if (url.username !== '' || url.password !== '') {
105
- throw new SsrfBlockedError('embedded-credentials', url.href);
106
- }
107
- const hostname = stripBrackets(url.hostname);
108
- if (hostname === '')
109
- throw new SsrfBlockedError('missing-host', url.href);
110
- if (opts.allowPrivate) {
111
- return { url, hostname, addresses: (0, node_net_1.isIP)(hostname) ? [hostname] : [] };
112
- }
113
- const blockList = opts.blockList ?? DEFAULT_BLOCK_LIST;
114
- // Literal IP (covers IPv4, IPv6, and IPv4-mapped IPv6 like [::ffff:127.0.0.1]).
115
- if ((0, node_net_1.isIP)(hostname) !== 0) {
116
- if (isBlockedAddress(hostname, blockList)) {
117
- throw new SsrfBlockedError('blocked-address', url.href, hostname);
118
- }
119
- return { url, hostname, addresses: [hostname] };
120
- }
121
- // DNS name → resolve and check EVERY record (defeats nip.io / rebinding).
122
- const resolve = opts.resolve ?? defaultResolve;
123
- let addresses;
124
- try {
125
- addresses = await resolve(hostname);
126
- }
127
- catch (e) {
128
- throw new SsrfBlockedError('dns-resolution-failed', url.href, undefined, e);
129
- }
130
- if (addresses.length === 0)
131
- throw new SsrfBlockedError('dns-no-records', url.href);
132
- for (const addr of addresses) {
133
- if (isBlockedAddress(addr, blockList)) {
134
- throw new SsrfBlockedError('blocked-resolved-address', url.href, addr);
135
- }
136
- }
137
- return { url, hostname, addresses };
138
- }
139
- /**
140
- * A `fetch` that validates the target — and re-validates every redirect hop —
141
- * with the SSRF guard. Redirects are handled manually (`redirect:'manual'`) so
142
- * a 3xx Location to an internal host is checked BEFORE it is followed, never
143
- * chased blindly. Returns the final non-redirect `Response`.
144
- *
145
- * Note: each hop re-issues with the original `init` (method + body preserved —
146
- * the security-relevant worst case for 307/308). A streamed request body cannot
147
- * be replayed across hops.
148
- */
149
- async function safeFetch(rawUrl, init = {}, opts = {}) {
150
- const doFetch = opts.fetchImpl ?? globalThis.fetch;
151
- const maxRedirects = opts.maxRedirects ?? 5;
152
- let current = rawUrl;
153
- for (let hop = 0;; hop++) {
154
- const { url } = await assertFetchableUrl(current, opts); // re-validate THIS hop
155
- const res = await doFetch(url, { ...init, redirect: 'manual' });
156
- const location = res.headers.get('location');
157
- if (res.status >= 300 && res.status < 400 && location) {
158
- if (hop >= maxRedirects)
159
- throw new SsrfBlockedError('too-many-redirects', url.href);
160
- current = new URL(location, url).href; // resolve relative; loop re-validates next hop
161
- continue;
162
- }
163
- return res;
164
- }
165
- }
166
- /**
167
- * A `dns.lookup`-compatible function that ONLY ever returns the pre-validated
168
- * `addresses`. Pass it to `new http.Agent({ lookup })` or an undici
169
- * `connect:{ lookup }` dispatcher so the socket connects to an address the
170
- * guard already cleared — closing the DNS-rebinding TOCTOU between check and
171
- * connect. Use the `addresses` from `assertFetchableUrl()`.
172
- */
173
- function pinnedLookup(addresses) {
174
- const recs = addresses.map((address) => ({
175
- address,
176
- family: (0, node_net_1.isIP)(address) === 6 ? 6 : 4,
177
- }));
178
- return (hostname, options, callback) => {
179
- const cb = (typeof options === 'function' ? options : callback);
180
- const all = typeof options === 'object' && options !== null && options.all === true;
181
- if (recs.length === 0) {
182
- cb(new Error(`SSRF guard: no pinned address for ${hostname}`));
183
- return;
184
- }
185
- if (all)
186
- cb(null, recs);
187
- else
188
- cb(null, recs[0].address, recs[0].family);
189
- };
190
- }
1
+ "use strict";var h=Object.defineProperty;var l=(e,t)=>h(e,"name",{value:t,configurable:!0});var u=Object.defineProperty,i=l((e,t)=>u(e,"name",{value:t,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.SsrfBlockedError=void 0,exports.isBlockedAddress=isBlockedAddress,exports.assertFetchableUrl=assertFetchableUrl,exports.safeFetch=safeFetch,exports.pinnedLookup=pinnedLookup;const node_net_1=require("node:net"),promises_1=require("node:dns/promises");class SsrfBlockedError extends Error{static{l(this,"SsrfBlockedError")}static{i(this,"SsrfBlockedError")}constructor(t,o,s,r){super(`SSRF guard blocked ${o??"request"}: ${t}${s?` (${s})`:""}`,r!==void 0?{cause:r}:void 0),this.name="SsrfBlockedError",this.reason=t,this.url=o,this.address=s}}exports.SsrfBlockedError=SsrfBlockedError;function buildDefaultBlockList(){const e=new node_net_1.BlockList;return e.addSubnet("0.0.0.0",8,"ipv4"),e.addSubnet("10.0.0.0",8,"ipv4"),e.addSubnet("100.64.0.0",10,"ipv4"),e.addSubnet("127.0.0.0",8,"ipv4"),e.addSubnet("169.254.0.0",16,"ipv4"),e.addSubnet("172.16.0.0",12,"ipv4"),e.addSubnet("192.0.0.0",24,"ipv4"),e.addSubnet("192.168.0.0",16,"ipv4"),e.addSubnet("198.18.0.0",15,"ipv4"),e.addSubnet("240.0.0.0",4,"ipv4"),e.addAddress("::","ipv6"),e.addAddress("::1","ipv6"),e.addSubnet("fc00::",7,"ipv6"),e.addSubnet("fe80::",10,"ipv6"),e}l(buildDefaultBlockList,"buildDefaultBlockList"),i(buildDefaultBlockList,"buildDefaultBlockList");const DEFAULT_BLOCK_LIST=buildDefaultBlockList();function isBlockedAddress(e,t=DEFAULT_BLOCK_LIST){const o=(0,node_net_1.isIP)(e);return o===0?!0:t.check(e,o===6?"ipv6":"ipv4")}l(isBlockedAddress,"isBlockedAddress"),i(isBlockedAddress,"isBlockedAddress");const stripBrackets=i(e=>e.startsWith("[")&&e.endsWith("]")?e.slice(1,-1):e,"stripBrackets"),defaultResolve=i(async e=>(await(0,promises_1.lookup)(e,{all:!0,verbatim:!0})).map(t=>t.address),"defaultResolve");async function assertFetchableUrl(e,t={}){const o=t.allowedProtocols??["http:","https:"];let s;try{s=new URL(e)}catch{throw new SsrfBlockedError("malformed-url",String(e))}if(!o.includes(s.protocol))throw new SsrfBlockedError(`scheme-not-allowed:${s.protocol}`,s.href);if(s.username!==""||s.password!=="")throw new SsrfBlockedError("embedded-credentials",s.href);const r=stripBrackets(s.hostname);if(r==="")throw new SsrfBlockedError("missing-host",s.href);if(t.allowPrivate)return{url:s,hostname:r,addresses:(0,node_net_1.isIP)(r)?[r]:[]};const d=t.blockList??DEFAULT_BLOCK_LIST;if((0,node_net_1.isIP)(r)!==0){if(isBlockedAddress(r,d))throw new SsrfBlockedError("blocked-address",s.href,r);return{url:s,hostname:r,addresses:[r]}}const c=t.resolve??defaultResolve;let a;try{a=await c(r)}catch(n){throw new SsrfBlockedError("dns-resolution-failed",s.href,void 0,n)}if(a.length===0)throw new SsrfBlockedError("dns-no-records",s.href);for(const n of a)if(isBlockedAddress(n,d))throw new SsrfBlockedError("blocked-resolved-address",s.href,n);return{url:s,hostname:r,addresses:a}}l(assertFetchableUrl,"assertFetchableUrl"),i(assertFetchableUrl,"assertFetchableUrl");async function safeFetch(e,t={},o={}){const s=o.fetchImpl??globalThis.fetch,r=o.maxRedirects??5;let d=e;for(let c=0;;c++){const{url:a}=await assertFetchableUrl(d,o),n=await s(a,{...t,redirect:"manual"}),f=n.headers.get("location");if(n.status>=300&&n.status<400&&f){if(c>=r)throw new SsrfBlockedError("too-many-redirects",a.href);d=new URL(f,a).href;continue}return n}}l(safeFetch,"safeFetch"),i(safeFetch,"safeFetch");function pinnedLookup(e){const t=e.map(o=>({address:o,family:(0,node_net_1.isIP)(o)===6?6:4}));return(o,s,r)=>{const d=typeof s=="function"?s:r,c=typeof s=="object"&&s!==null&&s.all===!0;if(t.length===0){d(new Error(`SSRF guard: no pinned address for ${o}`));return}c?d(null,t):d(null,t[0].address,t[0].family)}}l(pinnedLookup,"pinnedLookup"),i(pinnedLookup,"pinnedLookup");
@@ -22,4 +22,3 @@ declare function serveExecutor(options: ServeExecutorSchema, context: ExecutorCo
22
22
  baseUrl?: string;
23
23
  }>;
24
24
  export default serveExecutor;
25
- //# sourceMappingURL=serve.impl.d.ts.map