fastmcp 4.8.1 → 4.9.1

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.
@@ -37,6 +37,237 @@ function requireScopes(...requiredScopes) {
37
37
  var _crypto = require('crypto');
38
38
  var _zod = require('zod');
39
39
 
40
+ // src/auth/OAuthProxyStateStore.ts
41
+
42
+ var STORAGE_KEY_PREFIX = {
43
+ client: "client:",
44
+ code: "code:",
45
+ transaction: "transaction:"
46
+ };
47
+ var storedDateSchema = _zod.z.union([_zod.z.date(), _zod.z.string()]).transform((value, context) => {
48
+ const date = value instanceof Date ? value : new Date(value);
49
+ if (Number.isNaN(date.getTime())) {
50
+ context.addIssue({
51
+ code: "custom",
52
+ message: "Invalid stored date"
53
+ });
54
+ return _zod.z.NEVER;
55
+ }
56
+ return date;
57
+ });
58
+ var dcrClientMetadataSchema = _zod.z.object({
59
+ client_name: _zod.z.string().optional(),
60
+ client_uri: _zod.z.string().optional(),
61
+ contacts: _zod.z.array(_zod.z.string()).optional(),
62
+ jwks: _zod.z.record(_zod.z.string(), _zod.z.unknown()).optional(),
63
+ jwks_uri: _zod.z.string().optional(),
64
+ logo_uri: _zod.z.string().optional(),
65
+ policy_uri: _zod.z.string().optional(),
66
+ scope: _zod.z.string().optional(),
67
+ software_id: _zod.z.string().optional(),
68
+ software_version: _zod.z.string().optional(),
69
+ tos_uri: _zod.z.string().optional()
70
+ });
71
+ var proxyDcrClientStorageSchema = _zod.z.object({
72
+ callbackUrl: _zod.z.string(),
73
+ clientId: _zod.z.string(),
74
+ clientSecret: _zod.z.string().optional(),
75
+ metadata: dcrClientMetadataSchema.optional(),
76
+ redirectUris: _zod.z.array(_zod.z.string()),
77
+ registeredAt: storedDateSchema
78
+ });
79
+ var upstreamTokenSetStorageSchema = _zod.z.object({
80
+ accessToken: _zod.z.string(),
81
+ expiresIn: _zod.z.number(),
82
+ idToken: _zod.z.string().optional(),
83
+ issuedAt: storedDateSchema,
84
+ refreshExpiresIn: _zod.z.number().optional(),
85
+ refreshToken: _zod.z.string().optional(),
86
+ scope: _zod.z.array(_zod.z.string()),
87
+ tokenType: _zod.z.string()
88
+ });
89
+ var clientCodeStorageSchema = _zod.z.object({
90
+ clientId: _zod.z.string(),
91
+ code: _zod.z.string(),
92
+ codeChallenge: _zod.z.string(),
93
+ codeChallengeMethod: _zod.z.string(),
94
+ createdAt: storedDateSchema,
95
+ expiresAt: storedDateSchema,
96
+ transactionId: _zod.z.string(),
97
+ upstreamTokens: upstreamTokenSetStorageSchema,
98
+ used: _zod.z.boolean().optional()
99
+ });
100
+ var spentClientCodeStorageSchema = _zod.z.object({
101
+ expiresAt: storedDateSchema,
102
+ used: _zod.z.literal(true)
103
+ });
104
+ var oauthTransactionStorageSchema = _zod.z.object({
105
+ clientCallbackUrl: _zod.z.string(),
106
+ clientCodeChallenge: _zod.z.string(),
107
+ clientCodeChallengeMethod: _zod.z.string(),
108
+ clientId: _zod.z.string(),
109
+ consentGiven: _zod.z.boolean().optional(),
110
+ createdAt: storedDateSchema,
111
+ expiresAt: storedDateSchema,
112
+ id: _zod.z.string(),
113
+ metadata: _zod.z.record(_zod.z.string(), _zod.z.unknown()).optional(),
114
+ proxyCodeChallenge: _zod.z.string(),
115
+ proxyCodeVerifier: _zod.z.string(),
116
+ scope: _zod.z.array(_zod.z.string()),
117
+ state: _zod.z.string()
118
+ });
119
+ var OAuthProxyStateStore = class {
120
+
121
+
122
+ constructor(config) {
123
+ this.registeredClientsByClientId = config.registeredClientsByClientId;
124
+ this.tokenStorage = config.tokenStorage;
125
+ }
126
+ cacheRegisteredClient(client) {
127
+ this.registeredClientsByClientId.set(client.clientId, client);
128
+ }
129
+ /**
130
+ * Atomically consume an authorization code. At most one caller — across all
131
+ * processes sharing the storage — can receive a given code, which is what
132
+ * makes single use enforceable (RFC 6749 §4.1.2).
133
+ *
134
+ * Spent codes are taken too, so the caller must write the tombstone back
135
+ * with `markClientCodeSpent` to keep reporting the precise error on the
136
+ * attempt after this one.
137
+ */
138
+ async consumeClientCode(code) {
139
+ const stored = await this.takeFromStorage(
140
+ `${STORAGE_KEY_PREFIX.code}${code}`
141
+ );
142
+ const spent = spentClientCodeStorageSchema.safeParse(stored);
143
+ if (spent.success) {
144
+ return this.isExpired(spent.data.expiresAt) ? null : { expiresAt: spent.data.expiresAt, status: "spent" };
145
+ }
146
+ const parsed = clientCodeStorageSchema.safeParse(stored);
147
+ if (!parsed.success || this.isExpired(parsed.data.expiresAt)) {
148
+ return null;
149
+ }
150
+ return {
151
+ clientCode: parsed.data,
152
+ expiresAt: parsed.data.expiresAt,
153
+ status: "active"
154
+ };
155
+ }
156
+ /**
157
+ * Atomically consume a transaction, so a callback can only be redeemed once
158
+ * no matter which instance it lands on.
159
+ */
160
+ async consumeTransaction(transactionId) {
161
+ const stored = await this.takeFromStorage(
162
+ `${STORAGE_KEY_PREFIX.transaction}${transactionId}`
163
+ );
164
+ const parsed = oauthTransactionStorageSchema.safeParse(stored);
165
+ if (!parsed.success || this.isExpired(parsed.data.expiresAt)) {
166
+ return null;
167
+ }
168
+ return parsed.data;
169
+ }
170
+ async deleteTransaction(transactionId) {
171
+ await this.tokenStorage.delete(
172
+ `${STORAGE_KEY_PREFIX.transaction}${transactionId}`
173
+ );
174
+ }
175
+ async getRegisteredClientByClientId(clientId) {
176
+ const cached = this.registeredClientsByClientId.get(clientId);
177
+ if (cached) {
178
+ return cached;
179
+ }
180
+ const stored = await this.tokenStorage.get(
181
+ `${STORAGE_KEY_PREFIX.client}${clientId}`
182
+ );
183
+ const parsed = proxyDcrClientStorageSchema.safeParse(stored);
184
+ if (!parsed.success) {
185
+ return null;
186
+ }
187
+ this.cacheRegisteredClient(parsed.data);
188
+ return parsed.data;
189
+ }
190
+ /**
191
+ * Read a transaction without consuming it. Used by the consent screen, which
192
+ * has to hand the same transaction back to the upstream redirect.
193
+ */
194
+ async getTransaction(transactionId) {
195
+ const stored = await this.tokenStorage.get(
196
+ `${STORAGE_KEY_PREFIX.transaction}${transactionId}`
197
+ );
198
+ const parsed = oauthTransactionStorageSchema.safeParse(stored);
199
+ if (!parsed.success) {
200
+ return null;
201
+ }
202
+ if (this.isExpired(parsed.data.expiresAt)) {
203
+ await this.deleteTransaction(transactionId);
204
+ return null;
205
+ }
206
+ return parsed.data;
207
+ }
208
+ async isTransactionCallbackRegistered(transaction) {
209
+ const registeredClient = await this.getRegisteredClientByClientId(
210
+ transaction.clientId
211
+ );
212
+ return _nullishCoalesce(_optionalChain([registeredClient, 'optionalAccess', _ => _.redirectUris, 'access', _2 => _2.includes, 'call', _3 => _3(transaction.clientCallbackUrl)]), () => ( false));
213
+ }
214
+ /**
215
+ * Record that an authorization code has been redeemed, so a later attempt
216
+ * gets "already used" rather than looking like an unknown code. Keyed and
217
+ * expiring exactly like the code it replaces, so it cannot outlive it.
218
+ */
219
+ async markClientCodeSpent(code, expiresAt) {
220
+ await this.tokenStorage.save(
221
+ `${STORAGE_KEY_PREFIX.code}${code}`,
222
+ { expiresAt, used: true },
223
+ this.getTtlSeconds(expiresAt)
224
+ );
225
+ }
226
+ async saveClientCode(clientCode) {
227
+ await this.tokenStorage.save(
228
+ `${STORAGE_KEY_PREFIX.code}${clientCode.code}`,
229
+ clientCode,
230
+ this.getTtlSeconds(clientCode.expiresAt)
231
+ );
232
+ }
233
+ async saveRegisteredClient(client) {
234
+ await this.tokenStorage.save(
235
+ `${STORAGE_KEY_PREFIX.client}${client.clientId}`,
236
+ client
237
+ );
238
+ }
239
+ async saveTransaction(transaction) {
240
+ await this.tokenStorage.save(
241
+ `${STORAGE_KEY_PREFIX.transaction}${transaction.id}`,
242
+ transaction,
243
+ this.getTtlSeconds(transaction.expiresAt)
244
+ );
245
+ }
246
+ getTtlSeconds(expiresAt) {
247
+ const millisecondsUntilExpiry = expiresAt.getTime() - Date.now();
248
+ return Math.max(Math.ceil(millisecondsUntilExpiry / 1e3), 1);
249
+ }
250
+ isExpired(expiresAt) {
251
+ return expiresAt.getTime() < Date.now();
252
+ }
253
+ /**
254
+ * Prefer the storage's atomic take. Falling back to get + delete leaves a
255
+ * window in which two processes can both observe the same value, so
256
+ * multi-process deployments should implement `TokenStorage.take`.
257
+ */
258
+ async takeFromStorage(key) {
259
+ if (this.tokenStorage.take) {
260
+ return this.tokenStorage.take(key);
261
+ }
262
+ const stored = await this.tokenStorage.get(key);
263
+ if (stored === null) {
264
+ return null;
265
+ }
266
+ await this.tokenStorage.delete(key);
267
+ return stored;
268
+ }
269
+ };
270
+
40
271
  // src/auth/types.ts
41
272
  var DEFAULT_ACCESS_TOKEN_TTL = 3600;
42
273
  var DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH = 31536e3;
@@ -134,7 +365,7 @@ var ClaimsExtractor = (_class = class {
134
365
  if (this.PROTECTED_CLAIMS.has(key)) {
135
366
  continue;
136
367
  }
137
- if (_optionalChain([this, 'access', _ => _.config, 'access', _2 => _2.blockedClaims, 'optionalAccess', _3 => _3.includes, 'call', _4 => _4(key)])) {
368
+ if (_optionalChain([this, 'access', _4 => _4.config, 'access', _5 => _5.blockedClaims, 'optionalAccess', _6 => _6.includes, 'call', _7 => _7(key)])) {
138
369
  continue;
139
370
  }
140
371
  if (this.config.allowedClaims && !this.config.allowedClaims.includes(key)) {
@@ -743,20 +974,7 @@ var EncryptedTokenStorage = (_class2 = class {
743
974
  await this.backend.delete(key);
744
975
  }
745
976
  async get(key) {
746
- const encrypted = await this.backend.get(key);
747
- if (!encrypted) {
748
- return null;
749
- }
750
- try {
751
- const decrypted = await this.decrypt(
752
- encrypted,
753
- this.encryptionKey
754
- );
755
- return JSON.parse(decrypted);
756
- } catch (error) {
757
- console.error("Failed to decrypt value:", error);
758
- return null;
759
- }
977
+ return this.decryptStored(await this.backend.get(key));
760
978
  }
761
979
  async save(key, value, ttl) {
762
980
  const encrypted = await this.encrypt(
@@ -765,6 +983,21 @@ var EncryptedTokenStorage = (_class2 = class {
765
983
  );
766
984
  await this.backend.save(key, encrypted, ttl);
767
985
  }
986
+ /**
987
+ * Delegates to the backend so that the atomicity of the underlying store is
988
+ * preserved; falls back to get + delete when the backend cannot do better.
989
+ */
990
+ async take(key) {
991
+ if (this.backend.take) {
992
+ return this.decryptStored(await this.backend.take(key));
993
+ }
994
+ const value = await this.get(key);
995
+ if (value === null) {
996
+ return null;
997
+ }
998
+ await this.backend.delete(key);
999
+ return value;
1000
+ }
768
1001
  async decrypt(ciphertext, key) {
769
1002
  const parts = ciphertext.split(":");
770
1003
  if (parts.length !== 3) {
@@ -783,6 +1016,21 @@ var EncryptedTokenStorage = (_class2 = class {
783
1016
  decrypted += decipher.final("utf8");
784
1017
  return decrypted;
785
1018
  }
1019
+ async decryptStored(encrypted) {
1020
+ if (!encrypted) {
1021
+ return null;
1022
+ }
1023
+ try {
1024
+ const decrypted = await this.decrypt(
1025
+ encrypted,
1026
+ this.encryptionKey
1027
+ );
1028
+ return JSON.parse(decrypted);
1029
+ } catch (error) {
1030
+ console.error("Failed to decrypt value:", error);
1031
+ return null;
1032
+ }
1033
+ }
786
1034
  async encrypt(plaintext, key) {
787
1035
  const iv = _crypto.randomBytes.call(void 0, 16);
788
1036
  const cipher = _crypto.createCipheriv.call(void 0, this.algorithm, key, iv, {
@@ -852,6 +1100,18 @@ var MemoryTokenStorage = (_class3 = class {
852
1100
  size() {
853
1101
  return this.store.size;
854
1102
  }
1103
+ /**
1104
+ * Atomic by construction: the read and the delete happen in a single
1105
+ * synchronous block, so no other task can observe the value in between.
1106
+ */
1107
+ async take(key) {
1108
+ const entry = this.store.get(key);
1109
+ this.store.delete(key);
1110
+ if (!entry || entry.expiresAt < Date.now()) {
1111
+ return null;
1112
+ }
1113
+ return entry.value;
1114
+ }
855
1115
  }, _class3);
856
1116
 
857
1117
  // src/auth/OAuthProxy.ts
@@ -867,17 +1127,19 @@ var RESERVED_AUTHORIZATION_PARAMS = /* @__PURE__ */ new Set([
867
1127
  var OAuthProxy = (_class4 = class {
868
1128
  __init5() {this.claimsExtractor = null}
869
1129
  __init6() {this.cleanupInterval = null}
870
- __init7() {this.clientCodes = /* @__PURE__ */ new Map()}
871
1130
 
872
1131
 
873
1132
 
874
- /** Keyed by redirect_uri for defence-in-depth checks in handleCallback/handleConsent */
875
- __init8() {this.registeredClients = /* @__PURE__ */ new Map()}
876
- /** Keyed by proxy-issued client_id for authorize/token-exchange lookups */
877
- __init9() {this.registeredClientsByClientId = /* @__PURE__ */ new Map()}
1133
+ /**
1134
+ * Keyed by proxy-issued client_id for authorize/token-exchange lookups and
1135
+ * for the defence-in-depth callback checks. A registration never changes
1136
+ * after it is written, so caching it locally cannot go stale; it is also
1137
+ * persisted, so another instance can hydrate it.
1138
+ */
1139
+ __init7() {this.registeredClientsByClientId = /* @__PURE__ */ new Map()}
1140
+
878
1141
 
879
- __init10() {this.transactions = /* @__PURE__ */ new Map()}
880
- constructor(config) {;_class4.prototype.__init5.call(this);_class4.prototype.__init6.call(this);_class4.prototype.__init7.call(this);_class4.prototype.__init8.call(this);_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);
1142
+ constructor(config) {;_class4.prototype.__init5.call(this);_class4.prototype.__init6.call(this);_class4.prototype.__init7.call(this);
881
1143
  this.config = {
882
1144
  authorizationCodeTtl: DEFAULT_AUTHORIZATION_CODE_TTL,
883
1145
  consentRequired: true,
@@ -895,6 +1157,10 @@ var OAuthProxy = (_class4 = class {
895
1157
  storage = new EncryptedTokenStorage(storage, encryptionKey);
896
1158
  }
897
1159
  this.tokenStorage = storage;
1160
+ this.stateStore = new OAuthProxyStateStore({
1161
+ registeredClientsByClientId: this.registeredClientsByClientId,
1162
+ tokenStorage: this.tokenStorage
1163
+ });
898
1164
  this.consentManager = new ConsentManager(
899
1165
  config.consentSigningKey || this.generateSigningKey()
900
1166
  );
@@ -928,9 +1194,7 @@ var OAuthProxy = (_class4 = class {
928
1194
  "Only 'code' response type is supported"
929
1195
  );
930
1196
  }
931
- const registeredClient = this.registeredClientsByClientId.get(
932
- params.client_id
933
- );
1197
+ const registeredClient = await this.stateStore.getRegisteredClientByClientId(params.client_id);
934
1198
  if (!registeredClient) {
935
1199
  throw new OAuthProxyError("invalid_client", "Unknown client_id");
936
1200
  }
@@ -963,9 +1227,6 @@ var OAuthProxy = (_class4 = class {
963
1227
  clearInterval(this.cleanupInterval);
964
1228
  this.cleanupInterval = null;
965
1229
  }
966
- this.transactions.clear();
967
- this.clientCodes.clear();
968
- this.registeredClients.clear();
969
1230
  this.registeredClientsByClientId.clear();
970
1231
  }
971
1232
  /**
@@ -978,16 +1239,25 @@ var OAuthProxy = (_class4 = class {
978
1239
  "Only authorization_code grant type is supported"
979
1240
  );
980
1241
  }
981
- if (!this.registeredClientsByClientId.has(request.client_id)) {
1242
+ const registeredClient = await this.stateStore.getRegisteredClientByClientId(request.client_id);
1243
+ if (!registeredClient) {
982
1244
  throw new OAuthProxyError("invalid_client", "Unknown client_id");
983
1245
  }
984
- const clientCode = this.clientCodes.get(request.code);
985
- if (!clientCode) {
1246
+ const consumed = await this.stateStore.consumeClientCode(request.code);
1247
+ if (!consumed) {
986
1248
  throw new OAuthProxyError(
987
1249
  "invalid_grant",
988
1250
  "Invalid or expired authorization code"
989
1251
  );
990
1252
  }
1253
+ await this.stateStore.markClientCodeSpent(request.code, consumed.expiresAt);
1254
+ if (consumed.status === "spent") {
1255
+ throw new OAuthProxyError(
1256
+ "invalid_grant",
1257
+ "Authorization code already used"
1258
+ );
1259
+ }
1260
+ const clientCode = consumed.clientCode;
991
1261
  if (clientCode.clientId !== request.client_id) {
992
1262
  throw new OAuthProxyError("invalid_client", "Client ID mismatch");
993
1263
  }
@@ -1007,14 +1277,6 @@ var OAuthProxy = (_class4 = class {
1007
1277
  throw new OAuthProxyError("invalid_grant", "Invalid PKCE verifier");
1008
1278
  }
1009
1279
  }
1010
- if (clientCode.used) {
1011
- throw new OAuthProxyError(
1012
- "invalid_grant",
1013
- "Authorization code already used"
1014
- );
1015
- }
1016
- clientCode.used = true;
1017
- this.clientCodes.set(request.code, clientCode);
1018
1280
  if (this.config.enableTokenSwap && this.jwtIssuer) {
1019
1281
  return await this.issueSwappedTokens(
1020
1282
  clientCode.clientId,
@@ -1090,23 +1352,21 @@ var OAuthProxy = (_class4 = class {
1090
1352
  "Missing code or state parameter"
1091
1353
  );
1092
1354
  }
1093
- const transaction = this.transactions.get(state);
1355
+ const transaction = await this.stateStore.consumeTransaction(state);
1094
1356
  if (!transaction) {
1095
1357
  throw new OAuthProxyError("invalid_request", "Invalid or expired state");
1096
1358
  }
1097
- if (!this.registeredClients.has(transaction.clientCallbackUrl)) {
1098
- this.transactions.delete(state);
1359
+ if (!await this.stateStore.isTransactionCallbackRegistered(transaction)) {
1099
1360
  throw new OAuthProxyError(
1100
1361
  "invalid_request",
1101
1362
  "Transaction callback URL is not registered"
1102
1363
  );
1103
1364
  }
1104
1365
  const upstreamTokens = await this.exchangeUpstreamCode(code, transaction);
1105
- const clientCode = this.generateAuthorizationCode(
1366
+ const clientCode = await this.generateAuthorizationCode(
1106
1367
  transaction,
1107
1368
  upstreamTokens
1108
1369
  );
1109
- this.transactions.delete(state);
1110
1370
  const redirectUrl = new URL(transaction.clientCallbackUrl);
1111
1371
  redirectUrl.searchParams.set("code", clientCode);
1112
1372
  redirectUrl.searchParams.set("state", transaction.state);
@@ -1124,10 +1384,16 @@ var OAuthProxy = (_class4 = class {
1124
1384
  const formData = await request.formData();
1125
1385
  const transactionId = formData.get("transaction_id");
1126
1386
  const action = formData.get("action");
1127
- if (!transactionId) {
1387
+ if (typeof transactionId !== "string" || !transactionId) {
1128
1388
  throw new OAuthProxyError("invalid_request", "Missing transaction_id");
1129
1389
  }
1130
- const transaction = this.transactions.get(transactionId);
1390
+ if (action !== "approve" && action !== "deny") {
1391
+ throw new OAuthProxyError(
1392
+ "invalid_request",
1393
+ "action must be 'approve' or 'deny'"
1394
+ );
1395
+ }
1396
+ const transaction = await this.stateStore.getTransaction(transactionId);
1131
1397
  if (!transaction) {
1132
1398
  throw new OAuthProxyError(
1133
1399
  "invalid_request",
@@ -1135,8 +1401,8 @@ var OAuthProxy = (_class4 = class {
1135
1401
  );
1136
1402
  }
1137
1403
  if (action === "deny") {
1138
- this.transactions.delete(transactionId);
1139
- if (!this.registeredClients.has(transaction.clientCallbackUrl)) {
1404
+ await this.stateStore.deleteTransaction(transactionId);
1405
+ if (!await this.stateStore.isTransactionCallbackRegistered(transaction)) {
1140
1406
  throw new OAuthProxyError(
1141
1407
  "invalid_request",
1142
1408
  "Transaction callback URL is not registered"
@@ -1156,9 +1422,12 @@ var OAuthProxy = (_class4 = class {
1156
1422
  status: 302
1157
1423
  });
1158
1424
  }
1159
- transaction.consentGiven = true;
1160
- this.transactions.set(transactionId, transaction);
1161
- return this.redirectToUpstream(transaction);
1425
+ const approvedTransaction = {
1426
+ ...transaction,
1427
+ consentGiven: true
1428
+ };
1429
+ await this.stateStore.saveTransaction(approvedTransaction);
1430
+ return this.redirectToUpstream(approvedTransaction);
1162
1431
  }
1163
1432
  /**
1164
1433
  * Load upstream tokens from a FastMCP JWT
@@ -1168,7 +1437,7 @@ var OAuthProxy = (_class4 = class {
1168
1437
  return null;
1169
1438
  }
1170
1439
  const result = await this.jwtIssuer.verify(fastmcpToken);
1171
- if (!result.valid || !_optionalChain([result, 'access', _5 => _5.claims, 'optionalAccess', _6 => _6.jti])) {
1440
+ if (!result.valid || !_optionalChain([result, 'access', _8 => _8.claims, 'optionalAccess', _9 => _9.jti])) {
1172
1441
  return null;
1173
1442
  }
1174
1443
  const mapping = await this.tokenStorage.get(
@@ -1222,10 +1491,8 @@ var OAuthProxy = (_class4 = class {
1222
1491
  redirectUris: request.redirect_uris,
1223
1492
  registeredAt: /* @__PURE__ */ new Date()
1224
1493
  };
1225
- this.registeredClientsByClientId.set(proxyClientId, client);
1226
- for (const uri of request.redirect_uris) {
1227
- this.registeredClients.set(uri, client);
1228
- }
1494
+ this.stateStore.cacheRegisteredClient(client);
1495
+ await this.stateStore.saveRegisteredClient(client);
1229
1496
  const response = {
1230
1497
  client_id: proxyClientId,
1231
1498
  client_id_issued_at: Math.floor(Date.now() / 1e3),
@@ -1269,21 +1536,15 @@ var OAuthProxy = (_class4 = class {
1269
1536
  }
1270
1537
  }
1271
1538
  /**
1272
- * Clean up expired transactions and codes
1539
+ * Periodic maintenance hook. Expiry is enforced by the token storage.
1273
1540
  */
1274
1541
  cleanup() {
1275
- const now = Date.now();
1276
- for (const [id, transaction] of this.transactions.entries()) {
1277
- if (transaction.expiresAt.getTime() < now) {
1278
- this.transactions.delete(id);
1279
- }
1280
- }
1281
- for (const [code, clientCode] of this.clientCodes.entries()) {
1282
- if (clientCode.expiresAt.getTime() < now) {
1283
- this.clientCodes.delete(code);
1284
- }
1285
- }
1286
- void this.tokenStorage.cleanup();
1542
+ void this.tokenStorage.cleanup().catch((error) => {
1543
+ console.error(
1544
+ "[FastMCP] OAuth proxy token storage cleanup failed:",
1545
+ error instanceof Error ? error.message : String(error)
1546
+ );
1547
+ });
1287
1548
  }
1288
1549
  /**
1289
1550
  * Create a new OAuth transaction
@@ -1306,7 +1567,7 @@ var OAuthProxy = (_class4 = class {
1306
1567
  scope: params.scope ? params.scope.split(" ") : this.config.scopes || [],
1307
1568
  state: params.state || this.generateId()
1308
1569
  };
1309
- this.transactions.set(transactionId, transaction);
1570
+ await this.stateStore.saveTransaction(transaction);
1310
1571
  return transaction;
1311
1572
  }
1312
1573
  /**
@@ -1367,7 +1628,7 @@ var OAuthProxy = (_class4 = class {
1367
1628
  throw new Error("JWT issuer not initialized");
1368
1629
  }
1369
1630
  const result = await this.jwtIssuer.verify(token);
1370
- if (!result.valid || !_optionalChain([result, 'access', _7 => _7.claims, 'optionalAccess', _8 => _8.jti])) {
1631
+ if (!result.valid || !_optionalChain([result, 'access', _10 => _10.claims, 'optionalAccess', _11 => _11.jti])) {
1371
1632
  throw new Error("Failed to extract JTI from token");
1372
1633
  }
1373
1634
  return result.claims.jti;
@@ -1406,7 +1667,7 @@ var OAuthProxy = (_class4 = class {
1406
1667
  /**
1407
1668
  * Generate authorization code for client
1408
1669
  */
1409
- generateAuthorizationCode(transaction, upstreamTokens) {
1670
+ async generateAuthorizationCode(transaction, upstreamTokens) {
1410
1671
  const code = this.generateId();
1411
1672
  const clientCode = {
1412
1673
  clientId: transaction.clientId,
@@ -1420,7 +1681,7 @@ var OAuthProxy = (_class4 = class {
1420
1681
  transactionId: transaction.id,
1421
1682
  upstreamTokens
1422
1683
  };
1423
- this.clientCodes.set(code, clientCode);
1684
+ await this.stateStore.saveClientCode(clientCode);
1424
1685
  return code;
1425
1686
  }
1426
1687
  /**
@@ -1514,7 +1775,7 @@ var OAuthProxy = (_class4 = class {
1514
1775
  "Invalid or expired refresh token"
1515
1776
  );
1516
1777
  }
1517
- const jti = _optionalChain([verifyResult, 'access', _9 => _9.claims, 'optionalAccess', _10 => _10.jti]);
1778
+ const jti = _optionalChain([verifyResult, 'access', _12 => _12.claims, 'optionalAccess', _13 => _13.jti]);
1518
1779
  if (!jti) {
1519
1780
  throw new OAuthProxyError("invalid_grant", "Refresh token missing JTI");
1520
1781
  }
@@ -1916,7 +2177,7 @@ var AuthProvider = class {
1916
2177
  if (!request) {
1917
2178
  return void 0;
1918
2179
  }
1919
- const authHeader = _optionalChain([request, 'access', _11 => _11.headers, 'optionalAccess', _12 => _12.authorization]);
2180
+ const authHeader = _optionalChain([request, 'access', _14 => _14.headers, 'optionalAccess', _15 => _15.authorization]);
1920
2181
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
1921
2182
  return void 0;
1922
2183
  }
@@ -2105,13 +2366,22 @@ var OAuthProvider = class extends AuthProvider {
2105
2366
  };
2106
2367
 
2107
2368
  // src/auth/utils/diskStore.ts
2369
+
2370
+
2371
+
2372
+
2373
+
2374
+
2375
+
2376
+
2377
+
2108
2378
  var _promises = require('fs/promises');
2109
2379
  var _path = require('path');
2110
2380
  var DiskStore = (_class5 = class {
2111
- __init11() {this.cleanupInterval = null}
2381
+ __init8() {this.cleanupInterval = null}
2112
2382
 
2113
2383
 
2114
- constructor(options) {;_class5.prototype.__init11.call(this);
2384
+ constructor(options) {;_class5.prototype.__init8.call(this);
2115
2385
  this.directory = options.directory;
2116
2386
  this.fileExtension = options.fileExtension || ".json";
2117
2387
  void this.ensureDirectory();
@@ -2224,6 +2494,36 @@ var DiskStore = (_class5 = class {
2224
2494
  return 0;
2225
2495
  }
2226
2496
  }
2497
+ /**
2498
+ * Atomically retrieve a value and delete it.
2499
+ *
2500
+ * rename() is atomic on POSIX filesystems, so when several processes race
2501
+ * for the same key exactly one rename succeeds and the losers see ENOENT.
2502
+ * That is what makes single-use authorization codes safe when more than one
2503
+ * process shares the directory.
2504
+ */
2505
+ async take(key) {
2506
+ const filePath = this.getFilePath(key);
2507
+ const claimPath = `${filePath}.${_crypto.randomUUID.call(void 0, )}.claim${this.fileExtension}`;
2508
+ try {
2509
+ await _promises.rename.call(void 0, filePath, claimPath);
2510
+ } catch (error) {
2511
+ if (error.code !== "ENOENT") {
2512
+ console.error(`Failed to take key ${key}:`, error);
2513
+ }
2514
+ return null;
2515
+ }
2516
+ try {
2517
+ const content = await _promises.readFile.call(void 0, claimPath, "utf-8");
2518
+ const entry = JSON.parse(content);
2519
+ return entry.expiresAt < Date.now() ? null : entry.value;
2520
+ } catch (error) {
2521
+ console.error(`Failed to read key ${key}:`, error);
2522
+ return null;
2523
+ } finally {
2524
+ await _promises.rm.call(void 0, claimPath, { force: true });
2525
+ }
2526
+ }
2227
2527
  /**
2228
2528
  * Ensure storage directory exists
2229
2529
  */
@@ -2254,9 +2554,9 @@ var DiskStore = (_class5 = class {
2254
2554
  var JWKSVerifier = (_class6 = class {
2255
2555
 
2256
2556
 
2257
- __init12() {this.joseLoaded = false}
2557
+ __init9() {this.joseLoaded = false}
2258
2558
 
2259
- constructor(config) {;_class6.prototype.__init12.call(this);
2559
+ constructor(config) {;_class6.prototype.__init9.call(this);
2260
2560
  this.config = {
2261
2561
  cacheDuration: 36e5,
2262
2562
  // 1 hour
@@ -2414,4 +2714,4 @@ Original error: ${error.message}`
2414
2714
 
2415
2715
 
2416
2716
  exports.getAuthSession = getAuthSession; exports.requireAll = requireAll; exports.requireAny = requireAny; exports.requireAuth = requireAuth; exports.requireRole = requireRole; exports.requireScopes = requireScopes; exports.DEFAULT_ACCESS_TOKEN_TTL = DEFAULT_ACCESS_TOKEN_TTL; exports.DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH = DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH; exports.DEFAULT_REFRESH_TOKEN_TTL = DEFAULT_REFRESH_TOKEN_TTL; exports.DEFAULT_AUTHORIZATION_CODE_TTL = DEFAULT_AUTHORIZATION_CODE_TTL; exports.DEFAULT_TRANSACTION_TTL = DEFAULT_TRANSACTION_TTL; exports.ConsentManager = ConsentManager; exports.JWTIssuer = JWTIssuer; exports.PKCEUtils = PKCEUtils; exports.EncryptedTokenStorage = EncryptedTokenStorage; exports.MemoryTokenStorage = MemoryTokenStorage; exports.OAuthProxy = OAuthProxy; exports.OAuthProxyError = OAuthProxyError; exports.AuthProvider = AuthProvider; exports.AzureProvider = AzureProvider; exports.GitHubProvider = GitHubProvider; exports.GoogleProvider = GoogleProvider; exports.OAuthProvider = OAuthProvider; exports.DiskStore = DiskStore; exports.JWKSVerifier = JWKSVerifier;
2417
- //# sourceMappingURL=chunk-IX3HKAX4.cjs.map
2717
+ //# sourceMappingURL=chunk-DZYT6QAH.cjs.map