@modelcontextprotocol/inspector 2.10.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/clients/cli/build/index.js +9763 -8488
- package/clients/launcher/build/index.js +4 -1
- package/clients/mcpdo/build/{chunk-2E5EZYNU.js → chunk-LBQDT5VV.js} +1070 -161
- package/clients/mcpdo/build/mcp-bin.js +20 -21
- package/clients/mcpdo/build/mcpdod.js +1 -1
- package/clients/tui/build/index.js +1137 -207
- package/clients/web/build/index.js +871 -117
- package/clients/web/dist/assets/index-qtX15-Bz.js +1587 -0
- package/clients/web/dist/assets/{markdown-CZCm46w1.js → markdown-BlYcOtJg.js} +1 -1
- package/clients/web/dist/assets/{prism-light-B0A727v7.js → prism-light-DfUe_Xlh.js} +2 -2
- package/clients/web/dist/index.html +1 -1
- package/package.json +15 -10
- package/scripts/install-clients.mjs +68 -28
- package/clients/web/dist/assets/index-DgDEc2gL.js +0 -1589
|
@@ -159,6 +159,12 @@ var DirectoryReadResultSchema = z.looseObject({
|
|
|
159
159
|
resources: z.array(DirectoryChildSchema),
|
|
160
160
|
nextCursor: z.string().optional()
|
|
161
161
|
});
|
|
162
|
+
var ModernDirectoryReadResultSchema = DirectoryReadResultSchema.extend(
|
|
163
|
+
{
|
|
164
|
+
ttlMs: z.int().min(0),
|
|
165
|
+
cacheScope: z.enum(["public", "private"])
|
|
166
|
+
}
|
|
167
|
+
);
|
|
162
168
|
|
|
163
169
|
// ../../core/mcp/sha256.ts
|
|
164
170
|
var K = new Uint32Array([
|
|
@@ -821,16 +827,30 @@ function buildConfigFromOptions(options) {
|
|
|
821
827
|
const target = options.target ?? [];
|
|
822
828
|
const first = target[0];
|
|
823
829
|
const rest = target.slice(1);
|
|
830
|
+
if (first && options.serverUrl) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`Cannot combine a positional target (${first}) with --server-url (${options.serverUrl}). Pass one or the other.`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
824
835
|
const urlFromTarget = first && (first.startsWith("http://") || first.startsWith("https://")) ? first : null;
|
|
825
836
|
const url = urlFromTarget ?? options.serverUrl ?? null;
|
|
826
837
|
if (url) {
|
|
827
|
-
if (rest.length > 0
|
|
838
|
+
if (rest.length > 0) {
|
|
828
839
|
throw new Error("Arguments cannot be passed to a URL-based MCP server.");
|
|
829
840
|
}
|
|
841
|
+
if (options.transport === "stdio") {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`stdio transport cannot be used with a URL (${url}). Use --transport http or --transport sse, or pass a local command instead.`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
830
846
|
let transportType;
|
|
831
847
|
const t = options.transport === "http" ? "streamable-http" : options.transport;
|
|
832
848
|
if (t === "sse" || t === "streamable-http") {
|
|
833
849
|
transportType = t;
|
|
850
|
+
} else if (t) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
`Invalid transport type: ${t}. Valid types are: sse, http, stdio`
|
|
853
|
+
);
|
|
834
854
|
} else {
|
|
835
855
|
const u = new URL(url);
|
|
836
856
|
if (u.pathname.endsWith("/mcp")) {
|
|
@@ -1047,14 +1067,25 @@ function readInspectorVersionSafe(callerUrl) {
|
|
|
1047
1067
|
// ../../core/node/openUrl.ts
|
|
1048
1068
|
import open from "open";
|
|
1049
1069
|
var OPEN_URL_TIMEOUT_MS = 5e3;
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1070
|
+
var OPENABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1071
|
+
function toOpenableHref(href) {
|
|
1072
|
+
let parsed;
|
|
1073
|
+
try {
|
|
1074
|
+
parsed = new URL(href);
|
|
1075
|
+
} catch {
|
|
1076
|
+
throw new Error("refusing to open an invalid URL");
|
|
1077
|
+
}
|
|
1078
|
+
if (!OPENABLE_PROTOCOLS.has(parsed.protocol)) {
|
|
1079
|
+
throw new Error(`refusing to open a non-http(s) URL (${parsed.protocol})`);
|
|
1080
|
+
}
|
|
1081
|
+
return parsed.href;
|
|
1082
|
+
}
|
|
1083
|
+
function absorbLateErrors(child) {
|
|
1084
|
+
child.on("error", () => {
|
|
1054
1085
|
});
|
|
1055
1086
|
}
|
|
1056
1087
|
async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
|
|
1057
|
-
const href = typeof url === "string" ? url : url.href;
|
|
1088
|
+
const href = toOpenableHref(typeof url === "string" ? url : url.href);
|
|
1058
1089
|
let timer;
|
|
1059
1090
|
const timeout = new Promise((_, reject) => {
|
|
1060
1091
|
timer = setTimeout(
|
|
@@ -1063,8 +1094,7 @@ async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
|
|
|
1063
1094
|
);
|
|
1064
1095
|
});
|
|
1065
1096
|
try {
|
|
1066
|
-
|
|
1067
|
-
await Promise.race([opened, timeout]);
|
|
1097
|
+
await Promise.race([open(href).then(absorbLateErrors), timeout]);
|
|
1068
1098
|
} finally {
|
|
1069
1099
|
clearTimeout(timer);
|
|
1070
1100
|
}
|
|
@@ -1575,6 +1605,153 @@ function resolveAppOriginPublicOrigin(raw, allowedOrigins, sandboxUrl) {
|
|
|
1575
1605
|
return url.origin;
|
|
1576
1606
|
}
|
|
1577
1607
|
|
|
1608
|
+
// server/tls.ts
|
|
1609
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1610
|
+
import { createSecureContext } from "tls";
|
|
1611
|
+
var TLS_KEY_PATH_ENV = "INSPECTOR_TLS_KEY_PATH";
|
|
1612
|
+
var TLS_CERT_PATH_ENV = "INSPECTOR_TLS_CERT_PATH";
|
|
1613
|
+
function webScheme(tls) {
|
|
1614
|
+
return tls ? "https" : "http";
|
|
1615
|
+
}
|
|
1616
|
+
function readPem(env, path6) {
|
|
1617
|
+
try {
|
|
1618
|
+
return readFileSync4(path6, "utf-8");
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
const { code } = err;
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`Cannot read ${env}="${path6}" (${code}): check that the file exists and is readable.`,
|
|
1623
|
+
{ cause: err }
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
function resolveTlsConfig(env = process.env) {
|
|
1628
|
+
const keyPath = env[TLS_KEY_PATH_ENV]?.trim();
|
|
1629
|
+
const certPath = env[TLS_CERT_PATH_ENV]?.trim();
|
|
1630
|
+
if (!keyPath && !certPath) return void 0;
|
|
1631
|
+
if (!keyPath || !certPath) {
|
|
1632
|
+
const missing = keyPath ? TLS_CERT_PATH_ENV : TLS_KEY_PATH_ENV;
|
|
1633
|
+
throw new Error(
|
|
1634
|
+
`TLS needs both ${TLS_KEY_PATH_ENV} and ${TLS_CERT_PATH_ENV}; ${missing} is not set. Set both to enable https, or neither for plain http.`
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const key = readPem(TLS_KEY_PATH_ENV, keyPath);
|
|
1638
|
+
const cert = readPem(TLS_CERT_PATH_ENV, certPath);
|
|
1639
|
+
try {
|
|
1640
|
+
if (!key.trim() || !cert.trim()) throw new Error("empty PEM file");
|
|
1641
|
+
createSecureContext({ key, cert });
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
const reason = err.message;
|
|
1644
|
+
throw new Error(
|
|
1645
|
+
`${TLS_KEY_PATH_ENV}="${keyPath}" and ${TLS_CERT_PATH_ENV}="${certPath}" are not a usable PEM key/certificate pair (${reason}).`,
|
|
1646
|
+
{ cause: err }
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
return { key, cert };
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// server/host-allow-list.ts
|
|
1653
|
+
import { isIPv4, isIPv6 as isIPv62 } from "net";
|
|
1654
|
+
import { domainToASCII } from "url";
|
|
1655
|
+
var ALLOWED_HOSTS_ENV = "MCP_INSPECTOR_ALLOWED_HOSTS";
|
|
1656
|
+
var HOSTNAME_RE = /^[a-z0-9._-]+$/;
|
|
1657
|
+
function normalizeHostname(hostname) {
|
|
1658
|
+
return hostname.trim().toLowerCase().replace(/\.$/, "");
|
|
1659
|
+
}
|
|
1660
|
+
function parseHostHeader(value) {
|
|
1661
|
+
const raw = value.trim().toLowerCase();
|
|
1662
|
+
let hostname;
|
|
1663
|
+
let port;
|
|
1664
|
+
if (raw.startsWith("[")) {
|
|
1665
|
+
const end = raw.indexOf("]");
|
|
1666
|
+
if (end < 0) return null;
|
|
1667
|
+
hostname = raw.slice(0, end + 1);
|
|
1668
|
+
if (!isIPv62(hostname.slice(1, -1))) return null;
|
|
1669
|
+
const rest = raw.slice(end + 1);
|
|
1670
|
+
if (rest !== "") {
|
|
1671
|
+
if (!rest.startsWith(":")) return null;
|
|
1672
|
+
port = rest.slice(1);
|
|
1673
|
+
}
|
|
1674
|
+
} else {
|
|
1675
|
+
const colon = raw.indexOf(":");
|
|
1676
|
+
hostname = colon < 0 ? raw : raw.slice(0, colon);
|
|
1677
|
+
port = colon < 0 ? void 0 : raw.slice(colon + 1);
|
|
1678
|
+
}
|
|
1679
|
+
if (port !== void 0) {
|
|
1680
|
+
if (!/^\d{1,5}$/.test(port) || Number(port) > 65535) return null;
|
|
1681
|
+
}
|
|
1682
|
+
if (hostname.startsWith("[")) return hostname;
|
|
1683
|
+
const name = normalizeHostname(hostname);
|
|
1684
|
+
return HOSTNAME_RE.test(name) ? name : null;
|
|
1685
|
+
}
|
|
1686
|
+
function parseAllowedHostsEnv(value) {
|
|
1687
|
+
if (!value) return [];
|
|
1688
|
+
const out = [];
|
|
1689
|
+
for (const entry of value.split(",")) {
|
|
1690
|
+
const trimmed = entry.trim();
|
|
1691
|
+
if (trimmed === "") continue;
|
|
1692
|
+
const domain = trimmed.startsWith(".") ? "." : "";
|
|
1693
|
+
const ascii = /[\s/:*@]/.test(trimmed) ? "" : domainToASCII(normalizeHostname(trimmed.slice(domain.length)));
|
|
1694
|
+
if (!HOSTNAME_RE.test(ascii)) {
|
|
1695
|
+
console.warn(
|
|
1696
|
+
`Ignoring invalid ${ALLOWED_HOSTS_ENV} entry: ${trimmed} (list bare hostnames, e.g. inspector.example.com or .example.com)`
|
|
1697
|
+
);
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
out.push(domain + ascii);
|
|
1701
|
+
}
|
|
1702
|
+
return out;
|
|
1703
|
+
}
|
|
1704
|
+
function configuredAllowedHosts(options) {
|
|
1705
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
1706
|
+
if (!isAllInterfacesHost(options.bindHost)) {
|
|
1707
|
+
hosts.add(normalizeHostname(canonicalUrlHost(options.bindHost)));
|
|
1708
|
+
}
|
|
1709
|
+
for (const origin of options.allowedOrigins) {
|
|
1710
|
+
try {
|
|
1711
|
+
const { hostname } = new URL(origin);
|
|
1712
|
+
if (hostname !== "") hosts.add(normalizeHostname(hostname));
|
|
1713
|
+
} catch {
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
for (const extra of options.extraHosts ?? []) {
|
|
1717
|
+
hosts.add(normalizeHostname(extra));
|
|
1718
|
+
}
|
|
1719
|
+
return [...hosts];
|
|
1720
|
+
}
|
|
1721
|
+
function createHostAllowList(options) {
|
|
1722
|
+
const configured = configuredAllowedHosts(options);
|
|
1723
|
+
return (hostHeader) => {
|
|
1724
|
+
if (hostHeader === void 0) return false;
|
|
1725
|
+
const hostname = parseHostHeader(hostHeader);
|
|
1726
|
+
if (hostname === null) return false;
|
|
1727
|
+
if (hostname.startsWith("[")) return true;
|
|
1728
|
+
if (isIPv4(hostname)) return true;
|
|
1729
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
1730
|
+
return true;
|
|
1731
|
+
}
|
|
1732
|
+
for (const allowed of configured) {
|
|
1733
|
+
if (allowed === hostname) return true;
|
|
1734
|
+
if (allowed.startsWith(".") && (allowed.slice(1) === hostname || hostname.endsWith(allowed))) {
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return false;
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function hostAllowListMiddleware(options) {
|
|
1742
|
+
const isAllowed = createHostAllowList(options);
|
|
1743
|
+
return async (c, next) => {
|
|
1744
|
+
const host = c.req.header("host");
|
|
1745
|
+
if (!isAllowed(host)) {
|
|
1746
|
+
return c.text(
|
|
1747
|
+
`Forbidden: Host "${host ?? ""}" is not allowed. To serve the Inspector under this name, add it to ${ALLOWED_HOSTS_ENV} (or to ALLOWED_ORIGINS).`,
|
|
1748
|
+
403
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
await next();
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1578
1755
|
// server/web-server-config.ts
|
|
1579
1756
|
var inspectorVersion = readInspectorVersionSafe(import.meta.url);
|
|
1580
1757
|
function defaultEnvironmentFromProcess(extra) {
|
|
@@ -1656,7 +1833,7 @@ function transportDefaults(config) {
|
|
|
1656
1833
|
function printServerBanner(config, actualPort, resolvedToken, sandboxUrl, secretStorage) {
|
|
1657
1834
|
const h = canonicalUrlHost(config.hostname);
|
|
1658
1835
|
const bannerHost = isAllInterfacesHost(h) ? "localhost" : h;
|
|
1659
|
-
const baseUrl =
|
|
1836
|
+
const baseUrl = webOrigin(webScheme(config.tls), bannerHost, actualPort);
|
|
1660
1837
|
const url = config.dangerouslyOmitAuth || !resolvedToken ? baseUrl : `${baseUrl}?${API_SERVER_ENV_VARS.AUTH_TOKEN}=${resolvedToken}`;
|
|
1661
1838
|
console.log(`
|
|
1662
1839
|
MCP Inspector Web is up and running at:
|
|
@@ -1681,35 +1858,42 @@ MCP Inspector Web is up and running at:
|
|
|
1681
1858
|
}
|
|
1682
1859
|
return url;
|
|
1683
1860
|
}
|
|
1861
|
+
function portInUseMessage(config) {
|
|
1862
|
+
return `MCP Inspector PORT IS IN USE at ${webScheme(config.tls)}://${formatHostForUrl(config.hostname)}:${config.port}`;
|
|
1863
|
+
}
|
|
1684
1864
|
function openBrowser(url) {
|
|
1685
1865
|
openUrl(url).catch((err) => {
|
|
1686
|
-
|
|
1866
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1867
|
+
console.warn(
|
|
1868
|
+
`Could not open a browser automatically (${reason}). Open the URL above manually.`
|
|
1869
|
+
);
|
|
1687
1870
|
});
|
|
1688
1871
|
}
|
|
1689
1872
|
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1690
|
-
function
|
|
1691
|
-
|
|
1873
|
+
function webOrigin(scheme, host, port) {
|
|
1874
|
+
const defaultPort = scheme === "https" ? 443 : 80;
|
|
1875
|
+
return port === defaultPort ? `${scheme}://${host}` : `${scheme}://${host}:${port}`;
|
|
1692
1876
|
}
|
|
1693
|
-
function loopbackOrigins(port) {
|
|
1877
|
+
function loopbackOrigins(scheme, port) {
|
|
1694
1878
|
return [
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1879
|
+
webOrigin(scheme, "localhost", port),
|
|
1880
|
+
webOrigin(scheme, "127.0.0.1", port),
|
|
1881
|
+
webOrigin(scheme, "[::1]", port)
|
|
1698
1882
|
];
|
|
1699
1883
|
}
|
|
1700
|
-
function defaultAllowedOrigins(hostname, port) {
|
|
1884
|
+
function defaultAllowedOrigins(hostname, port, scheme = "http") {
|
|
1701
1885
|
const h = canonicalUrlHost(hostname);
|
|
1702
1886
|
if (LOOPBACK_HOSTNAMES.has(h)) {
|
|
1703
|
-
return loopbackOrigins(port);
|
|
1887
|
+
return loopbackOrigins(scheme, port);
|
|
1704
1888
|
}
|
|
1705
1889
|
if (isAllInterfacesHost(h)) {
|
|
1706
1890
|
return [
|
|
1707
|
-
...loopbackOrigins(port),
|
|
1708
|
-
|
|
1709
|
-
|
|
1891
|
+
...loopbackOrigins(scheme, port),
|
|
1892
|
+
webOrigin(scheme, "0.0.0.0", port),
|
|
1893
|
+
webOrigin(scheme, "[::]", port)
|
|
1710
1894
|
];
|
|
1711
1895
|
}
|
|
1712
|
-
return [
|
|
1896
|
+
return [webOrigin(scheme, h, port)];
|
|
1713
1897
|
}
|
|
1714
1898
|
function buildWebServerConfig(options = {}) {
|
|
1715
1899
|
const {
|
|
@@ -1726,6 +1910,7 @@ function buildWebServerConfig(options = {}) {
|
|
|
1726
1910
|
);
|
|
1727
1911
|
}
|
|
1728
1912
|
const hostname = resolveBindHostname();
|
|
1913
|
+
const tls = resolveTlsConfig();
|
|
1729
1914
|
const dangerouslyOmitAuth = isEnvFlagEnabled(
|
|
1730
1915
|
process.env.DANGEROUSLY_OMIT_AUTH
|
|
1731
1916
|
);
|
|
@@ -1761,7 +1946,7 @@ function buildWebServerConfig(options = {}) {
|
|
|
1761
1946
|
return null;
|
|
1762
1947
|
}
|
|
1763
1948
|
}).filter((o) => o !== null);
|
|
1764
|
-
const allowedOrigins = configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port);
|
|
1949
|
+
const allowedOrigins = configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port, webScheme(tls));
|
|
1765
1950
|
const sandboxPublicUrl = resolveSandboxPublicUrl(
|
|
1766
1951
|
process.env[SANDBOX_FULL_ADDRESS_ENV],
|
|
1767
1952
|
allowedOrigins
|
|
@@ -1782,13 +1967,15 @@ function buildWebServerConfig(options = {}) {
|
|
|
1782
1967
|
initialServers,
|
|
1783
1968
|
storageDir: process.env.MCP_STORAGE_DIR,
|
|
1784
1969
|
allowedOrigins,
|
|
1970
|
+
allowedHosts: parseAllowedHostsEnv(process.env[ALLOWED_HOSTS_ENV]),
|
|
1785
1971
|
sandboxPort,
|
|
1786
1972
|
sandboxHost: hostname,
|
|
1787
1973
|
appOriginPort,
|
|
1788
1974
|
sandboxPublicUrl,
|
|
1789
1975
|
appOriginPublicOrigin,
|
|
1790
1976
|
logger,
|
|
1791
|
-
autoOpen: resolveAutoOpen()
|
|
1977
|
+
autoOpen: resolveAutoOpen(),
|
|
1978
|
+
...tls ? { tls } : {}
|
|
1792
1979
|
};
|
|
1793
1980
|
}
|
|
1794
1981
|
function resolveAutoOpen() {
|
|
@@ -1806,7 +1993,7 @@ import react from "@vitejs/plugin-react";
|
|
|
1806
1993
|
|
|
1807
1994
|
// ../../core/mcp/remote/node/server.ts
|
|
1808
1995
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
1809
|
-
import { existsSync as existsSync3, readFileSync as
|
|
1996
|
+
import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
|
|
1810
1997
|
import { stat as fsStat } from "fs/promises";
|
|
1811
1998
|
import { homedir } from "os";
|
|
1812
1999
|
import { Hono } from "hono";
|
|
@@ -2291,7 +2478,10 @@ function parseAuthChallengeFromResponse(response, context) {
|
|
|
2291
2478
|
if (status !== 401 && status !== 403) {
|
|
2292
2479
|
return void 0;
|
|
2293
2480
|
}
|
|
2294
|
-
const wwwAuthenticate = response.headers.get("WWW-Authenticate")
|
|
2481
|
+
const wwwAuthenticate = response.headers.get("WWW-Authenticate") || void 0;
|
|
2482
|
+
if (status === 403 && !wwwAuthenticate) {
|
|
2483
|
+
return void 0;
|
|
2484
|
+
}
|
|
2295
2485
|
const bearer = wwwAuthenticate ? parseWwwAuthenticateBearer(wwwAuthenticate) : {};
|
|
2296
2486
|
const requiredScopes = parseScopeString(bearer.scope);
|
|
2297
2487
|
return {
|
|
@@ -2620,9 +2810,96 @@ function createTransportNode(config, options = {}) {
|
|
|
2620
2810
|
}
|
|
2621
2811
|
}
|
|
2622
2812
|
|
|
2813
|
+
// ../../core/mcp/remote/node/cappedBody.ts
|
|
2814
|
+
async function readCappedText(body, maxBytes) {
|
|
2815
|
+
const reader = body.getReader();
|
|
2816
|
+
const decoder = new TextDecoder();
|
|
2817
|
+
let text = "";
|
|
2818
|
+
let bytes = 0;
|
|
2819
|
+
for (; ; ) {
|
|
2820
|
+
const next = await reader.read();
|
|
2821
|
+
if (next.done) return { kind: "complete", text: text + decoder.decode() };
|
|
2822
|
+
bytes += next.value.byteLength;
|
|
2823
|
+
if (bytes > maxBytes) {
|
|
2824
|
+
void reader.cancel().catch(() => {
|
|
2825
|
+
});
|
|
2826
|
+
return { kind: "overflow" };
|
|
2827
|
+
}
|
|
2828
|
+
text += decoder.decode(next.value, { stream: true });
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
function declaresOversizeBody(contentLength, maxBytes) {
|
|
2832
|
+
if (contentLength === null || !/^\s*\d+\s*$/.test(contentLength)) {
|
|
2833
|
+
return false;
|
|
2834
|
+
}
|
|
2835
|
+
return Number(contentLength) > maxBytes;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2623
2838
|
// ../../core/auth/requestTimeout.ts
|
|
2624
2839
|
var OAUTH_TIMEOUT_WIRE_CODE = "oauth_request_timeout";
|
|
2625
2840
|
|
|
2841
|
+
// ../../core/mcp/rateLimit.ts
|
|
2842
|
+
import {
|
|
2843
|
+
SdkHttpError
|
|
2844
|
+
} from "@modelcontextprotocol/client";
|
|
2845
|
+
|
|
2846
|
+
// ../../core/mcp/connectionDiagnostics.ts
|
|
2847
|
+
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
|
|
2848
|
+
|
|
2849
|
+
// ../../core/mcp/rateLimit.ts
|
|
2850
|
+
var HTTP_TOO_MANY_REQUESTS = 429;
|
|
2851
|
+
function requestIdsOf(body) {
|
|
2852
|
+
if (body === void 0) return [];
|
|
2853
|
+
let parsed;
|
|
2854
|
+
try {
|
|
2855
|
+
parsed = JSON.parse(body);
|
|
2856
|
+
} catch {
|
|
2857
|
+
return [];
|
|
2858
|
+
}
|
|
2859
|
+
const messages = Array.isArray(parsed) ? parsed : [parsed];
|
|
2860
|
+
const ids = [];
|
|
2861
|
+
for (const message of messages) {
|
|
2862
|
+
if (typeof message !== "object" || message === null) continue;
|
|
2863
|
+
if (!("method" in message) || !("id" in message)) continue;
|
|
2864
|
+
const { id } = message;
|
|
2865
|
+
if (typeof id === "string" || typeof id === "number") ids.push(id);
|
|
2866
|
+
}
|
|
2867
|
+
return ids;
|
|
2868
|
+
}
|
|
2869
|
+
var MAX_UNCLAIMED_RATE_LIMITS = 100;
|
|
2870
|
+
var RateLimitLedger = class {
|
|
2871
|
+
// `undefined` is a real value here: a 429 that carried no header.
|
|
2872
|
+
byRequestId = /* @__PURE__ */ new Map();
|
|
2873
|
+
/** File a tracked fetch, if it was a 429. Anything else is ignored. */
|
|
2874
|
+
record(entry) {
|
|
2875
|
+
if (entry.responseStatus !== HTTP_TOO_MANY_REQUESTS) return;
|
|
2876
|
+
const retryAfter = findHeader(entry.responseHeaders, "retry-after");
|
|
2877
|
+
for (const id of requestIdsOf(entry.requestBody)) {
|
|
2878
|
+
this.byRequestId.delete(id);
|
|
2879
|
+
this.byRequestId.set(id, retryAfter);
|
|
2880
|
+
if (this.byRequestId.size > MAX_UNCLAIMED_RATE_LIMITS) {
|
|
2881
|
+
const [oldest] = this.byRequestId.keys();
|
|
2882
|
+
if (oldest !== void 0) this.byRequestId.delete(oldest);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
/** The `Retry-After` filed for `id`, removed as it is read. */
|
|
2887
|
+
take(id) {
|
|
2888
|
+
const retryAfter = this.byRequestId.get(id);
|
|
2889
|
+
this.byRequestId.delete(id);
|
|
2890
|
+
return retryAfter;
|
|
2891
|
+
}
|
|
2892
|
+
clear() {
|
|
2893
|
+
this.byRequestId.clear();
|
|
2894
|
+
}
|
|
2895
|
+
};
|
|
2896
|
+
function isRateLimitError(err) {
|
|
2897
|
+
return SdkHttpError.isInstance(err) && err.status === HTTP_TOO_MANY_REQUESTS;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
// ../../core/mcp/remote/node/server.ts
|
|
2901
|
+
import { SdkHttpError as SdkHttpError2 } from "@modelcontextprotocol/client";
|
|
2902
|
+
|
|
2626
2903
|
// ../../core/auth/oauth-persist.ts
|
|
2627
2904
|
import { OAuthTokensSchema } from "@modelcontextprotocol/core";
|
|
2628
2905
|
var OAUTH_PERSIST_STORE_ID = "oauth";
|
|
@@ -3250,7 +3527,7 @@ import * as path4 from "path";
|
|
|
3250
3527
|
|
|
3251
3528
|
// ../../core/auth/node/file-secret-store.ts
|
|
3252
3529
|
import * as crypto2 from "crypto";
|
|
3253
|
-
import { readFileSync as
|
|
3530
|
+
import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
3254
3531
|
import * as fs3 from "fs/promises";
|
|
3255
3532
|
import * as path3 from "path";
|
|
3256
3533
|
var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve10, reject) => {
|
|
@@ -3303,7 +3580,7 @@ function resolveSecretPassphrase(env = process.env, secretFilePath) {
|
|
|
3303
3580
|
}
|
|
3304
3581
|
let contents;
|
|
3305
3582
|
try {
|
|
3306
|
-
contents =
|
|
3583
|
+
contents = readFileSync5(resolved2, "utf-8");
|
|
3307
3584
|
} catch (err) {
|
|
3308
3585
|
return {
|
|
3309
3586
|
// `String(err)` rather than `.message`: `readFileSync` only ever throws
|
|
@@ -4333,7 +4610,7 @@ async function resolveConcreteSecretStore(store) {
|
|
|
4333
4610
|
import { resolve as resolve6 } from "path";
|
|
4334
4611
|
|
|
4335
4612
|
// ../../core/auth/node/oauth-secrets.ts
|
|
4336
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
4613
|
+
import { createHmac, randomUUID as randomUUID2 } from "crypto";
|
|
4337
4614
|
import { OAuthTokensSchema as OAuthTokensSchema2 } from "@modelcontextprotocol/core";
|
|
4338
4615
|
var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
|
|
4339
4616
|
var warnedPolicyValues = /* @__PURE__ */ new Set();
|
|
@@ -4366,6 +4643,29 @@ var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
|
|
|
4366
4643
|
var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
|
|
4367
4644
|
var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
|
|
4368
4645
|
var IDP_SESSION_FIELD = "idp-session";
|
|
4646
|
+
var clientCredentialStampField = (field) => `${field}#owner`;
|
|
4647
|
+
var credentialMac = (clientId, value) => createHmac("sha256", value).update(clientId).digest("hex");
|
|
4648
|
+
function clientCredentialStamp(clientId, value) {
|
|
4649
|
+
return JSON.stringify({
|
|
4650
|
+
client_id: clientId,
|
|
4651
|
+
mac: credentialMac(clientId, value)
|
|
4652
|
+
});
|
|
4653
|
+
}
|
|
4654
|
+
function stampedOwner(stamp, value) {
|
|
4655
|
+
if (stamp === void 0) return void 0;
|
|
4656
|
+
let parsed;
|
|
4657
|
+
try {
|
|
4658
|
+
parsed = JSON.parse(stamp);
|
|
4659
|
+
} catch {
|
|
4660
|
+
return void 0;
|
|
4661
|
+
}
|
|
4662
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
4663
|
+
const { client_id: clientId, mac } = parsed;
|
|
4664
|
+
if (typeof clientId !== "string" || mac !== credentialMac(clientId, value)) {
|
|
4665
|
+
return void 0;
|
|
4666
|
+
}
|
|
4667
|
+
return clientId;
|
|
4668
|
+
}
|
|
4369
4669
|
function applyTokensPolicy(tokens, policy) {
|
|
4370
4670
|
if (policy === "none") return void 0;
|
|
4371
4671
|
if (policy === "access" && tokens.refresh_token !== void 0) {
|
|
@@ -4394,12 +4694,18 @@ function splitClientInformation(info, fields, secrets) {
|
|
|
4394
4694
|
if (client_secret === void 0 && registration_access_token === void 0) {
|
|
4395
4695
|
return info;
|
|
4396
4696
|
}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4697
|
+
const put = (field, value) => {
|
|
4698
|
+
if (value === void 0) return;
|
|
4699
|
+
secrets[field] = value;
|
|
4700
|
+
if (fields.stamped && typeof info.client_id === "string") {
|
|
4701
|
+
secrets[clientCredentialStampField(field)] = clientCredentialStamp(
|
|
4702
|
+
info.client_id,
|
|
4703
|
+
value
|
|
4704
|
+
);
|
|
4705
|
+
}
|
|
4706
|
+
};
|
|
4707
|
+
put(fields.clientSecret, client_secret);
|
|
4708
|
+
put(fields.registrationToken, registration_access_token);
|
|
4403
4709
|
return publicInfo;
|
|
4404
4710
|
}
|
|
4405
4711
|
function joinClientInformation(info, fields, secrets) {
|
|
@@ -4418,15 +4724,18 @@ function joinClientInformation(info, fields, secrets) {
|
|
|
4418
4724
|
}
|
|
4419
4725
|
var issuerClientInfoFields = (issuer) => ({
|
|
4420
4726
|
clientSecret: issuerClientSecretField(issuer),
|
|
4421
|
-
registrationToken: issuerRegistrationTokenField(issuer)
|
|
4727
|
+
registrationToken: issuerRegistrationTokenField(issuer),
|
|
4728
|
+
stamped: true
|
|
4422
4729
|
});
|
|
4423
4730
|
var LEGACY_CLIENT_INFO_FIELDS = {
|
|
4424
4731
|
clientSecret: LEGACY_CLIENT_SECRET_FIELD,
|
|
4425
|
-
registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
|
|
4732
|
+
registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD,
|
|
4733
|
+
stamped: true
|
|
4426
4734
|
};
|
|
4427
4735
|
var PREREG_CLIENT_INFO_FIELDS = {
|
|
4428
4736
|
clientSecret: PREREG_CLIENT_SECRET_FIELD,
|
|
4429
|
-
registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
|
|
4737
|
+
registrationToken: PREREG_REGISTRATION_TOKEN_FIELD,
|
|
4738
|
+
stamped: false
|
|
4430
4739
|
};
|
|
4431
4740
|
function splitServerOAuthState(state, policy) {
|
|
4432
4741
|
const secrets = {};
|
|
@@ -4576,20 +4885,54 @@ function joinIdpSession(residue, secrets) {
|
|
|
4576
4885
|
return stored ? { ...residue, ...stored } : { ...residue };
|
|
4577
4886
|
}
|
|
4578
4887
|
function serverSecretFields(state) {
|
|
4888
|
+
const stamped = (field) => [
|
|
4889
|
+
field,
|
|
4890
|
+
clientCredentialStampField(field)
|
|
4891
|
+
];
|
|
4579
4892
|
const fields = /* @__PURE__ */ new Set([
|
|
4580
4893
|
LEGACY_TOKENS_FIELD,
|
|
4581
|
-
LEGACY_CLIENT_SECRET_FIELD,
|
|
4582
|
-
LEGACY_REGISTRATION_TOKEN_FIELD,
|
|
4894
|
+
...stamped(LEGACY_CLIENT_SECRET_FIELD),
|
|
4895
|
+
...stamped(LEGACY_REGISTRATION_TOKEN_FIELD),
|
|
4583
4896
|
PREREG_CLIENT_SECRET_FIELD,
|
|
4584
4897
|
PREREG_REGISTRATION_TOKEN_FIELD
|
|
4585
4898
|
]);
|
|
4586
4899
|
for (const issuer of Object.keys(state?.byIssuer ?? {})) {
|
|
4587
4900
|
fields.add(issuerTokensField(issuer));
|
|
4588
|
-
|
|
4589
|
-
|
|
4901
|
+
for (const field of stamped(issuerClientSecretField(issuer))) {
|
|
4902
|
+
fields.add(field);
|
|
4903
|
+
}
|
|
4904
|
+
for (const field of stamped(issuerRegistrationTokenField(issuer))) {
|
|
4905
|
+
fields.add(field);
|
|
4906
|
+
}
|
|
4590
4907
|
}
|
|
4591
4908
|
return [...fields];
|
|
4592
4909
|
}
|
|
4910
|
+
function clientCredentialOwners(state) {
|
|
4911
|
+
const owners = /* @__PURE__ */ new Map();
|
|
4912
|
+
const add = (info, fields) => {
|
|
4913
|
+
if (typeof info?.client_id !== "string") return;
|
|
4914
|
+
owners.set(fields.clientSecret, info.client_id);
|
|
4915
|
+
owners.set(fields.registrationToken, info.client_id);
|
|
4916
|
+
};
|
|
4917
|
+
add(state.clientInformation, LEGACY_CLIENT_INFO_FIELDS);
|
|
4918
|
+
for (const [issuer, slot] of Object.entries(state.byIssuer ?? {})) {
|
|
4919
|
+
add(slot.clientInformation, issuerClientInfoFields(issuer));
|
|
4920
|
+
}
|
|
4921
|
+
return owners;
|
|
4922
|
+
}
|
|
4923
|
+
function restampClientCredentials(state, secrets) {
|
|
4924
|
+
const out = { ...secrets };
|
|
4925
|
+
for (const [field, clientId] of clientCredentialOwners(state)) {
|
|
4926
|
+
const stampField = clientCredentialStampField(field);
|
|
4927
|
+
const value = Object.hasOwn(out, field) ? out[field] : void 0;
|
|
4928
|
+
if (value === void 0) {
|
|
4929
|
+
delete out[stampField];
|
|
4930
|
+
} else if (stampedOwner(out[stampField], value) === void 0) {
|
|
4931
|
+
setOwnEntry(out, stampField, clientCredentialStamp(clientId, value));
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
return out;
|
|
4935
|
+
}
|
|
4593
4936
|
function clientInfoHasPlaintext(info) {
|
|
4594
4937
|
return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
|
|
4595
4938
|
}
|
|
@@ -4630,6 +4973,44 @@ function coveredLocations(location, byLocation) {
|
|
|
4630
4973
|
([recorded]) => recorded === location || location === KEYRING_LOCATION && recorded.startsWith(FILE_LOCATION_PREFIX)
|
|
4631
4974
|
).map(([recorded, keys]) => ({ location: recorded, keys }));
|
|
4632
4975
|
}
|
|
4976
|
+
var emptyKeys = () => ({
|
|
4977
|
+
servers: /* @__PURE__ */ new Set(),
|
|
4978
|
+
idpSessions: /* @__PURE__ */ new Set(),
|
|
4979
|
+
owners: /* @__PURE__ */ new Map()
|
|
4980
|
+
});
|
|
4981
|
+
function candidateFields(location, byLocation) {
|
|
4982
|
+
const fields = /* @__PURE__ */ new Map();
|
|
4983
|
+
for (const { keys } of coveredLocations(location, byLocation)) {
|
|
4984
|
+
for (const [url, owned] of keys.owners) {
|
|
4985
|
+
const kept = fields.get(url) ?? /* @__PURE__ */ new Set();
|
|
4986
|
+
for (const field of owned.keys()) kept.add(field);
|
|
4987
|
+
fields.set(url, kept);
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
return fields;
|
|
4991
|
+
}
|
|
4992
|
+
function addMissingOwners(target, source) {
|
|
4993
|
+
for (const [url, fields] of source) {
|
|
4994
|
+
const kept = target.get(url) ?? /* @__PURE__ */ new Map();
|
|
4995
|
+
for (const [field, clientId] of fields) {
|
|
4996
|
+
if (!kept.has(field)) kept.set(field, clientId);
|
|
4997
|
+
}
|
|
4998
|
+
if (kept.size > 0) target.set(url, kept);
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
function mergeOwners(target, source) {
|
|
5002
|
+
let changed = false;
|
|
5003
|
+
for (const [url, fields] of source) {
|
|
5004
|
+
const kept = target.get(url) ?? /* @__PURE__ */ new Map();
|
|
5005
|
+
for (const [field, clientId] of fields) {
|
|
5006
|
+
if (kept.get(field) === clientId) continue;
|
|
5007
|
+
kept.set(field, clientId);
|
|
5008
|
+
changed = true;
|
|
5009
|
+
}
|
|
5010
|
+
if (kept.size > 0) target.set(url, kept);
|
|
5011
|
+
}
|
|
5012
|
+
return changed;
|
|
5013
|
+
}
|
|
4633
5014
|
var warnedLedgerFailures = /* @__PURE__ */ new Set();
|
|
4634
5015
|
function warnLedgerFailure(what, error) {
|
|
4635
5016
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -4643,6 +5024,19 @@ function warnLedgerFailure(what, error) {
|
|
|
4643
5024
|
function isRecord3(value) {
|
|
4644
5025
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4645
5026
|
}
|
|
5027
|
+
function parseOwners(value) {
|
|
5028
|
+
const owners = /* @__PURE__ */ new Map();
|
|
5029
|
+
if (!isRecord3(value)) return owners;
|
|
5030
|
+
for (const [url, fields] of Object.entries(value)) {
|
|
5031
|
+
if (!isRecord3(fields)) continue;
|
|
5032
|
+
const byField = /* @__PURE__ */ new Map();
|
|
5033
|
+
for (const [field, clientId] of Object.entries(fields)) {
|
|
5034
|
+
if (typeof clientId === "string") byField.set(field, clientId);
|
|
5035
|
+
}
|
|
5036
|
+
if (byField.size > 0) owners.set(url, byField);
|
|
5037
|
+
}
|
|
5038
|
+
return owners;
|
|
5039
|
+
}
|
|
4646
5040
|
function stringsOf(value) {
|
|
4647
5041
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
4648
5042
|
}
|
|
@@ -4664,7 +5058,9 @@ function parseLedger(raw) {
|
|
|
4664
5058
|
if (!isRecord3(keys)) continue;
|
|
4665
5059
|
byLocation.set(location, {
|
|
4666
5060
|
servers: new Set(stringsOf(keys.servers)),
|
|
4667
|
-
idpSessions: new Set(stringsOf(keys.idpSessions))
|
|
5061
|
+
idpSessions: new Set(stringsOf(keys.idpSessions)),
|
|
5062
|
+
owners: parseOwners(keys.owners),
|
|
5063
|
+
...isValidSecretsNamespace(keys.owedTo) && { owedTo: keys.owedTo }
|
|
4668
5064
|
});
|
|
4669
5065
|
}
|
|
4670
5066
|
if (byLocation.size > 0) ledger.set(namespace, byLocation);
|
|
@@ -4678,7 +5074,19 @@ function serializeLedger(ledger) {
|
|
|
4678
5074
|
Object.fromEntries(
|
|
4679
5075
|
[...byLocation].map(([location, keys]) => [
|
|
4680
5076
|
location,
|
|
4681
|
-
{
|
|
5077
|
+
{
|
|
5078
|
+
servers: [...keys.servers],
|
|
5079
|
+
idpSessions: [...keys.idpSessions],
|
|
5080
|
+
...keys.owners.size > 0 && {
|
|
5081
|
+
owners: Object.fromEntries(
|
|
5082
|
+
[...keys.owners].map(([url, fields]) => [
|
|
5083
|
+
url,
|
|
5084
|
+
Object.fromEntries(fields)
|
|
5085
|
+
])
|
|
5086
|
+
)
|
|
5087
|
+
},
|
|
5088
|
+
...keys.owedTo !== void 0 && { owedTo: keys.owedTo }
|
|
5089
|
+
}
|
|
4682
5090
|
])
|
|
4683
5091
|
)
|
|
4684
5092
|
])
|
|
@@ -4690,7 +5098,7 @@ async function writeLedger(stateFilePath, ledger) {
|
|
|
4690
5098
|
if (ledger.size === 0) await deleteStoreFile(path6);
|
|
4691
5099
|
else await writeStoreFile(path6, serializeLedger(ledger));
|
|
4692
5100
|
}
|
|
4693
|
-
async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions) {
|
|
5101
|
+
async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions, owners = /* @__PURE__ */ new Map()) {
|
|
4694
5102
|
try {
|
|
4695
5103
|
const ledger = parseLedger(
|
|
4696
5104
|
await readStoreFile(namespaceLedgerPath(stateFilePath))
|
|
@@ -4704,7 +5112,7 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
|
|
|
4704
5112
|
let keys = byLocation.get(location);
|
|
4705
5113
|
let changed = false;
|
|
4706
5114
|
if (!keys) {
|
|
4707
|
-
keys =
|
|
5115
|
+
keys = emptyKeys();
|
|
4708
5116
|
byLocation.set(location, keys);
|
|
4709
5117
|
changed = true;
|
|
4710
5118
|
}
|
|
@@ -4718,12 +5126,105 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
|
|
|
4718
5126
|
keys.idpSessions.add(issuer);
|
|
4719
5127
|
changed = true;
|
|
4720
5128
|
}
|
|
5129
|
+
if (mergeOwners(keys.owners, owners)) changed = true;
|
|
4721
5130
|
if (changed) await writeLedger(stateFilePath, ledger);
|
|
4722
5131
|
} catch (error) {
|
|
4723
5132
|
warnLedgerFailure("update", error);
|
|
4724
5133
|
}
|
|
4725
5134
|
}
|
|
4726
|
-
async function
|
|
5135
|
+
async function markUncarriedNamespaces(stateFilePath, secretStore, into) {
|
|
5136
|
+
const ledger = parseLedger(
|
|
5137
|
+
await readStoreFile(namespaceLedgerPath(stateFilePath))
|
|
5138
|
+
);
|
|
5139
|
+
const location = await secretStoreLocation(secretStore);
|
|
5140
|
+
let changed = false;
|
|
5141
|
+
for (const [namespace, byLocation] of ledger) {
|
|
5142
|
+
if (namespace === into) continue;
|
|
5143
|
+
for (const [recorded, keys] of byLocation) {
|
|
5144
|
+
if (recorded === location || keys.owners.size === 0) continue;
|
|
5145
|
+
if (keys.owedTo === into) continue;
|
|
5146
|
+
keys.owedTo = into;
|
|
5147
|
+
changed = true;
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
if (changed) await writeLedger(stateFilePath, ledger);
|
|
5151
|
+
}
|
|
5152
|
+
async function pendingCarries(stateFilePath, secretStore, current) {
|
|
5153
|
+
const ledger = parseLedger(
|
|
5154
|
+
await readStoreFile(namespaceLedgerPath(stateFilePath))
|
|
5155
|
+
);
|
|
5156
|
+
const location = await secretStoreLocation(secretStore);
|
|
5157
|
+
const carries = [];
|
|
5158
|
+
for (const [namespace, byLocation] of ledger) {
|
|
5159
|
+
const owed = coveredLocations(location, byLocation).some(
|
|
5160
|
+
({ keys }) => keys.owedTo === current
|
|
5161
|
+
);
|
|
5162
|
+
if (owed) {
|
|
5163
|
+
carries.push({
|
|
5164
|
+
namespace,
|
|
5165
|
+
fields: candidateFields(location, byLocation)
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
return carries.reverse();
|
|
5170
|
+
}
|
|
5171
|
+
async function carryOwed(stateFilePath, ledger, location, secretStore, namespace, current, recorded, now) {
|
|
5172
|
+
let carried = true;
|
|
5173
|
+
for (const [url, fields] of recorded) {
|
|
5174
|
+
for (const field of fields) {
|
|
5175
|
+
const clientId = now.get(url)?.get(field);
|
|
5176
|
+
if (clientId === void 0) continue;
|
|
5177
|
+
try {
|
|
5178
|
+
const from = oauthSecretServerId(url, namespace);
|
|
5179
|
+
const to = oauthSecretServerId(url, current);
|
|
5180
|
+
const stampField = clientCredentialStampField(field);
|
|
5181
|
+
const value = await secretStoreGetStrict(secretStore, from, field);
|
|
5182
|
+
if (value === null) continue;
|
|
5183
|
+
const stamp = await secretStoreGetStrict(secretStore, from, stampField);
|
|
5184
|
+
if (stamp === null || stampedOwner(stamp, value) !== clientId) {
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
if (await secretStoreGetStrict(secretStore, to, field) !== null) {
|
|
5188
|
+
continue;
|
|
5189
|
+
}
|
|
5190
|
+
let byLocation = ledger.get(current);
|
|
5191
|
+
if (!byLocation) {
|
|
5192
|
+
byLocation = /* @__PURE__ */ new Map();
|
|
5193
|
+
ledger.set(current, byLocation);
|
|
5194
|
+
}
|
|
5195
|
+
const target = byLocation.get(location) ?? emptyKeys();
|
|
5196
|
+
byLocation.set(location, target);
|
|
5197
|
+
const isNew = !target.servers.has(url);
|
|
5198
|
+
target.servers.add(url);
|
|
5199
|
+
const owner = /* @__PURE__ */ new Map([[url, /* @__PURE__ */ new Map([[field, clientId]])]]);
|
|
5200
|
+
if (mergeOwners(target.owners, owner) || isNew) {
|
|
5201
|
+
await writeLedger(stateFilePath, ledger);
|
|
5202
|
+
}
|
|
5203
|
+
await secretStore.set(to, stampField, stamp);
|
|
5204
|
+
await secretStore.set(to, field, value);
|
|
5205
|
+
} catch (error) {
|
|
5206
|
+
carried = false;
|
|
5207
|
+
warnLedgerFailure(
|
|
5208
|
+
"carry over a superseded namespace recorded in",
|
|
5209
|
+
error
|
|
5210
|
+
);
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
return carried;
|
|
5215
|
+
}
|
|
5216
|
+
async function recordedNamespaces(stateFilePath) {
|
|
5217
|
+
try {
|
|
5218
|
+
const ledger = parseLedger(
|
|
5219
|
+
await readStoreFile(namespaceLedgerPath(stateFilePath))
|
|
5220
|
+
);
|
|
5221
|
+
return [...ledger.keys()].reverse();
|
|
5222
|
+
} catch (error) {
|
|
5223
|
+
warnLedgerFailure("read", error);
|
|
5224
|
+
return [];
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
async function purgeSupersededNamespaces(stateFilePath, secretStore, current, owners = /* @__PURE__ */ new Map()) {
|
|
4727
5228
|
let ledger;
|
|
4728
5229
|
let location;
|
|
4729
5230
|
try {
|
|
@@ -4736,8 +5237,22 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
|
|
|
4736
5237
|
return;
|
|
4737
5238
|
}
|
|
4738
5239
|
let changed = false;
|
|
4739
|
-
for (const [namespace, byLocation] of ledger) {
|
|
5240
|
+
for (const [namespace, byLocation] of [...ledger].reverse()) {
|
|
4740
5241
|
if (namespace === current) continue;
|
|
5242
|
+
if (current !== void 0 && coveredLocations(location, byLocation).some(
|
|
5243
|
+
({ keys }) => keys.owedTo === current
|
|
5244
|
+
) && !await carryOwed(
|
|
5245
|
+
stateFilePath,
|
|
5246
|
+
ledger,
|
|
5247
|
+
location,
|
|
5248
|
+
secretStore,
|
|
5249
|
+
namespace,
|
|
5250
|
+
current,
|
|
5251
|
+
candidateFields(location, byLocation),
|
|
5252
|
+
owners
|
|
5253
|
+
)) {
|
|
5254
|
+
break;
|
|
5255
|
+
}
|
|
4741
5256
|
for (const { location: recorded, keys } of coveredLocations(
|
|
4742
5257
|
location,
|
|
4743
5258
|
byLocation
|
|
@@ -4762,12 +5277,11 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
|
|
|
4762
5277
|
if (!purged || recorded !== location) continue;
|
|
4763
5278
|
byLocation.delete(recorded);
|
|
4764
5279
|
if (recorded.startsWith(FILE_LOCATION_PREFIX)) {
|
|
4765
|
-
const keyring = byLocation.get(KEYRING_LOCATION) ??
|
|
4766
|
-
|
|
4767
|
-
idpSessions: /* @__PURE__ */ new Set()
|
|
4768
|
-
};
|
|
5280
|
+
const keyring = byLocation.get(KEYRING_LOCATION) ?? emptyKeys();
|
|
5281
|
+
addMissingOwners(keyring.owners, keys.owners);
|
|
4769
5282
|
for (const url of keys.servers) keyring.servers.add(url);
|
|
4770
5283
|
for (const issuer of keys.idpSessions) keyring.idpSessions.add(issuer);
|
|
5284
|
+
keyring.owedTo ??= keys.owedTo;
|
|
4771
5285
|
byLocation.set(KEYRING_LOCATION, keyring);
|
|
4772
5286
|
}
|
|
4773
5287
|
changed = true;
|
|
@@ -4878,7 +5392,12 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
|
|
|
4878
5392
|
const existing = parseSecretsNamespace(raw);
|
|
4879
5393
|
if (existing !== void 0) {
|
|
4880
5394
|
if (locked) {
|
|
4881
|
-
await purgeSupersededNamespaces(
|
|
5395
|
+
await purgeSupersededNamespaces(
|
|
5396
|
+
filePath,
|
|
5397
|
+
secretStore,
|
|
5398
|
+
existing,
|
|
5399
|
+
snapshotClientOwners(parseOAuthPersistBlob(raw))
|
|
5400
|
+
);
|
|
4882
5401
|
}
|
|
4883
5402
|
return existing;
|
|
4884
5403
|
}
|
|
@@ -4886,42 +5405,53 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
|
|
|
4886
5405
|
if (raw !== null && snapshot === null) {
|
|
4887
5406
|
throw new OAuthStateFileUnrecognizedError(filePath, "save");
|
|
4888
5407
|
}
|
|
4889
|
-
if (locked) {
|
|
4890
|
-
await purgeSupersededNamespaces(filePath, secretStore, void 0);
|
|
4891
|
-
}
|
|
4892
5408
|
const namespace = newSecretsNamespace();
|
|
4893
|
-
if (snapshot === null)
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
})),
|
|
4900
|
-
...Object.keys(snapshot.idpSessions).map((issuer) => ({
|
|
4901
|
-
legacyId: oauthIdpSecretServerId(issuer),
|
|
4902
|
-
scopedId: oauthIdpSecretServerId(issuer, namespace),
|
|
4903
|
-
fields: [IDP_SESSION_FIELD]
|
|
4904
|
-
}))
|
|
4905
|
-
];
|
|
4906
|
-
if (moves.length === 0) return namespace;
|
|
5409
|
+
if (snapshot === null || Object.keys(snapshot.servers).length === 0 && Object.keys(snapshot.idpSessions).length === 0) {
|
|
5410
|
+
if (locked) {
|
|
5411
|
+
await purgeSupersededNamespaces(filePath, secretStore, void 0);
|
|
5412
|
+
}
|
|
5413
|
+
return namespace;
|
|
5414
|
+
}
|
|
4907
5415
|
if (!locked) {
|
|
4908
5416
|
throw new SecretStoreUnavailableError(
|
|
4909
5417
|
`Could not save OAuth state: ${filePath} predates per-state-file secret namespaces, and migrating its secret-store entries needs the file lock, which is unavailable here (see the lock warning above). Migrating without it could lose credentials if two processes migrate at once. Nothing was changed; make the lock directory writable and retry \u2014 or, if you cannot, clear this file's stored OAuth state and re-authorize (clearing does not migrate, and the fresh state file mints its namespace without the lock).`
|
|
4910
5418
|
);
|
|
4911
5419
|
}
|
|
5420
|
+
const values = await readEntrySecrets(
|
|
5421
|
+
snapshot,
|
|
5422
|
+
secretStore,
|
|
5423
|
+
void 0,
|
|
5424
|
+
await recordedNamespaces(filePath)
|
|
5425
|
+
);
|
|
5426
|
+
const owners = snapshotClientOwners(snapshot);
|
|
4912
5427
|
await recordNamespaceKeys(
|
|
4913
5428
|
filePath,
|
|
4914
5429
|
secretStore,
|
|
4915
5430
|
namespace,
|
|
4916
5431
|
Object.keys(snapshot.servers),
|
|
4917
|
-
Object.keys(snapshot.idpSessions)
|
|
5432
|
+
Object.keys(snapshot.idpSessions),
|
|
5433
|
+
writtenClientOwners(snapshot, (url) => values.servers.get(url) ?? {})
|
|
4918
5434
|
);
|
|
5435
|
+
await markUncarriedNamespaces(filePath, secretStore, namespace);
|
|
4919
5436
|
const touched = [];
|
|
4920
5437
|
try {
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
5438
|
+
const copies = [
|
|
5439
|
+
// Restamped: a value read from the legacy ids predates stamps, and a
|
|
5440
|
+
// fallback's stamp may belong to a value read from elsewhere (#2734).
|
|
5441
|
+
...[...values.servers].map(([url, secrets]) => ({
|
|
5442
|
+
scopedId: oauthSecretServerId(url, namespace),
|
|
5443
|
+
secrets: restampClientCredentials(
|
|
5444
|
+
getOwnEntry(snapshot.servers, url) ?? {},
|
|
5445
|
+
secrets
|
|
5446
|
+
)
|
|
5447
|
+
})),
|
|
5448
|
+
...[...values.idpSessions].map(([issuer, secrets]) => ({
|
|
5449
|
+
scopedId: oauthIdpSecretServerId(issuer, namespace),
|
|
5450
|
+
secrets
|
|
5451
|
+
}))
|
|
5452
|
+
];
|
|
5453
|
+
for (const { scopedId, secrets } of copies) {
|
|
5454
|
+
for (const [field, value] of Object.entries(secrets)) {
|
|
4925
5455
|
touched.push({ serverId: scopedId, field, value: null });
|
|
4926
5456
|
await secretStore.set(scopedId, field, value);
|
|
4927
5457
|
}
|
|
@@ -4931,7 +5461,14 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
|
|
|
4931
5461
|
await restoreSecretFields(secretStore, touched, warnRestoreFailure);
|
|
4932
5462
|
throw error;
|
|
4933
5463
|
}
|
|
4934
|
-
|
|
5464
|
+
await purgeSupersededNamespaces(filePath, secretStore, namespace, owners);
|
|
5465
|
+
const legacyIds = [
|
|
5466
|
+
...Object.keys(snapshot.servers).map((url) => oauthSecretServerId(url)),
|
|
5467
|
+
...Object.keys(snapshot.idpSessions).map(
|
|
5468
|
+
(issuer) => oauthIdpSecretServerId(issuer)
|
|
5469
|
+
)
|
|
5470
|
+
];
|
|
5471
|
+
for (const legacyId of legacyIds) {
|
|
4935
5472
|
try {
|
|
4936
5473
|
await secretStore.deleteAllForServer(legacyId);
|
|
4937
5474
|
} catch (error) {
|
|
@@ -4984,6 +5521,14 @@ function preserveNonDurableSecrets(diskSecrets, secrets) {
|
|
|
4984
5521
|
return keep;
|
|
4985
5522
|
}
|
|
4986
5523
|
async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
|
|
5524
|
+
await writeOAuthSectionsFrom(
|
|
5525
|
+
filePath,
|
|
5526
|
+
async () => snapshot,
|
|
5527
|
+
sections,
|
|
5528
|
+
secretStore
|
|
5529
|
+
);
|
|
5530
|
+
}
|
|
5531
|
+
async function writeOAuthSectionsFrom(filePath, resolveSnapshot, sections, secretStore) {
|
|
4987
5532
|
const policy = getPersistTokensPolicy();
|
|
4988
5533
|
const durable = await secretStoreIsDurable(secretStore);
|
|
4989
5534
|
await withOAuthStateLock(filePath, "save", async (locked) => {
|
|
@@ -5067,6 +5612,7 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
|
|
|
5067
5612
|
unconfirmed = null;
|
|
5068
5613
|
namespace = diskNamespace;
|
|
5069
5614
|
}
|
|
5615
|
+
const snapshot = await resolveSnapshot(disk, namespace);
|
|
5070
5616
|
const effective = sections ? {
|
|
5071
5617
|
servers: sections.servers && [...new Set(sections.servers)],
|
|
5072
5618
|
idpSessions: sections.idpSessions && [
|
|
@@ -5092,7 +5638,22 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
|
|
|
5092
5638
|
secretStore,
|
|
5093
5639
|
namespace,
|
|
5094
5640
|
Object.keys(merged.servers),
|
|
5095
|
-
Object.keys(merged.idpSessions)
|
|
5641
|
+
Object.keys(merged.idpSessions),
|
|
5642
|
+
// Only the entries this save writes: another entry's secret in
|
|
5643
|
+
// this store was written for whatever client it was written for,
|
|
5644
|
+
// and relabeling it from the file would misattribute it (#2672).
|
|
5645
|
+
writtenClientOwners(
|
|
5646
|
+
{
|
|
5647
|
+
servers: Object.fromEntries(
|
|
5648
|
+
(effective.servers ?? []).flatMap((url) => {
|
|
5649
|
+
const entry = getOwnEntry(merged.servers, url);
|
|
5650
|
+
return entry === void 0 ? [] : [[url, entry]];
|
|
5651
|
+
})
|
|
5652
|
+
),
|
|
5653
|
+
idpSessions: {}
|
|
5654
|
+
},
|
|
5655
|
+
splitSecretsOf
|
|
5656
|
+
)
|
|
5096
5657
|
);
|
|
5097
5658
|
for (const url of effective.servers ?? []) {
|
|
5098
5659
|
const serverId = oauthSecretServerId(url, namespace);
|
|
@@ -5100,7 +5661,8 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
|
|
|
5100
5661
|
const candidates = [
|
|
5101
5662
|
.../* @__PURE__ */ new Set([
|
|
5102
5663
|
...serverSecretFields(getOwnEntry(disk?.servers, url)),
|
|
5103
|
-
...serverSecretFields(next)
|
|
5664
|
+
...serverSecretFields(next),
|
|
5665
|
+
...[...restoreBaseline.values()].filter((prior) => prior.serverId === serverId).map(({ field }) => field)
|
|
5104
5666
|
])
|
|
5105
5667
|
];
|
|
5106
5668
|
const entryPrior = await snapshotSecretFields(
|
|
@@ -5109,8 +5671,12 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
|
|
|
5109
5671
|
candidates
|
|
5110
5672
|
);
|
|
5111
5673
|
priorSecrets.push(...entryPrior);
|
|
5674
|
+
const entryPriorValue = new Map(
|
|
5675
|
+
entryPrior.map(({ field, value }) => [field, value])
|
|
5676
|
+
);
|
|
5112
5677
|
if (next === void 0) {
|
|
5113
5678
|
for (const field of candidates) {
|
|
5679
|
+
if (entryPriorValue.get(field) === null) continue;
|
|
5114
5680
|
ourWrites.set(priorKey({ serverId, field }), null);
|
|
5115
5681
|
}
|
|
5116
5682
|
await secretStore.deleteAllForServer(serverId);
|
|
@@ -5125,10 +5691,9 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
|
|
|
5125
5691
|
}
|
|
5126
5692
|
const { residue, secrets } = splitServerOAuthState(next, policy);
|
|
5127
5693
|
for (const field of candidates) {
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
);
|
|
5694
|
+
const value = secrets[field] ?? null;
|
|
5695
|
+
if (value === entryPriorValue.get(field)) continue;
|
|
5696
|
+
ourWrites.set(priorKey({ serverId, field }), value);
|
|
5132
5697
|
}
|
|
5133
5698
|
setOwnEntry(merged.servers, url, residue);
|
|
5134
5699
|
if (!durable) {
|
|
@@ -5271,26 +5836,109 @@ function secretRequestsFor(snapshot, namespace) {
|
|
|
5271
5836
|
}
|
|
5272
5837
|
return requests;
|
|
5273
5838
|
}
|
|
5274
|
-
|
|
5275
|
-
const
|
|
5276
|
-
|
|
5839
|
+
function fillMissingSecrets(target, source) {
|
|
5840
|
+
for (const [field, value] of Object.entries(source ?? {})) {
|
|
5841
|
+
if (!Object.hasOwn(target, field)) setOwnEntry(target, field, value);
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
function writtenClientOwners(snapshot, secretsOf) {
|
|
5845
|
+
const owners = /* @__PURE__ */ new Map();
|
|
5846
|
+
for (const [url, state] of Object.entries(snapshot.servers)) {
|
|
5847
|
+
const secrets = secretsOf(url, state);
|
|
5848
|
+
const fields = new Map(
|
|
5849
|
+
[...clientCredentialOwners(state)].filter(
|
|
5850
|
+
([field]) => Object.hasOwn(secrets, field)
|
|
5851
|
+
)
|
|
5852
|
+
);
|
|
5853
|
+
if (fields.size > 0) owners.set(url, fields);
|
|
5854
|
+
}
|
|
5855
|
+
return owners;
|
|
5856
|
+
}
|
|
5857
|
+
var splitSecretsOf = (_url, state) => splitServerOAuthState(state, "all").secrets;
|
|
5858
|
+
function snapshotClientOwners(snapshot) {
|
|
5859
|
+
const owners = /* @__PURE__ */ new Map();
|
|
5860
|
+
for (const [url, state] of Object.entries(snapshot?.servers ?? {})) {
|
|
5861
|
+
const fields = clientCredentialOwners(state);
|
|
5862
|
+
if (fields.size > 0) owners.set(url, fields);
|
|
5863
|
+
}
|
|
5864
|
+
return owners;
|
|
5865
|
+
}
|
|
5866
|
+
async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks, carries = []) {
|
|
5867
|
+
const servers = /* @__PURE__ */ new Map();
|
|
5868
|
+
const idpSessions = /* @__PURE__ */ new Map();
|
|
5869
|
+
for (const url of Object.keys(snapshot.servers)) servers.set(url, {});
|
|
5870
|
+
for (const issuer of Object.keys(snapshot.idpSessions)) {
|
|
5871
|
+
idpSessions.set(issuer, {});
|
|
5872
|
+
}
|
|
5873
|
+
for (const source of [namespace, ...fallbacks]) {
|
|
5874
|
+
const requests = secretRequestsFor(snapshot, source);
|
|
5875
|
+
if (requests.length === 0) break;
|
|
5876
|
+
const values = await secretStoreGetManyStrict(secretStore, requests);
|
|
5877
|
+
for (const [url, target] of servers) {
|
|
5878
|
+
fillMissingSecrets(target, values[oauthSecretServerId(url, source)]);
|
|
5879
|
+
}
|
|
5880
|
+
for (const [issuer, target] of idpSessions) {
|
|
5881
|
+
fillMissingSecrets(
|
|
5882
|
+
target,
|
|
5883
|
+
values[oauthIdpSecretServerId(issuer, source)]
|
|
5884
|
+
);
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
const owners = snapshotClientOwners(snapshot);
|
|
5888
|
+
for (const { namespace: from, fields: recorded } of carries) {
|
|
5889
|
+
const requests = [];
|
|
5890
|
+
for (const [url, fields] of recorded) {
|
|
5891
|
+
const target = servers.get(url);
|
|
5892
|
+
if (!target) continue;
|
|
5893
|
+
const wanted = [...fields].filter(
|
|
5894
|
+
(field) => owners.get(url)?.has(field) === true && !Object.hasOwn(target, field)
|
|
5895
|
+
);
|
|
5896
|
+
if (wanted.length > 0) {
|
|
5897
|
+
requests.push({
|
|
5898
|
+
serverId: oauthSecretServerId(url, from),
|
|
5899
|
+
fields: wanted.flatMap((field) => [
|
|
5900
|
+
field,
|
|
5901
|
+
clientCredentialStampField(field)
|
|
5902
|
+
])
|
|
5903
|
+
});
|
|
5904
|
+
}
|
|
5905
|
+
}
|
|
5906
|
+
if (requests.length === 0) continue;
|
|
5907
|
+
const values = await secretStoreGetManyStrict(secretStore, requests);
|
|
5908
|
+
for (const [url, target] of servers) {
|
|
5909
|
+
const found = values[oauthSecretServerId(url, from)] ?? {};
|
|
5910
|
+
for (const [field, clientId] of owners.get(url) ?? []) {
|
|
5911
|
+
const value = Object.hasOwn(found, field) ? found[field] : void 0;
|
|
5912
|
+
const stampField = clientCredentialStampField(field);
|
|
5913
|
+
if (value === void 0 || Object.hasOwn(target, field) || stampedOwner(found[stampField], value) !== clientId) {
|
|
5914
|
+
continue;
|
|
5915
|
+
}
|
|
5916
|
+
setOwnEntry(target, field, value);
|
|
5917
|
+
setOwnEntry(target, stampField, found[stampField]);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
return { servers, idpSessions };
|
|
5922
|
+
}
|
|
5923
|
+
async function joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries = []) {
|
|
5924
|
+
const values = await readEntrySecrets(
|
|
5925
|
+
snapshot,
|
|
5926
|
+
secretStore,
|
|
5927
|
+
namespace,
|
|
5928
|
+
fallbacks,
|
|
5929
|
+
carries
|
|
5930
|
+
);
|
|
5277
5931
|
return {
|
|
5278
5932
|
servers: Object.fromEntries(
|
|
5279
5933
|
Object.entries(snapshot.servers).map(([url, state]) => [
|
|
5280
5934
|
url,
|
|
5281
|
-
joinServerOAuthState(
|
|
5282
|
-
state,
|
|
5283
|
-
values[oauthSecretServerId(url, namespace)] ?? {}
|
|
5284
|
-
)
|
|
5935
|
+
joinServerOAuthState(state, values.servers.get(url) ?? {})
|
|
5285
5936
|
])
|
|
5286
5937
|
),
|
|
5287
5938
|
idpSessions: Object.fromEntries(
|
|
5288
5939
|
Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
|
|
5289
5940
|
issuer,
|
|
5290
|
-
joinIdpSession(
|
|
5291
|
-
session,
|
|
5292
|
-
values[oauthIdpSecretServerId(issuer, namespace)] ?? {}
|
|
5293
|
-
)
|
|
5941
|
+
joinIdpSession(session, values.idpSessions.get(issuer) ?? {})
|
|
5294
5942
|
])
|
|
5295
5943
|
)
|
|
5296
5944
|
};
|
|
@@ -5306,7 +5954,8 @@ async function migratePlaintextSecrets(filePath, secretStore) {
|
|
|
5306
5954
|
secretStore,
|
|
5307
5955
|
namespace,
|
|
5308
5956
|
Object.keys(fresh.servers),
|
|
5309
|
-
Object.keys(fresh.idpSessions)
|
|
5957
|
+
Object.keys(fresh.idpSessions),
|
|
5958
|
+
writtenClientOwners(fresh, splitSecretsOf)
|
|
5310
5959
|
);
|
|
5311
5960
|
}
|
|
5312
5961
|
const migrateEntrySecrets = async (serverId, secrets) => {
|
|
@@ -5357,7 +6006,10 @@ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
|
|
|
5357
6006
|
warnMigrationFailure(error);
|
|
5358
6007
|
}
|
|
5359
6008
|
}
|
|
5360
|
-
|
|
6009
|
+
const namespace = parseSecretsNamespace(raw);
|
|
6010
|
+
const fallbacks = namespace === void 0 ? await recordedNamespaces(filePath) : [];
|
|
6011
|
+
const carries = namespace === void 0 ? [] : await pendingCarries(filePath, secretStore, namespace);
|
|
6012
|
+
return joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries);
|
|
5361
6013
|
});
|
|
5362
6014
|
}
|
|
5363
6015
|
async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
|
|
@@ -5597,6 +6249,8 @@ var RemoteSession = class _RemoteSession {
|
|
|
5597
6249
|
requestWaits = /* @__PURE__ */ new Map();
|
|
5598
6250
|
authProviderHandle = null;
|
|
5599
6251
|
appliedProtocolVersion;
|
|
6252
|
+
/** Each upstream `429`'s `Retry-After`, by the JSON-RPC id that drew it (#2442). */
|
|
6253
|
+
rateLimits = new RateLimitLedger();
|
|
5600
6254
|
/** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
|
|
5601
6255
|
static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
5602
6256
|
constructor(sessionId) {
|
|
@@ -5808,6 +6462,7 @@ var RemoteSession = class _RemoteSession {
|
|
|
5808
6462
|
this.pushEvent({ type: "message", data: message });
|
|
5809
6463
|
}
|
|
5810
6464
|
onFetchRequest(entry) {
|
|
6465
|
+
this.rateLimits.record(entry);
|
|
5811
6466
|
this.pushEvent({
|
|
5812
6467
|
type: "fetch_request",
|
|
5813
6468
|
data: {
|
|
@@ -5816,6 +6471,14 @@ var RemoteSession = class _RemoteSession {
|
|
|
5816
6471
|
}
|
|
5817
6472
|
});
|
|
5818
6473
|
}
|
|
6474
|
+
/**
|
|
6475
|
+
* The `Retry-After` of the upstream `429` that answered request `id` — read
|
|
6476
|
+
* by the send route, so the header rides back on the send's own error rather
|
|
6477
|
+
* than on a fetch event racing it (#2442).
|
|
6478
|
+
*/
|
|
6479
|
+
takeRetryAfter(id) {
|
|
6480
|
+
return this.rateLimits.take(id);
|
|
6481
|
+
}
|
|
5819
6482
|
onFetchResponseBody(id, responseBody) {
|
|
5820
6483
|
this.pushEvent({
|
|
5821
6484
|
type: "fetch_request_body_update",
|
|
@@ -6259,6 +6922,17 @@ var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
|
|
|
6259
6922
|
var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
|
|
6260
6923
|
var MAX_APP_CSP_CHARS = 8 * 1024;
|
|
6261
6924
|
var MAX_STORAGE_BODY_BYTES = 4 * 1024 * 1024;
|
|
6925
|
+
var MAX_PROXIED_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
6926
|
+
var BODY_FRAMING_HEADERS = /* @__PURE__ */ new Set([
|
|
6927
|
+
"content-length",
|
|
6928
|
+
"transfer-encoding",
|
|
6929
|
+
"content-encoding"
|
|
6930
|
+
]);
|
|
6931
|
+
function withoutBodyFraming(headers) {
|
|
6932
|
+
return Object.fromEntries(
|
|
6933
|
+
Object.entries(headers).filter(([k]) => !BODY_FRAMING_HEADERS.has(k))
|
|
6934
|
+
);
|
|
6935
|
+
}
|
|
6262
6936
|
function isHeaderSafeValue(value) {
|
|
6263
6937
|
return !/[^\t\x20-\x7e]/.test(value);
|
|
6264
6938
|
}
|
|
@@ -6714,6 +7388,25 @@ function createRemoteApp(options) {
|
|
|
6714
7388
|
});
|
|
6715
7389
|
}
|
|
6716
7390
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7391
|
+
if (err instanceof SdkHttpError2) {
|
|
7392
|
+
const text = err.data.text;
|
|
7393
|
+
const sentId = "id" in message ? message.id : void 0;
|
|
7394
|
+
const retryAfter = isRateLimitError(err) && (typeof sentId === "string" || typeof sentId === "number") ? session.takeRetryAfter(sentId) : void 0;
|
|
7395
|
+
return c.json({
|
|
7396
|
+
ok: false,
|
|
7397
|
+
kind: "transport_error",
|
|
7398
|
+
error: msg,
|
|
7399
|
+
http: {
|
|
7400
|
+
code: err.code,
|
|
7401
|
+
status: err.status,
|
|
7402
|
+
...err.statusText !== void 0 && {
|
|
7403
|
+
statusText: err.statusText
|
|
7404
|
+
},
|
|
7405
|
+
...typeof text === "string" && { text },
|
|
7406
|
+
...retryAfter !== void 0 && { retryAfter }
|
|
7407
|
+
}
|
|
7408
|
+
});
|
|
7409
|
+
}
|
|
6717
7410
|
return c.json({ ok: false, kind: "transport_error", error: msg });
|
|
6718
7411
|
} finally {
|
|
6719
7412
|
clientSignal.removeEventListener("abort", onClientDisconnect);
|
|
@@ -6843,7 +7536,28 @@ function createRemoteApp(options) {
|
|
|
6843
7536
|
const isStream = contentType?.includes("text/event-stream") || contentType?.includes("application/x-ndjson");
|
|
6844
7537
|
let resBody;
|
|
6845
7538
|
if (!isStream && res.body) {
|
|
6846
|
-
|
|
7539
|
+
const refuseOversize = () => res.ok ? c.json(
|
|
7540
|
+
{
|
|
7541
|
+
error: `response from ${safeUrl} exceeds the ${MAX_PROXIED_RESPONSE_BYTES}-byte limit for a proxied response`
|
|
7542
|
+
},
|
|
7543
|
+
502
|
|
7544
|
+
) : c.json({
|
|
7545
|
+
ok: res.ok,
|
|
7546
|
+
status: res.status,
|
|
7547
|
+
statusText: res.statusText,
|
|
7548
|
+
headers: withoutBodyFraming(resHeaders)
|
|
7549
|
+
});
|
|
7550
|
+
if (declaresOversizeBody(
|
|
7551
|
+
res.headers.get("content-length"),
|
|
7552
|
+
MAX_PROXIED_RESPONSE_BYTES
|
|
7553
|
+
)) {
|
|
7554
|
+
void res.body.cancel().catch(() => {
|
|
7555
|
+
});
|
|
7556
|
+
return refuseOversize();
|
|
7557
|
+
}
|
|
7558
|
+
const read = await readCappedText(res.body, MAX_PROXIED_RESPONSE_BYTES);
|
|
7559
|
+
if (read.kind === "overflow") return refuseOversize();
|
|
7560
|
+
resBody = read.text;
|
|
6847
7561
|
} else if (isStream) {
|
|
6848
7562
|
void res.body?.cancel().catch(() => {
|
|
6849
7563
|
});
|
|
@@ -7656,7 +8370,7 @@ function createRemoteApp(options) {
|
|
|
7656
8370
|
type,
|
|
7657
8371
|
process.platform,
|
|
7658
8372
|
homedir(),
|
|
7659
|
-
(path6) => existsSync3(path6) ?
|
|
8373
|
+
(path6) => existsSync3(path6) ? readFileSync7(path6, "utf-8") : null
|
|
7660
8374
|
);
|
|
7661
8375
|
if (!result) {
|
|
7662
8376
|
return c.json(
|
|
@@ -8072,6 +8786,30 @@ function handleNodeHealthRequest(req, res) {
|
|
|
8072
8786
|
return true;
|
|
8073
8787
|
}
|
|
8074
8788
|
|
|
8789
|
+
// server/node-request-bridge.ts
|
|
8790
|
+
function fetchUrlForNodeRequest(headers, pathname, scheme) {
|
|
8791
|
+
const authority = headers.host ?? headers[":authority"];
|
|
8792
|
+
return `${scheme}://${authority}${pathname}`;
|
|
8793
|
+
}
|
|
8794
|
+
function fetchHeadersFromNode(headers) {
|
|
8795
|
+
const out = new Headers();
|
|
8796
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
8797
|
+
if (!value || key.startsWith(":")) continue;
|
|
8798
|
+
out.set(key, Array.isArray(value) ? value.join(", ") : value);
|
|
8799
|
+
}
|
|
8800
|
+
return out;
|
|
8801
|
+
}
|
|
8802
|
+
var CONNECTION_SPECIFIC_HEADERS = /* @__PURE__ */ new Set([
|
|
8803
|
+
"connection",
|
|
8804
|
+
"keep-alive",
|
|
8805
|
+
"proxy-connection",
|
|
8806
|
+
"transfer-encoding",
|
|
8807
|
+
"upgrade"
|
|
8808
|
+
]);
|
|
8809
|
+
function isResponseHeaderAllowed(name, httpVersionMajor) {
|
|
8810
|
+
return httpVersionMajor < 2 || !CONNECTION_SPECIFIC_HEADERS.has(name.toLowerCase());
|
|
8811
|
+
}
|
|
8812
|
+
|
|
8075
8813
|
// server/vite-hono-plugin.ts
|
|
8076
8814
|
function honoMiddlewarePlugin(config) {
|
|
8077
8815
|
let resolvedAuthToken = "";
|
|
@@ -8179,18 +8917,12 @@ function honoMiddlewarePlugin(config) {
|
|
|
8179
8917
|
if (!pathname.startsWith("/api")) {
|
|
8180
8918
|
return next();
|
|
8181
8919
|
}
|
|
8182
|
-
const url =
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
(
|
|
8186
|
-
if (value) {
|
|
8187
|
-
headers.set(
|
|
8188
|
-
key,
|
|
8189
|
-
Array.isArray(value) ? value.join(", ") : value
|
|
8190
|
-
);
|
|
8191
|
-
}
|
|
8192
|
-
}
|
|
8920
|
+
const url = fetchUrlForNodeRequest(
|
|
8921
|
+
req.headers,
|
|
8922
|
+
pathname,
|
|
8923
|
+
webScheme(config.tls)
|
|
8193
8924
|
);
|
|
8925
|
+
const headers = fetchHeadersFromNode(req.headers);
|
|
8194
8926
|
const init = { method: req.method, headers };
|
|
8195
8927
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
8196
8928
|
const chunks = [];
|
|
@@ -8207,13 +8939,17 @@ function honoMiddlewarePlugin(config) {
|
|
|
8207
8939
|
const response = await honoApp.fetch(new Request(url, init));
|
|
8208
8940
|
res.statusCode = response.status;
|
|
8209
8941
|
response.headers.forEach((value, key) => {
|
|
8210
|
-
|
|
8942
|
+
if (isResponseHeaderAllowed(key, req.httpVersionMajor)) {
|
|
8943
|
+
res.setHeader(key, value);
|
|
8944
|
+
}
|
|
8211
8945
|
});
|
|
8212
8946
|
const isSSE = response.headers.get("content-type")?.includes("text/event-stream");
|
|
8213
8947
|
if (isSSE) {
|
|
8214
8948
|
res.setHeader("X-Accel-Buffering", "no");
|
|
8215
8949
|
res.setHeader("Cache-Control", "no-cache");
|
|
8216
|
-
|
|
8950
|
+
if (isResponseHeaderAllowed("connection", req.httpVersionMajor)) {
|
|
8951
|
+
res.setHeader("Connection", "keep-alive");
|
|
8952
|
+
}
|
|
8217
8953
|
}
|
|
8218
8954
|
if (response.body) {
|
|
8219
8955
|
res.flushHeaders?.();
|
|
@@ -8566,6 +9302,10 @@ async function startViteDevServer(config) {
|
|
|
8566
9302
|
// `config.port`, so a drifted bind would 403 every connect and CSP-block
|
|
8567
9303
|
// the MCP Apps iframe while the banner advertises the unusable port.
|
|
8568
9304
|
strictPort: true,
|
|
9305
|
+
// The same TLS pair the prod listener uses (#2446), matching
|
|
9306
|
+
// `vite.config.ts`. Vite serves it over HTTP/2 with an HTTP/1.1
|
|
9307
|
+
// fallback; the Hono plugin's /api bridge handles both.
|
|
9308
|
+
https: config.tls,
|
|
8569
9309
|
// Allow Vite to serve source files from the repo root (core/ lives
|
|
8570
9310
|
// outside clients/web), matching `vite.config.ts`'s `server.fs.allow`.
|
|
8571
9311
|
fs: {
|
|
@@ -8584,10 +9324,11 @@ async function startViteDevServer(config) {
|
|
|
8584
9324
|
}
|
|
8585
9325
|
|
|
8586
9326
|
// server/server.ts
|
|
8587
|
-
import { readFileSync as
|
|
9327
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
8588
9328
|
import { join as join8, dirname as dirname8 } from "path";
|
|
8589
9329
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
8590
9330
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
9331
|
+
import { createServer as createHttpsServer } from "https";
|
|
8591
9332
|
import { serve } from "@hono/node-server";
|
|
8592
9333
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
8593
9334
|
import { Hono as Hono2 } from "hono";
|
|
@@ -8634,12 +9375,20 @@ async function startHonoServer(config) {
|
|
|
8634
9375
|
});
|
|
8635
9376
|
const app = new Hono2();
|
|
8636
9377
|
app.get(HEALTH_PATH, (c) => healthResponse(c.req.method));
|
|
9378
|
+
app.use(
|
|
9379
|
+
"*",
|
|
9380
|
+
hostAllowListMiddleware({
|
|
9381
|
+
bindHost: config.hostname,
|
|
9382
|
+
allowedOrigins: config.allowedOrigins,
|
|
9383
|
+
extraHosts: config.allowedHosts
|
|
9384
|
+
})
|
|
9385
|
+
);
|
|
8637
9386
|
app.use("/api/*", async (c) => {
|
|
8638
9387
|
return apiApp.fetch(c.req.raw);
|
|
8639
9388
|
});
|
|
8640
9389
|
const serveIndexHtml = (c) => {
|
|
8641
9390
|
const indexPath = join8(rootPath, "index.html");
|
|
8642
|
-
const html =
|
|
9391
|
+
const html = readFileSync8(indexPath, "utf-8");
|
|
8643
9392
|
c.header("Cache-Control", "no-store");
|
|
8644
9393
|
return c.html(injectAuthToken(html, resolvedAuthToken));
|
|
8645
9394
|
};
|
|
@@ -8667,7 +9416,10 @@ async function startHonoServer(config) {
|
|
|
8667
9416
|
{
|
|
8668
9417
|
fetch: app.fetch,
|
|
8669
9418
|
port: config.port,
|
|
8670
|
-
hostname: config.hostname
|
|
9419
|
+
hostname: config.hostname,
|
|
9420
|
+
// https only when both TLS paths were set (#2446); otherwise
|
|
9421
|
+
// @hono/node-server's default plain `http.createServer`.
|
|
9422
|
+
...config.tls ? { createServer: createHttpsServer, serverOptions: config.tls } : {}
|
|
8671
9423
|
},
|
|
8672
9424
|
(info) => {
|
|
8673
9425
|
const sandboxUrl = sandboxController.getUrl();
|
|
@@ -8685,9 +9437,7 @@ async function startHonoServer(config) {
|
|
|
8685
9437
|
);
|
|
8686
9438
|
httpServer.on("error", (err) => {
|
|
8687
9439
|
if (err.message.includes("EADDRINUSE")) {
|
|
8688
|
-
console.error(
|
|
8689
|
-
`MCP Inspector PORT IS IN USE at http://${formatHostForUrl(config.hostname)}:${config.port}`
|
|
8690
|
-
);
|
|
9440
|
+
console.error(portInUseMessage(config));
|
|
8691
9441
|
process.exit(1);
|
|
8692
9442
|
} else {
|
|
8693
9443
|
throw err;
|
|
@@ -8768,7 +9518,11 @@ async function runWeb(argv) {
|
|
|
8768
9518
|
preArgs = argv.slice(0, argSeparatorIndex);
|
|
8769
9519
|
postArgs = argv.slice(argSeparatorIndex + 1);
|
|
8770
9520
|
}
|
|
8771
|
-
program.name("mcp-inspector-web").description("Web UI for MCP Inspector").
|
|
9521
|
+
program.name("mcp-inspector-web").description("Web UI for MCP Inspector").version(
|
|
9522
|
+
readInspectorVersionSafe(import.meta.url) ?? "unknown",
|
|
9523
|
+
"-V, --version",
|
|
9524
|
+
"Print the Inspector version"
|
|
9525
|
+
).allowExcessArguments().allowUnknownOption().option(
|
|
8772
9526
|
"-e <env>",
|
|
8773
9527
|
"environment variables in KEY=VALUE format",
|
|
8774
9528
|
parseKeyValuePair,
|