fastmcp 4.12.4 → 4.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1802,51 +1802,62 @@ var OAuthProxy = class {
1802
1802
  if (!jti) {
1803
1803
  throw new OAuthProxyError("invalid_grant", "Refresh token missing JTI");
1804
1804
  }
1805
- const mapping = await this.tokenStorage.get(`mapping:${jti}`);
1805
+ const mapping = await this.takeMapping(jti);
1806
1806
  if (!mapping) {
1807
1807
  throw new OAuthProxyError(
1808
1808
  "invalid_grant",
1809
1809
  "Refresh token already used or expired"
1810
1810
  );
1811
1811
  }
1812
- const upstreamTokens = await this.tokenStorage.get(
1813
- `upstream:${mapping.upstreamTokenKey}`
1814
- );
1815
- if (!upstreamTokens) {
1816
- throw new OAuthProxyError(
1817
- "invalid_grant",
1818
- "Upstream tokens not found or expired"
1812
+ try {
1813
+ const upstreamTokens = await this.tokenStorage.get(
1814
+ `upstream:${mapping.upstreamTokenKey}`
1819
1815
  );
1820
- }
1821
- if (!upstreamTokens.refreshToken) {
1822
- throw new OAuthProxyError(
1823
- "invalid_grant",
1824
- "No upstream refresh token available"
1816
+ if (!upstreamTokens) {
1817
+ throw new OAuthProxyError(
1818
+ "invalid_grant",
1819
+ "Upstream tokens not found or expired"
1820
+ );
1821
+ }
1822
+ if (!upstreamTokens.refreshToken) {
1823
+ throw new OAuthProxyError(
1824
+ "invalid_grant",
1825
+ "No upstream refresh token available"
1826
+ );
1827
+ }
1828
+ const refreshedUpstreamTokens = await this.refreshUpstreamTokens(
1829
+ upstreamTokens.refreshToken,
1830
+ request.scope
1825
1831
  );
1832
+ if (refreshedUpstreamTokens.scope.length === 0) {
1833
+ refreshedUpstreamTokens.scope = upstreamTokens.scope;
1834
+ }
1835
+ const refreshTokenTtl = refreshedUpstreamTokens.refreshExpiresIn ?? this.config.refreshTokenTtl ?? DEFAULT_REFRESH_TOKEN_TTL;
1836
+ const accessTokenTtl = this.calculateAccessTokenTtl(
1837
+ refreshedUpstreamTokens
1838
+ );
1839
+ const upstreamStorageTtl = Math.max(accessTokenTtl, refreshTokenTtl, 1);
1840
+ await this.tokenStorage.save(
1841
+ `upstream:${mapping.upstreamTokenKey}`,
1842
+ refreshedUpstreamTokens,
1843
+ upstreamStorageTtl
1844
+ );
1845
+ return await this.issueSwappedTokensForRefresh(
1846
+ mapping.clientId,
1847
+ refreshedUpstreamTokens,
1848
+ mapping.upstreamTokenKey
1849
+ );
1850
+ } catch (error) {
1851
+ try {
1852
+ await this.restoreMapping(jti, mapping);
1853
+ } catch (restoreError) {
1854
+ console.error(
1855
+ `Failed to restore refresh token mapping ${jti}:`,
1856
+ restoreError
1857
+ );
1858
+ }
1859
+ throw error;
1826
1860
  }
1827
- const refreshedUpstreamTokens = await this.refreshUpstreamTokens(
1828
- upstreamTokens.refreshToken,
1829
- request.scope
1830
- );
1831
- if (refreshedUpstreamTokens.scope.length === 0) {
1832
- refreshedUpstreamTokens.scope = upstreamTokens.scope;
1833
- }
1834
- const refreshTokenTtl = refreshedUpstreamTokens.refreshExpiresIn ?? this.config.refreshTokenTtl ?? DEFAULT_REFRESH_TOKEN_TTL;
1835
- const accessTokenTtl = this.calculateAccessTokenTtl(
1836
- refreshedUpstreamTokens
1837
- );
1838
- const upstreamStorageTtl = Math.max(accessTokenTtl, refreshTokenTtl, 1);
1839
- await this.tokenStorage.save(
1840
- `upstream:${mapping.upstreamTokenKey}`,
1841
- refreshedUpstreamTokens,
1842
- upstreamStorageTtl
1843
- );
1844
- return await this.issueSwappedTokensForRefresh(
1845
- mapping.clientId,
1846
- refreshedUpstreamTokens,
1847
- mapping.upstreamTokenKey,
1848
- jti
1849
- );
1850
1861
  }
1851
1862
  /**
1852
1863
  * Issue swapped tokens (JWT pattern)
@@ -1927,11 +1938,10 @@ var OAuthProxy = class {
1927
1938
  /**
1928
1939
  * Issue swapped tokens for refresh flow
1929
1940
  */
1930
- async issueSwappedTokensForRefresh(clientId, upstreamTokens, upstreamTokenKey, oldJti) {
1941
+ async issueSwappedTokensForRefresh(clientId, upstreamTokens, upstreamTokenKey) {
1931
1942
  if (!this.jwtIssuer) {
1932
1943
  throw new Error("JWT issuer not initialized");
1933
1944
  }
1934
- await this.tokenStorage.delete(`mapping:${oldJti}`);
1935
1945
  const customClaims = await this.extractUpstreamClaims(upstreamTokens);
1936
1946
  const accessTokenTtl = this.calculateAccessTokenTtl(upstreamTokens);
1937
1947
  const refreshTokenTtl = upstreamTokens.refreshToken ? upstreamTokens.refreshExpiresIn ?? this.config.refreshTokenTtl ?? DEFAULT_REFRESH_TOKEN_TTL : 0;
@@ -2108,6 +2118,29 @@ var OAuthProxy = class {
2108
2118
  tokenType: tokens.token_type || "Bearer"
2109
2119
  };
2110
2120
  }
2121
+ /**
2122
+ * Put a claimed refresh-token mapping back after a rotation failed, keeping
2123
+ * whatever lifetime it had left.
2124
+ *
2125
+ * A mapping that is already expired, or that carries no usable expiry, is
2126
+ * dropped instead. Inventing a TTL here would extend the lifetime of a
2127
+ * credential in a security path, and every mapping this proxy writes records
2128
+ * `expiresAt`.
2129
+ */
2130
+ async restoreMapping(jti, mapping) {
2131
+ if (!mapping.expiresAt) {
2132
+ return;
2133
+ }
2134
+ const expiresAt = new Date(mapping.expiresAt).getTime();
2135
+ if (Number.isNaN(expiresAt)) {
2136
+ return;
2137
+ }
2138
+ const ttl = Math.ceil((expiresAt - Date.now()) / 1e3);
2139
+ if (ttl <= 0) {
2140
+ return;
2141
+ }
2142
+ await this.tokenStorage.save(`mapping:${jti}`, mapping, ttl);
2143
+ }
2111
2144
  /**
2112
2145
  * Start periodic cleanup of expired transactions and codes
2113
2146
  */
@@ -2116,6 +2149,24 @@ var OAuthProxy = class {
2116
2149
  this.cleanup();
2117
2150
  }, 6e4);
2118
2151
  }
2152
+ /**
2153
+ * Atomically claim a token mapping, mirroring `OAuthProxyStateStore`: at most
2154
+ * one caller — across all processes sharing the storage — can receive a given
2155
+ * mapping, which is what makes single use enforceable. Falls back to a
2156
+ * non-atomic get + delete for storages that do not implement `take`.
2157
+ */
2158
+ async takeMapping(jti) {
2159
+ const key = `mapping:${jti}`;
2160
+ if (this.tokenStorage.take) {
2161
+ return await this.tokenStorage.take(key);
2162
+ }
2163
+ const stored = await this.tokenStorage.get(key);
2164
+ if (stored === null) {
2165
+ return null;
2166
+ }
2167
+ await this.tokenStorage.delete(key);
2168
+ return stored;
2169
+ }
2119
2170
  /**
2120
2171
  * Validate a redirect URI against the configured allow-list.
2121
2172
  *
@@ -2733,4 +2784,4 @@ export {
2733
2784
  DiskStore,
2734
2785
  JWKSVerifier
2735
2786
  };
2736
- //# sourceMappingURL=chunk-2ZFQSPXZ.js.map
2787
+ //# sourceMappingURL=chunk-SZ4XCW5X.js.map