@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.
- package/basic-ssl.d.ts +0 -1
- package/basic-ssl.js +1 -241
- package/build-scope-patterns.d.ts +0 -1
- package/build-scope-patterns.js +1 -40
- package/create-webpack-dev.d.ts +0 -1
- package/create-webpack-dev.js +1 -186
- package/create-webpack-prod.d.ts +0 -1
- package/create-webpack-prod.js +1 -169
- package/executors/build/build.impl.d.ts +0 -1
- package/executors/build/build.impl.js +1 -98
- package/executors/gateway/gateway-errors.d.ts +0 -1
- package/executors/gateway/gateway-errors.js +1 -43
- package/executors/gateway/gateway.impl.d.ts +0 -1
- package/executors/gateway/gateway.impl.js +1 -53
- package/executors/gateway/gateway.lib.d.ts +0 -1
- package/executors/gateway/gateway.lib.js +1 -29
- package/executors/gateway/health-check.d.ts +0 -1
- package/executors/gateway/health-check.js +1 -66
- package/executors/gateway/jwks-verify.d.ts +0 -1
- package/executors/gateway/jwks-verify.js +1 -121
- package/executors/gateway/main.prod-gateway.d.ts +0 -1
- package/executors/gateway/main.prod-gateway.js +2 -573
- package/executors/gateway/main.rollout.d.ts +0 -1
- package/executors/gateway/main.rollout.js +11 -117
- package/executors/gateway/manifest.d.ts +0 -1
- package/executors/gateway/manifest.js +1 -374
- package/executors/gateway/metrics.d.ts +0 -1
- package/executors/gateway/metrics.js +1 -56
- package/executors/gateway/otel-tracing.d.ts +0 -1
- package/executors/gateway/otel-tracing.js +1 -74
- package/executors/gateway/prod-gateway.lib.d.ts +0 -1
- package/executors/gateway/prod-gateway.lib.js +1 -22
- package/executors/gateway/prod-runtime/access-log.d.ts +0 -1
- package/executors/gateway/prod-runtime/access-log.js +1 -24
- package/executors/gateway/prod-runtime/app.d.ts +0 -1
- package/executors/gateway/prod-runtime/app.js +1 -123
- package/executors/gateway/prod-runtime/auth.d.ts +0 -1
- package/executors/gateway/prod-runtime/auth.js +1 -51
- package/executors/gateway/prod-runtime/cors.d.ts +0 -1
- package/executors/gateway/prod-runtime/cors.js +1 -40
- package/executors/gateway/prod-runtime/edge.d.ts +0 -1
- package/executors/gateway/prod-runtime/edge.js +1 -65
- package/executors/gateway/prod-runtime/handler.d.ts +0 -1
- package/executors/gateway/prod-runtime/handler.js +1 -226
- package/executors/gateway/prod-runtime/hooks.d.ts +0 -1
- package/executors/gateway/prod-runtime/hooks.js +1 -42
- package/executors/gateway/prod-runtime/observability.d.ts +0 -1
- package/executors/gateway/prod-runtime/observability.js +1 -125
- package/executors/gateway/prod-runtime/rollout.d.ts +0 -1
- package/executors/gateway/prod-runtime/rollout.js +1 -103
- package/executors/gateway/prod-runtime/routing.d.ts +0 -1
- package/executors/gateway/prod-runtime/routing.js +1 -40
- package/executors/gateway/prod-runtime/server.d.ts +0 -1
- package/executors/gateway/prod-runtime/server.js +1 -79
- package/executors/gateway/prod-runtime/trust.d.ts +0 -1
- package/executors/gateway/prod-runtime/trust.js +1 -32
- package/executors/gateway/prod-runtime/types.d.ts +0 -1
- package/executors/gateway/prod-runtime/types.js +1 -2
- package/executors/gateway/prod-runtime/upgrade.d.ts +0 -1
- package/executors/gateway/prod-runtime/upgrade.js +4 -116
- package/executors/gateway/prod-runtime/upstream.d.ts +0 -1
- package/executors/gateway/prod-runtime/upstream.js +1 -21
- package/executors/gateway/providers-prod.d.ts +0 -1
- package/executors/gateway/providers-prod.js +1 -232
- package/executors/gateway/rate-limit.d.ts +0 -1
- package/executors/gateway/rate-limit.js +2 -75
- package/executors/gateway/registry-source.d.ts +0 -1
- package/executors/gateway/registry-source.js +1 -131
- package/executors/gateway/rollout-ops.d.ts +0 -1
- package/executors/gateway/rollout-ops.js +2 -167
- package/executors/gateway/runtime/auth.d.ts +0 -1
- package/executors/gateway/runtime/auth.js +1 -64
- package/executors/gateway/runtime/chooser.d.ts +0 -1
- package/executors/gateway/runtime/chooser.js +12 -45
- package/executors/gateway/runtime/control.d.ts +0 -1
- package/executors/gateway/runtime/control.js +1 -128
- package/executors/gateway/runtime/dev-auth.d.ts +0 -1
- package/executors/gateway/runtime/dev-auth.js +1 -108
- package/executors/gateway/runtime/discovery.d.ts +0 -1
- package/executors/gateway/runtime/discovery.js +1 -123
- package/executors/gateway/runtime/edge.d.ts +0 -1
- package/executors/gateway/runtime/edge.js +1 -47
- package/executors/gateway/runtime/handler.d.ts +0 -1
- package/executors/gateway/runtime/handler.js +1 -183
- package/executors/gateway/runtime/hooks.d.ts +0 -1
- package/executors/gateway/runtime/hooks.js +1 -55
- package/executors/gateway/runtime/lens-children.d.ts +0 -1
- package/executors/gateway/runtime/lens-children.js +1 -651
- package/executors/gateway/runtime/lifecycle.d.ts +0 -1
- package/executors/gateway/runtime/lifecycle.js +3 -842
- package/executors/gateway/runtime/observability.d.ts +0 -1
- package/executors/gateway/runtime/observability.js +2 -148
- package/executors/gateway/runtime/pod-env.d.ts +0 -1
- package/executors/gateway/runtime/pod-env.js +2 -89
- package/executors/gateway/runtime/proxy.d.ts +0 -1
- package/executors/gateway/runtime/proxy.js +1 -457
- package/executors/gateway/runtime/route-registry.d.ts +0 -1
- package/executors/gateway/runtime/route-registry.js +1 -72
- package/executors/gateway/runtime/scope.d.ts +0 -1
- package/executors/gateway/runtime/scope.js +1 -117
- package/executors/gateway/runtime/server.d.ts +0 -1
- package/executors/gateway/runtime/server.js +3 -487
- package/executors/gateway/runtime/service-keys.d.ts +0 -1
- package/executors/gateway/runtime/service-keys.js +1 -49
- package/executors/gateway/runtime/types.d.ts +0 -1
- package/executors/gateway/runtime/types.js +1 -151
- package/executors/gateway/runtime/upgrade.d.ts +0 -1
- package/executors/gateway/runtime/upgrade.js +1 -71
- package/executors/gateway/runtime/workspace-registry.d.ts +0 -1
- package/executors/gateway/runtime/workspace-registry.js +1 -99
- package/executors/gateway/ssrf-guard.d.ts +0 -1
- package/executors/gateway/ssrf-guard.js +1 -190
- package/executors/serve/serve.impl.d.ts +0 -1
- package/executors/serve/serve.impl.js +1 -280
- package/executors/trust/trust.impl.d.ts +0 -1
- package/executors/trust/trust.impl.js +4 -162
- package/gateway.d.ts +0 -1
- package/gateway.js +1 -35
- package/index.d.ts +0 -1
- package/index.js +1 -16
- package/main.devserver.d.ts +0 -1
- package/main.devserver.js +10 -1117
- package/package.json +4 -3
- package/tsgo-check-plugin.d.ts +0 -1
- package/tsgo-check-plugin.js +4 -364
- package/typecheck-bus.d.ts +0 -1
- package/typecheck-bus.js +4 -256
- package/basic-ssl.d.ts.map +0 -1
- package/build-scope-patterns.d.ts.map +0 -1
- package/create-webpack-dev.d.ts.map +0 -1
- package/create-webpack-prod.d.ts.map +0 -1
- package/executors/build/build.impl.d.ts.map +0 -1
- package/executors/gateway/gateway-errors.d.ts.map +0 -1
- package/executors/gateway/gateway.impl.d.ts.map +0 -1
- package/executors/gateway/gateway.lib.d.ts.map +0 -1
- package/executors/gateway/health-check.d.ts.map +0 -1
- package/executors/gateway/jwks-verify.d.ts.map +0 -1
- package/executors/gateway/main.prod-gateway.d.ts.map +0 -1
- package/executors/gateway/main.rollout.d.ts.map +0 -1
- package/executors/gateway/manifest.d.ts.map +0 -1
- package/executors/gateway/metrics.d.ts.map +0 -1
- package/executors/gateway/otel-tracing.d.ts.map +0 -1
- package/executors/gateway/prod-gateway.lib.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/access-log.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/app.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/auth.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/cors.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/edge.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/handler.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/hooks.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/observability.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/rollout.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/routing.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/server.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/trust.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/types.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/upgrade.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/upstream.d.ts.map +0 -1
- package/executors/gateway/providers-prod.d.ts.map +0 -1
- package/executors/gateway/rate-limit.d.ts.map +0 -1
- package/executors/gateway/registry-source.d.ts.map +0 -1
- package/executors/gateway/rollout-ops.d.ts.map +0 -1
- package/executors/gateway/runtime/auth.d.ts.map +0 -1
- package/executors/gateway/runtime/chooser.d.ts.map +0 -1
- package/executors/gateway/runtime/control.d.ts.map +0 -1
- package/executors/gateway/runtime/dev-auth.d.ts.map +0 -1
- package/executors/gateway/runtime/discovery.d.ts.map +0 -1
- package/executors/gateway/runtime/edge.d.ts.map +0 -1
- package/executors/gateway/runtime/handler.d.ts.map +0 -1
- package/executors/gateway/runtime/hooks.d.ts.map +0 -1
- package/executors/gateway/runtime/lens-children.d.ts.map +0 -1
- package/executors/gateway/runtime/lifecycle.d.ts.map +0 -1
- package/executors/gateway/runtime/observability.d.ts.map +0 -1
- package/executors/gateway/runtime/pod-env.d.ts.map +0 -1
- package/executors/gateway/runtime/proxy.d.ts.map +0 -1
- package/executors/gateway/runtime/route-registry.d.ts.map +0 -1
- package/executors/gateway/runtime/scope.d.ts.map +0 -1
- package/executors/gateway/runtime/server.d.ts.map +0 -1
- package/executors/gateway/runtime/service-keys.d.ts.map +0 -1
- package/executors/gateway/runtime/types.d.ts.map +0 -1
- package/executors/gateway/runtime/upgrade.d.ts.map +0 -1
- package/executors/gateway/runtime/workspace-registry.d.ts.map +0 -1
- package/executors/gateway/ssrf-guard.d.ts.map +0 -1
- package/executors/serve/serve.impl.d.ts.map +0 -1
- package/executors/trust/trust.impl.d.ts.map +0 -1
- package/gateway.d.ts.map +0 -1
- package/index.d.ts.map +0 -1
- package/main.devserver.d.ts.map +0 -1
- package/tsgo-check-plugin.d.ts.map +0 -1
- 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");
|
|
@@ -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);
|
|
@@ -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");
|