@modelcontextprotocol/inspector 2.11.0 → 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.
@@ -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([
@@ -1061,14 +1067,25 @@ function readInspectorVersionSafe(callerUrl) {
1061
1067
  // ../../core/node/openUrl.ts
1062
1068
  import open from "open";
1063
1069
  var OPEN_URL_TIMEOUT_MS = 5e3;
1064
- function launched(child) {
1065
- return new Promise((resolve10, reject) => {
1066
- child.on("error", reject);
1067
- child.once("spawn", () => resolve10());
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", () => {
1068
1085
  });
1069
1086
  }
1070
1087
  async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
1071
- const href = typeof url === "string" ? url : url.href;
1088
+ const href = toOpenableHref(typeof url === "string" ? url : url.href);
1072
1089
  let timer;
1073
1090
  const timeout = new Promise((_, reject) => {
1074
1091
  timer = setTimeout(
@@ -1077,8 +1094,7 @@ async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
1077
1094
  );
1078
1095
  });
1079
1096
  try {
1080
- const opened = Promise.resolve().then(() => open(href)).then(launched);
1081
- await Promise.race([opened, timeout]);
1097
+ await Promise.race([open(href).then(absorbLateErrors), timeout]);
1082
1098
  } finally {
1083
1099
  clearTimeout(timer);
1084
1100
  }
@@ -1589,6 +1605,153 @@ function resolveAppOriginPublicOrigin(raw, allowedOrigins, sandboxUrl) {
1589
1605
  return url.origin;
1590
1606
  }
1591
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
+
1592
1755
  // server/web-server-config.ts
1593
1756
  var inspectorVersion = readInspectorVersionSafe(import.meta.url);
1594
1757
  function defaultEnvironmentFromProcess(extra) {
@@ -1670,7 +1833,7 @@ function transportDefaults(config) {
1670
1833
  function printServerBanner(config, actualPort, resolvedToken, sandboxUrl, secretStorage) {
1671
1834
  const h = canonicalUrlHost(config.hostname);
1672
1835
  const bannerHost = isAllInterfacesHost(h) ? "localhost" : h;
1673
- const baseUrl = httpOrigin(bannerHost, actualPort);
1836
+ const baseUrl = webOrigin(webScheme(config.tls), bannerHost, actualPort);
1674
1837
  const url = config.dangerouslyOmitAuth || !resolvedToken ? baseUrl : `${baseUrl}?${API_SERVER_ENV_VARS.AUTH_TOKEN}=${resolvedToken}`;
1675
1838
  console.log(`
1676
1839
  MCP Inspector Web is up and running at:
@@ -1695,6 +1858,9 @@ MCP Inspector Web is up and running at:
1695
1858
  }
1696
1859
  return url;
1697
1860
  }
1861
+ function portInUseMessage(config) {
1862
+ return `MCP Inspector PORT IS IN USE at ${webScheme(config.tls)}://${formatHostForUrl(config.hostname)}:${config.port}`;
1863
+ }
1698
1864
  function openBrowser(url) {
1699
1865
  openUrl(url).catch((err) => {
1700
1866
  const reason = err instanceof Error ? err.message : String(err);
@@ -1704,29 +1870,30 @@ function openBrowser(url) {
1704
1870
  });
1705
1871
  }
1706
1872
  var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1707
- function httpOrigin(host, port) {
1708
- return port === 80 ? `http://${host}` : `http://${host}:${port}`;
1873
+ function webOrigin(scheme, host, port) {
1874
+ const defaultPort = scheme === "https" ? 443 : 80;
1875
+ return port === defaultPort ? `${scheme}://${host}` : `${scheme}://${host}:${port}`;
1709
1876
  }
1710
- function loopbackOrigins(port) {
1877
+ function loopbackOrigins(scheme, port) {
1711
1878
  return [
1712
- httpOrigin("localhost", port),
1713
- httpOrigin("127.0.0.1", port),
1714
- httpOrigin("[::1]", port)
1879
+ webOrigin(scheme, "localhost", port),
1880
+ webOrigin(scheme, "127.0.0.1", port),
1881
+ webOrigin(scheme, "[::1]", port)
1715
1882
  ];
1716
1883
  }
1717
- function defaultAllowedOrigins(hostname, port) {
1884
+ function defaultAllowedOrigins(hostname, port, scheme = "http") {
1718
1885
  const h = canonicalUrlHost(hostname);
1719
1886
  if (LOOPBACK_HOSTNAMES.has(h)) {
1720
- return loopbackOrigins(port);
1887
+ return loopbackOrigins(scheme, port);
1721
1888
  }
1722
1889
  if (isAllInterfacesHost(h)) {
1723
1890
  return [
1724
- ...loopbackOrigins(port),
1725
- httpOrigin("0.0.0.0", port),
1726
- httpOrigin("[::]", port)
1891
+ ...loopbackOrigins(scheme, port),
1892
+ webOrigin(scheme, "0.0.0.0", port),
1893
+ webOrigin(scheme, "[::]", port)
1727
1894
  ];
1728
1895
  }
1729
- return [httpOrigin(h, port)];
1896
+ return [webOrigin(scheme, h, port)];
1730
1897
  }
1731
1898
  function buildWebServerConfig(options = {}) {
1732
1899
  const {
@@ -1743,6 +1910,7 @@ function buildWebServerConfig(options = {}) {
1743
1910
  );
1744
1911
  }
1745
1912
  const hostname = resolveBindHostname();
1913
+ const tls = resolveTlsConfig();
1746
1914
  const dangerouslyOmitAuth = isEnvFlagEnabled(
1747
1915
  process.env.DANGEROUSLY_OMIT_AUTH
1748
1916
  );
@@ -1778,7 +1946,7 @@ function buildWebServerConfig(options = {}) {
1778
1946
  return null;
1779
1947
  }
1780
1948
  }).filter((o) => o !== null);
1781
- const allowedOrigins = configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port);
1949
+ const allowedOrigins = configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port, webScheme(tls));
1782
1950
  const sandboxPublicUrl = resolveSandboxPublicUrl(
1783
1951
  process.env[SANDBOX_FULL_ADDRESS_ENV],
1784
1952
  allowedOrigins
@@ -1799,13 +1967,15 @@ function buildWebServerConfig(options = {}) {
1799
1967
  initialServers,
1800
1968
  storageDir: process.env.MCP_STORAGE_DIR,
1801
1969
  allowedOrigins,
1970
+ allowedHosts: parseAllowedHostsEnv(process.env[ALLOWED_HOSTS_ENV]),
1802
1971
  sandboxPort,
1803
1972
  sandboxHost: hostname,
1804
1973
  appOriginPort,
1805
1974
  sandboxPublicUrl,
1806
1975
  appOriginPublicOrigin,
1807
1976
  logger,
1808
- autoOpen: resolveAutoOpen()
1977
+ autoOpen: resolveAutoOpen(),
1978
+ ...tls ? { tls } : {}
1809
1979
  };
1810
1980
  }
1811
1981
  function resolveAutoOpen() {
@@ -1823,7 +1993,7 @@ import react from "@vitejs/plugin-react";
1823
1993
 
1824
1994
  // ../../core/mcp/remote/node/server.ts
1825
1995
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1826
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1996
+ import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
1827
1997
  import { stat as fsStat } from "fs/promises";
1828
1998
  import { homedir } from "os";
1829
1999
  import { Hono } from "hono";
@@ -2640,11 +2810,95 @@ function createTransportNode(config, options = {}) {
2640
2810
  }
2641
2811
  }
2642
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
+
2643
2838
  // ../../core/auth/requestTimeout.ts
2644
2839
  var OAUTH_TIMEOUT_WIRE_CODE = "oauth_request_timeout";
2645
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
+
2646
2900
  // ../../core/mcp/remote/node/server.ts
2647
- import { SdkHttpError } from "@modelcontextprotocol/client";
2901
+ import { SdkHttpError as SdkHttpError2 } from "@modelcontextprotocol/client";
2648
2902
 
2649
2903
  // ../../core/auth/oauth-persist.ts
2650
2904
  import { OAuthTokensSchema } from "@modelcontextprotocol/core";
@@ -3273,7 +3527,7 @@ import * as path4 from "path";
3273
3527
 
3274
3528
  // ../../core/auth/node/file-secret-store.ts
3275
3529
  import * as crypto2 from "crypto";
3276
- import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3530
+ import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
3277
3531
  import * as fs3 from "fs/promises";
3278
3532
  import * as path3 from "path";
3279
3533
  var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve10, reject) => {
@@ -3326,7 +3580,7 @@ function resolveSecretPassphrase(env = process.env, secretFilePath) {
3326
3580
  }
3327
3581
  let contents;
3328
3582
  try {
3329
- contents = readFileSync4(resolved2, "utf-8");
3583
+ contents = readFileSync5(resolved2, "utf-8");
3330
3584
  } catch (err) {
3331
3585
  return {
3332
3586
  // `String(err)` rather than `.message`: `readFileSync` only ever throws
@@ -4356,7 +4610,7 @@ async function resolveConcreteSecretStore(store) {
4356
4610
  import { resolve as resolve6 } from "path";
4357
4611
 
4358
4612
  // ../../core/auth/node/oauth-secrets.ts
4359
- import { randomUUID as randomUUID2 } from "crypto";
4613
+ import { createHmac, randomUUID as randomUUID2 } from "crypto";
4360
4614
  import { OAuthTokensSchema as OAuthTokensSchema2 } from "@modelcontextprotocol/core";
4361
4615
  var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
4362
4616
  var warnedPolicyValues = /* @__PURE__ */ new Set();
@@ -4389,6 +4643,29 @@ var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
4389
4643
  var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
4390
4644
  var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
4391
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
+ }
4392
4669
  function applyTokensPolicy(tokens, policy) {
4393
4670
  if (policy === "none") return void 0;
4394
4671
  if (policy === "access" && tokens.refresh_token !== void 0) {
@@ -4417,12 +4694,18 @@ function splitClientInformation(info, fields, secrets) {
4417
4694
  if (client_secret === void 0 && registration_access_token === void 0) {
4418
4695
  return info;
4419
4696
  }
4420
- if (client_secret !== void 0) {
4421
- secrets[fields.clientSecret] = client_secret;
4422
- }
4423
- if (registration_access_token !== void 0) {
4424
- secrets[fields.registrationToken] = registration_access_token;
4425
- }
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);
4426
4709
  return publicInfo;
4427
4710
  }
4428
4711
  function joinClientInformation(info, fields, secrets) {
@@ -4441,15 +4724,18 @@ function joinClientInformation(info, fields, secrets) {
4441
4724
  }
4442
4725
  var issuerClientInfoFields = (issuer) => ({
4443
4726
  clientSecret: issuerClientSecretField(issuer),
4444
- registrationToken: issuerRegistrationTokenField(issuer)
4727
+ registrationToken: issuerRegistrationTokenField(issuer),
4728
+ stamped: true
4445
4729
  });
4446
4730
  var LEGACY_CLIENT_INFO_FIELDS = {
4447
4731
  clientSecret: LEGACY_CLIENT_SECRET_FIELD,
4448
- registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
4732
+ registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD,
4733
+ stamped: true
4449
4734
  };
4450
4735
  var PREREG_CLIENT_INFO_FIELDS = {
4451
4736
  clientSecret: PREREG_CLIENT_SECRET_FIELD,
4452
- registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
4737
+ registrationToken: PREREG_REGISTRATION_TOKEN_FIELD,
4738
+ stamped: false
4453
4739
  };
4454
4740
  function splitServerOAuthState(state, policy) {
4455
4741
  const secrets = {};
@@ -4599,20 +4885,54 @@ function joinIdpSession(residue, secrets) {
4599
4885
  return stored ? { ...residue, ...stored } : { ...residue };
4600
4886
  }
4601
4887
  function serverSecretFields(state) {
4888
+ const stamped = (field) => [
4889
+ field,
4890
+ clientCredentialStampField(field)
4891
+ ];
4602
4892
  const fields = /* @__PURE__ */ new Set([
4603
4893
  LEGACY_TOKENS_FIELD,
4604
- LEGACY_CLIENT_SECRET_FIELD,
4605
- LEGACY_REGISTRATION_TOKEN_FIELD,
4894
+ ...stamped(LEGACY_CLIENT_SECRET_FIELD),
4895
+ ...stamped(LEGACY_REGISTRATION_TOKEN_FIELD),
4606
4896
  PREREG_CLIENT_SECRET_FIELD,
4607
4897
  PREREG_REGISTRATION_TOKEN_FIELD
4608
4898
  ]);
4609
4899
  for (const issuer of Object.keys(state?.byIssuer ?? {})) {
4610
4900
  fields.add(issuerTokensField(issuer));
4611
- fields.add(issuerClientSecretField(issuer));
4612
- fields.add(issuerRegistrationTokenField(issuer));
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
+ }
4613
4907
  }
4614
4908
  return [...fields];
4615
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
+ }
4616
4936
  function clientInfoHasPlaintext(info) {
4617
4937
  return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
4618
4938
  }
@@ -4653,6 +4973,44 @@ function coveredLocations(location, byLocation) {
4653
4973
  ([recorded]) => recorded === location || location === KEYRING_LOCATION && recorded.startsWith(FILE_LOCATION_PREFIX)
4654
4974
  ).map(([recorded, keys]) => ({ location: recorded, keys }));
4655
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
+ }
4656
5014
  var warnedLedgerFailures = /* @__PURE__ */ new Set();
4657
5015
  function warnLedgerFailure(what, error) {
4658
5016
  const reason = error instanceof Error ? error.message : String(error);
@@ -4666,6 +5024,19 @@ function warnLedgerFailure(what, error) {
4666
5024
  function isRecord3(value) {
4667
5025
  return typeof value === "object" && value !== null && !Array.isArray(value);
4668
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
+ }
4669
5040
  function stringsOf(value) {
4670
5041
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
4671
5042
  }
@@ -4687,7 +5058,9 @@ function parseLedger(raw) {
4687
5058
  if (!isRecord3(keys)) continue;
4688
5059
  byLocation.set(location, {
4689
5060
  servers: new Set(stringsOf(keys.servers)),
4690
- 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 }
4691
5064
  });
4692
5065
  }
4693
5066
  if (byLocation.size > 0) ledger.set(namespace, byLocation);
@@ -4701,7 +5074,19 @@ function serializeLedger(ledger) {
4701
5074
  Object.fromEntries(
4702
5075
  [...byLocation].map(([location, keys]) => [
4703
5076
  location,
4704
- { servers: [...keys.servers], idpSessions: [...keys.idpSessions] }
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
+ }
4705
5090
  ])
4706
5091
  )
4707
5092
  ])
@@ -4713,7 +5098,7 @@ async function writeLedger(stateFilePath, ledger) {
4713
5098
  if (ledger.size === 0) await deleteStoreFile(path6);
4714
5099
  else await writeStoreFile(path6, serializeLedger(ledger));
4715
5100
  }
4716
- async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions) {
5101
+ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions, owners = /* @__PURE__ */ new Map()) {
4717
5102
  try {
4718
5103
  const ledger = parseLedger(
4719
5104
  await readStoreFile(namespaceLedgerPath(stateFilePath))
@@ -4727,7 +5112,7 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
4727
5112
  let keys = byLocation.get(location);
4728
5113
  let changed = false;
4729
5114
  if (!keys) {
4730
- keys = { servers: /* @__PURE__ */ new Set(), idpSessions: /* @__PURE__ */ new Set() };
5115
+ keys = emptyKeys();
4731
5116
  byLocation.set(location, keys);
4732
5117
  changed = true;
4733
5118
  }
@@ -4741,11 +5126,93 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
4741
5126
  keys.idpSessions.add(issuer);
4742
5127
  changed = true;
4743
5128
  }
5129
+ if (mergeOwners(keys.owners, owners)) changed = true;
4744
5130
  if (changed) await writeLedger(stateFilePath, ledger);
4745
5131
  } catch (error) {
4746
5132
  warnLedgerFailure("update", error);
4747
5133
  }
4748
5134
  }
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
+ }
4749
5216
  async function recordedNamespaces(stateFilePath) {
4750
5217
  try {
4751
5218
  const ledger = parseLedger(
@@ -4757,7 +5224,7 @@ async function recordedNamespaces(stateFilePath) {
4757
5224
  return [];
4758
5225
  }
4759
5226
  }
4760
- async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
5227
+ async function purgeSupersededNamespaces(stateFilePath, secretStore, current, owners = /* @__PURE__ */ new Map()) {
4761
5228
  let ledger;
4762
5229
  let location;
4763
5230
  try {
@@ -4770,8 +5237,22 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
4770
5237
  return;
4771
5238
  }
4772
5239
  let changed = false;
4773
- for (const [namespace, byLocation] of ledger) {
5240
+ for (const [namespace, byLocation] of [...ledger].reverse()) {
4774
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
+ }
4775
5256
  for (const { location: recorded, keys } of coveredLocations(
4776
5257
  location,
4777
5258
  byLocation
@@ -4796,12 +5277,11 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
4796
5277
  if (!purged || recorded !== location) continue;
4797
5278
  byLocation.delete(recorded);
4798
5279
  if (recorded.startsWith(FILE_LOCATION_PREFIX)) {
4799
- const keyring = byLocation.get(KEYRING_LOCATION) ?? {
4800
- servers: /* @__PURE__ */ new Set(),
4801
- idpSessions: /* @__PURE__ */ new Set()
4802
- };
5280
+ const keyring = byLocation.get(KEYRING_LOCATION) ?? emptyKeys();
5281
+ addMissingOwners(keyring.owners, keys.owners);
4803
5282
  for (const url of keys.servers) keyring.servers.add(url);
4804
5283
  for (const issuer of keys.idpSessions) keyring.idpSessions.add(issuer);
5284
+ keyring.owedTo ??= keys.owedTo;
4805
5285
  byLocation.set(KEYRING_LOCATION, keyring);
4806
5286
  }
4807
5287
  changed = true;
@@ -4912,7 +5392,12 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4912
5392
  const existing = parseSecretsNamespace(raw);
4913
5393
  if (existing !== void 0) {
4914
5394
  if (locked) {
4915
- await purgeSupersededNamespaces(filePath, secretStore, existing);
5395
+ await purgeSupersededNamespaces(
5396
+ filePath,
5397
+ secretStore,
5398
+ existing,
5399
+ snapshotClientOwners(parseOAuthPersistBlob(raw))
5400
+ );
4916
5401
  }
4917
5402
  return existing;
4918
5403
  }
@@ -4932,25 +5417,33 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4932
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).`
4933
5418
  );
4934
5419
  }
5420
+ const values = await readEntrySecrets(
5421
+ snapshot,
5422
+ secretStore,
5423
+ void 0,
5424
+ await recordedNamespaces(filePath)
5425
+ );
5426
+ const owners = snapshotClientOwners(snapshot);
4935
5427
  await recordNamespaceKeys(
4936
5428
  filePath,
4937
5429
  secretStore,
4938
5430
  namespace,
4939
5431
  Object.keys(snapshot.servers),
4940
- Object.keys(snapshot.idpSessions)
5432
+ Object.keys(snapshot.idpSessions),
5433
+ writtenClientOwners(snapshot, (url) => values.servers.get(url) ?? {})
4941
5434
  );
5435
+ await markUncarriedNamespaces(filePath, secretStore, namespace);
4942
5436
  const touched = [];
4943
5437
  try {
4944
- const values = await readEntrySecrets(
4945
- snapshot,
4946
- secretStore,
4947
- void 0,
4948
- await recordedNamespaces(filePath)
4949
- );
4950
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).
4951
5441
  ...[...values.servers].map(([url, secrets]) => ({
4952
5442
  scopedId: oauthSecretServerId(url, namespace),
4953
- secrets
5443
+ secrets: restampClientCredentials(
5444
+ getOwnEntry(snapshot.servers, url) ?? {},
5445
+ secrets
5446
+ )
4954
5447
  })),
4955
5448
  ...[...values.idpSessions].map(([issuer, secrets]) => ({
4956
5449
  scopedId: oauthIdpSecretServerId(issuer, namespace),
@@ -4968,7 +5461,7 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4968
5461
  await restoreSecretFields(secretStore, touched, warnRestoreFailure);
4969
5462
  throw error;
4970
5463
  }
4971
- await purgeSupersededNamespaces(filePath, secretStore, namespace);
5464
+ await purgeSupersededNamespaces(filePath, secretStore, namespace, owners);
4972
5465
  const legacyIds = [
4973
5466
  ...Object.keys(snapshot.servers).map((url) => oauthSecretServerId(url)),
4974
5467
  ...Object.keys(snapshot.idpSessions).map(
@@ -5145,7 +5638,22 @@ async function writeOAuthSectionsFrom(filePath, resolveSnapshot, sections, secre
5145
5638
  secretStore,
5146
5639
  namespace,
5147
5640
  Object.keys(merged.servers),
5148
- 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
+ )
5149
5657
  );
5150
5658
  for (const url of effective.servers ?? []) {
5151
5659
  const serverId = oauthSecretServerId(url, namespace);
@@ -5333,7 +5841,29 @@ function fillMissingSecrets(target, source) {
5333
5841
  if (!Object.hasOwn(target, field)) setOwnEntry(target, field, value);
5334
5842
  }
5335
5843
  }
5336
- async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks) {
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 = []) {
5337
5867
  const servers = /* @__PURE__ */ new Map();
5338
5868
  const idpSessions = /* @__PURE__ */ new Map();
5339
5869
  for (const url of Object.keys(snapshot.servers)) servers.set(url, {});
@@ -5354,14 +5884,49 @@ async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks) {
5354
5884
  );
5355
5885
  }
5356
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
+ }
5357
5921
  return { servers, idpSessions };
5358
5922
  }
5359
- async function joinSnapshot(snapshot, secretStore, namespace, fallbacks) {
5923
+ async function joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries = []) {
5360
5924
  const values = await readEntrySecrets(
5361
5925
  snapshot,
5362
5926
  secretStore,
5363
5927
  namespace,
5364
- fallbacks
5928
+ fallbacks,
5929
+ carries
5365
5930
  );
5366
5931
  return {
5367
5932
  servers: Object.fromEntries(
@@ -5389,7 +5954,8 @@ async function migratePlaintextSecrets(filePath, secretStore) {
5389
5954
  secretStore,
5390
5955
  namespace,
5391
5956
  Object.keys(fresh.servers),
5392
- Object.keys(fresh.idpSessions)
5957
+ Object.keys(fresh.idpSessions),
5958
+ writtenClientOwners(fresh, splitSecretsOf)
5393
5959
  );
5394
5960
  }
5395
5961
  const migrateEntrySecrets = async (serverId, secrets) => {
@@ -5442,7 +6008,8 @@ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
5442
6008
  }
5443
6009
  const namespace = parseSecretsNamespace(raw);
5444
6010
  const fallbacks = namespace === void 0 ? await recordedNamespaces(filePath) : [];
5445
- return joinSnapshot(snapshot, secretStore, namespace, fallbacks);
6011
+ const carries = namespace === void 0 ? [] : await pendingCarries(filePath, secretStore, namespace);
6012
+ return joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries);
5446
6013
  });
5447
6014
  }
5448
6015
  async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
@@ -5682,6 +6249,8 @@ var RemoteSession = class _RemoteSession {
5682
6249
  requestWaits = /* @__PURE__ */ new Map();
5683
6250
  authProviderHandle = null;
5684
6251
  appliedProtocolVersion;
6252
+ /** Each upstream `429`'s `Retry-After`, by the JSON-RPC id that drew it (#2442). */
6253
+ rateLimits = new RateLimitLedger();
5685
6254
  /** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
5686
6255
  static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
5687
6256
  constructor(sessionId) {
@@ -5893,6 +6462,7 @@ var RemoteSession = class _RemoteSession {
5893
6462
  this.pushEvent({ type: "message", data: message });
5894
6463
  }
5895
6464
  onFetchRequest(entry) {
6465
+ this.rateLimits.record(entry);
5896
6466
  this.pushEvent({
5897
6467
  type: "fetch_request",
5898
6468
  data: {
@@ -5901,6 +6471,14 @@ var RemoteSession = class _RemoteSession {
5901
6471
  }
5902
6472
  });
5903
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
+ }
5904
6482
  onFetchResponseBody(id, responseBody) {
5905
6483
  this.pushEvent({
5906
6484
  type: "fetch_request_body_update",
@@ -6344,6 +6922,17 @@ var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
6344
6922
  var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
6345
6923
  var MAX_APP_CSP_CHARS = 8 * 1024;
6346
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
+ }
6347
6936
  function isHeaderSafeValue(value) {
6348
6937
  return !/[^\t\x20-\x7e]/.test(value);
6349
6938
  }
@@ -6799,8 +7388,10 @@ function createRemoteApp(options) {
6799
7388
  });
6800
7389
  }
6801
7390
  const msg = err instanceof Error ? err.message : String(err);
6802
- if (err instanceof SdkHttpError) {
7391
+ if (err instanceof SdkHttpError2) {
6803
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;
6804
7395
  return c.json({
6805
7396
  ok: false,
6806
7397
  kind: "transport_error",
@@ -6811,7 +7402,8 @@ function createRemoteApp(options) {
6811
7402
  ...err.statusText !== void 0 && {
6812
7403
  statusText: err.statusText
6813
7404
  },
6814
- ...typeof text === "string" && { text }
7405
+ ...typeof text === "string" && { text },
7406
+ ...retryAfter !== void 0 && { retryAfter }
6815
7407
  }
6816
7408
  });
6817
7409
  }
@@ -6944,7 +7536,28 @@ function createRemoteApp(options) {
6944
7536
  const isStream = contentType?.includes("text/event-stream") || contentType?.includes("application/x-ndjson");
6945
7537
  let resBody;
6946
7538
  if (!isStream && res.body) {
6947
- resBody = await res.text();
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;
6948
7561
  } else if (isStream) {
6949
7562
  void res.body?.cancel().catch(() => {
6950
7563
  });
@@ -7757,7 +8370,7 @@ function createRemoteApp(options) {
7757
8370
  type,
7758
8371
  process.platform,
7759
8372
  homedir(),
7760
- (path6) => existsSync3(path6) ? readFileSync6(path6, "utf-8") : null
8373
+ (path6) => existsSync3(path6) ? readFileSync7(path6, "utf-8") : null
7761
8374
  );
7762
8375
  if (!result) {
7763
8376
  return c.json(
@@ -8173,6 +8786,30 @@ function handleNodeHealthRequest(req, res) {
8173
8786
  return true;
8174
8787
  }
8175
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
+
8176
8813
  // server/vite-hono-plugin.ts
8177
8814
  function honoMiddlewarePlugin(config) {
8178
8815
  let resolvedAuthToken = "";
@@ -8280,18 +8917,12 @@ function honoMiddlewarePlugin(config) {
8280
8917
  if (!pathname.startsWith("/api")) {
8281
8918
  return next();
8282
8919
  }
8283
- const url = `http://${req.headers.host}${pathname}`;
8284
- const headers = new Headers();
8285
- Object.entries(req.headers).forEach(
8286
- ([key, value]) => {
8287
- if (value) {
8288
- headers.set(
8289
- key,
8290
- Array.isArray(value) ? value.join(", ") : value
8291
- );
8292
- }
8293
- }
8920
+ const url = fetchUrlForNodeRequest(
8921
+ req.headers,
8922
+ pathname,
8923
+ webScheme(config.tls)
8294
8924
  );
8925
+ const headers = fetchHeadersFromNode(req.headers);
8295
8926
  const init = { method: req.method, headers };
8296
8927
  if (req.method !== "GET" && req.method !== "HEAD") {
8297
8928
  const chunks = [];
@@ -8308,13 +8939,17 @@ function honoMiddlewarePlugin(config) {
8308
8939
  const response = await honoApp.fetch(new Request(url, init));
8309
8940
  res.statusCode = response.status;
8310
8941
  response.headers.forEach((value, key) => {
8311
- res.setHeader(key, value);
8942
+ if (isResponseHeaderAllowed(key, req.httpVersionMajor)) {
8943
+ res.setHeader(key, value);
8944
+ }
8312
8945
  });
8313
8946
  const isSSE = response.headers.get("content-type")?.includes("text/event-stream");
8314
8947
  if (isSSE) {
8315
8948
  res.setHeader("X-Accel-Buffering", "no");
8316
8949
  res.setHeader("Cache-Control", "no-cache");
8317
- res.setHeader("Connection", "keep-alive");
8950
+ if (isResponseHeaderAllowed("connection", req.httpVersionMajor)) {
8951
+ res.setHeader("Connection", "keep-alive");
8952
+ }
8318
8953
  }
8319
8954
  if (response.body) {
8320
8955
  res.flushHeaders?.();
@@ -8667,6 +9302,10 @@ async function startViteDevServer(config) {
8667
9302
  // `config.port`, so a drifted bind would 403 every connect and CSP-block
8668
9303
  // the MCP Apps iframe while the banner advertises the unusable port.
8669
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,
8670
9309
  // Allow Vite to serve source files from the repo root (core/ lives
8671
9310
  // outside clients/web), matching `vite.config.ts`'s `server.fs.allow`.
8672
9311
  fs: {
@@ -8685,10 +9324,11 @@ async function startViteDevServer(config) {
8685
9324
  }
8686
9325
 
8687
9326
  // server/server.ts
8688
- import { readFileSync as readFileSync7 } from "fs";
9327
+ import { readFileSync as readFileSync8 } from "fs";
8689
9328
  import { join as join8, dirname as dirname8 } from "path";
8690
9329
  import { fileURLToPath as fileURLToPath5 } from "url";
8691
9330
  import { randomBytes as randomBytes4 } from "crypto";
9331
+ import { createServer as createHttpsServer } from "https";
8692
9332
  import { serve } from "@hono/node-server";
8693
9333
  import { serveStatic } from "@hono/node-server/serve-static";
8694
9334
  import { Hono as Hono2 } from "hono";
@@ -8735,12 +9375,20 @@ async function startHonoServer(config) {
8735
9375
  });
8736
9376
  const app = new Hono2();
8737
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
+ );
8738
9386
  app.use("/api/*", async (c) => {
8739
9387
  return apiApp.fetch(c.req.raw);
8740
9388
  });
8741
9389
  const serveIndexHtml = (c) => {
8742
9390
  const indexPath = join8(rootPath, "index.html");
8743
- const html = readFileSync7(indexPath, "utf-8");
9391
+ const html = readFileSync8(indexPath, "utf-8");
8744
9392
  c.header("Cache-Control", "no-store");
8745
9393
  return c.html(injectAuthToken(html, resolvedAuthToken));
8746
9394
  };
@@ -8768,7 +9416,10 @@ async function startHonoServer(config) {
8768
9416
  {
8769
9417
  fetch: app.fetch,
8770
9418
  port: config.port,
8771
- 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 } : {}
8772
9423
  },
8773
9424
  (info) => {
8774
9425
  const sandboxUrl = sandboxController.getUrl();
@@ -8786,9 +9437,7 @@ async function startHonoServer(config) {
8786
9437
  );
8787
9438
  httpServer.on("error", (err) => {
8788
9439
  if (err.message.includes("EADDRINUSE")) {
8789
- console.error(
8790
- `MCP Inspector PORT IS IN USE at http://${formatHostForUrl(config.hostname)}:${config.port}`
8791
- );
9440
+ console.error(portInUseMessage(config));
8792
9441
  process.exit(1);
8793
9442
  } else {
8794
9443
  throw err;