toolcraft 0.0.310 → 0.0.312
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/composition.json +3 -3
- package/dist/composition.json +3 -3
- package/node_modules/auth-store/README.md +20 -0
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +6 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +15 -0
- package/node_modules/auth-store/dist/index.d.ts +1 -0
- package/node_modules/auth-store/dist/keychain-store.d.ts +8 -0
- package/node_modules/auth-store/dist/keychain-store.js +13 -0
- package/node_modules/auth-store/dist/transaction-lock.d.ts +24 -0
- package/node_modules/auth-store/dist/transaction-lock.js +152 -0
- package/node_modules/auth-store/dist/types.d.ts +2 -0
- package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/README.md +30 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +23 -2
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +176 -182
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/session-transaction.d.ts +6 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/session-transaction.js +42 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +3 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.js +7 -3
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.d.ts +9 -0
- package/node_modules/tiny-http-mcp-server/package.json +1 -1
- package/node_modules/tiny-mcp-client/dist/index.d.ts +35 -0
- package/node_modules/tiny-mcp-client/dist/index.js +486 -249
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
|
@@ -7,6 +7,7 @@ import { createAuthorizationState } from "./authorization-state.js";
|
|
|
7
7
|
import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
|
|
8
8
|
import { exchangeAuthorizationCode, OAuthError, refreshAccessToken, isRetryableOAuthError, readOAuthJsonObjectResponse } from "./token-endpoint.js";
|
|
9
9
|
import { canonicalizeResourceIndicator } from "../resource-indicator.js";
|
|
10
|
+
import { withOAuthSessionTransaction } from "./session-transaction.js";
|
|
10
11
|
const MAX_JS_DATE_MS = 8_640_000_000_000_000;
|
|
11
12
|
export function createOAuthClientProvider(options) {
|
|
12
13
|
if (isProviderOptions(options)) {
|
|
@@ -20,8 +21,6 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
20
21
|
const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore);
|
|
21
22
|
const now = options.now ?? Date.now;
|
|
22
23
|
const registeredClients = new Map();
|
|
23
|
-
const refreshPromises = new Map();
|
|
24
|
-
const authorizationPromises = new Map();
|
|
25
24
|
if (options.initialGrant !== undefined) {
|
|
26
25
|
let resource;
|
|
27
26
|
try {
|
|
@@ -115,205 +114,196 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
115
114
|
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
116
115
|
signal?.throwIfAborted();
|
|
117
116
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
123
|
-
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
124
|
-
}
|
|
125
|
-
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
|
|
126
|
-
|| getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
|
|
127
|
-
|| (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
|
|
128
|
-
await clearSession(canonicalResource);
|
|
129
|
-
session = null;
|
|
130
|
-
}
|
|
131
|
-
if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
|
|
132
|
-
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
133
|
-
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
134
|
-
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
135
|
-
client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
|
|
136
|
-
await saveSession(canonicalResource, session);
|
|
137
|
-
initialGrantConsumed = true;
|
|
117
|
+
return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
|
|
118
|
+
let session = await loadSession(canonicalResource);
|
|
119
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
120
|
+
initialGrantConsumed = true;
|
|
138
121
|
signal?.throwIfAborted();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
122
|
+
if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
123
|
+
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
124
|
+
}
|
|
125
|
+
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
|
|
126
|
+
|| getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
|
|
127
|
+
|| (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
|
|
128
|
+
await clearSession(canonicalResource);
|
|
129
|
+
session = null;
|
|
130
|
+
}
|
|
131
|
+
if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
|
|
132
|
+
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
133
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
134
|
+
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
135
|
+
client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
|
|
136
|
+
await saveSession(canonicalResource, session);
|
|
137
|
+
initialGrantConsumed = true;
|
|
138
|
+
signal?.throwIfAborted();
|
|
139
|
+
}
|
|
140
|
+
if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
141
|
+
forceRefresh = false;
|
|
142
|
+
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
143
|
+
if (session?.refreshState === "pending") {
|
|
144
|
+
if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
|
|
145
|
+
throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
|
|
146
|
+
return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch, signal);
|
|
147
|
+
}
|
|
148
|
+
if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
151
149
|
return session;
|
|
152
150
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
151
|
+
if (session?.tokens?.refreshToken !== undefined &&
|
|
152
|
+
sessionDiscovery !== undefined &&
|
|
153
|
+
(forceRefresh || isExpired(session.tokens, now))) {
|
|
154
|
+
session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch, signal);
|
|
155
|
+
if (session?.tokens !== undefined && !isExpired(session.tokens, now)) {
|
|
156
|
+
return session;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (forceRefresh && session?.tokens !== undefined) {
|
|
160
|
+
session = clearSessionTokens(session);
|
|
161
|
+
await saveSession(canonicalResource, session);
|
|
162
|
+
}
|
|
163
|
+
if (!allowInteractive || sessionDiscovery === undefined) {
|
|
164
|
+
return session;
|
|
165
|
+
}
|
|
166
|
+
if (options.allowInteractive === false)
|
|
167
|
+
throw new Error("OAuth interactive authorization is disabled");
|
|
168
|
+
return authorizeSession(canonicalResource, session, sessionDiscovery, fetch, signal);
|
|
169
|
+
}, { signal, timeoutMs: options.sessionLockTimeoutMs });
|
|
164
170
|
}
|
|
165
171
|
async function refreshSession(resource, session, discovery, fetch, signal) {
|
|
166
172
|
signal?.throwIfAborted();
|
|
167
173
|
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return inFlight;
|
|
174
|
+
if (session.tokens?.refreshToken === undefined) {
|
|
175
|
+
return session;
|
|
171
176
|
}
|
|
172
|
-
const
|
|
177
|
+
const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
|
|
178
|
+
await saveSession(resource, pendingSession);
|
|
179
|
+
signal?.throwIfAborted();
|
|
180
|
+
let refreshAttempted = false;
|
|
181
|
+
let refreshedTokens;
|
|
182
|
+
while (true) {
|
|
173
183
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
refreshedTokens = await refreshAccessToken({
|
|
185
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
186
|
+
clientId: session.client.clientId,
|
|
187
|
+
clientSecret: session.client.clientSecret,
|
|
188
|
+
refreshToken: session.tokens.refreshToken,
|
|
189
|
+
resource,
|
|
190
|
+
fetch, signal,
|
|
191
|
+
now
|
|
192
|
+
});
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
signal?.throwIfAborted();
|
|
197
|
+
// Network errors, lost/malformed bodies and gateway failures cannot
|
|
198
|
+
// establish whether a rotating refresh token was already consumed.
|
|
199
|
+
if (!(error instanceof OAuthError) || !error.outcomeKnown)
|
|
200
|
+
throw error;
|
|
201
|
+
if (error.error === "invalid_grant") {
|
|
202
|
+
const clearedSession = clearSessionTokens(session);
|
|
203
|
+
await saveSession(resource, clearedSession);
|
|
204
|
+
return clearedSession;
|
|
176
205
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
refreshedTokens = await refreshAccessToken({
|
|
182
|
-
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
183
|
-
clientId: session.client.clientId,
|
|
184
|
-
clientSecret: session.client.clientSecret,
|
|
185
|
-
refreshToken: session.tokens.refreshToken,
|
|
186
|
-
resource,
|
|
187
|
-
fetch, signal,
|
|
188
|
-
now
|
|
189
|
-
});
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
signal?.throwIfAborted();
|
|
194
|
-
if (error instanceof OAuthError && error.error === "invalid_grant") {
|
|
195
|
-
const clearedSession = clearSessionTokens(session);
|
|
196
|
-
await saveSession(resource, clearedSession);
|
|
197
|
-
return clearedSession;
|
|
198
|
-
}
|
|
199
|
-
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
200
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
201
|
-
await clearSession(resource);
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
205
|
-
refreshAttempted = true;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
206
|
+
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
207
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
208
|
+
await clearSession(resource);
|
|
209
|
+
return null;
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
discovery: toStoredDiscovery(discovery)
|
|
218
|
-
};
|
|
219
|
-
await saveSession(resource, updatedSession);
|
|
220
|
-
return updatedSession;
|
|
221
|
-
}
|
|
222
|
-
finally {
|
|
223
|
-
refreshPromises.delete(resource);
|
|
211
|
+
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
212
|
+
refreshAttempted = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
await saveSession(resource, session);
|
|
216
|
+
throw error;
|
|
224
217
|
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
218
|
+
}
|
|
219
|
+
const updatedSession = {
|
|
220
|
+
...session,
|
|
221
|
+
tokens: {
|
|
222
|
+
...refreshedTokens,
|
|
223
|
+
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
224
|
+
},
|
|
225
|
+
discovery: toStoredDiscovery(discovery)
|
|
226
|
+
};
|
|
227
|
+
await saveSession(resource, updatedSession);
|
|
228
|
+
return updatedSession;
|
|
228
229
|
}
|
|
229
230
|
async function authorizeSession(resource, existingSession, discovery, fetch, signal) {
|
|
230
231
|
signal?.throwIfAborted();
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
232
|
+
assertS256PkceSupport(discovery.authorizationServerMetadata);
|
|
233
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
234
|
+
let currentSession = existingSession;
|
|
235
|
+
let transientRetryAttempted = false;
|
|
236
|
+
let reRegistrationAttempted = false;
|
|
237
|
+
while (true) {
|
|
238
|
+
const loopback = await createLoopbackAuthorizationSession({
|
|
239
|
+
openBrowser: options.browser.openBrowser,
|
|
240
|
+
readLine: options.browser.readLine,
|
|
241
|
+
createServer: options.browser.createServer,
|
|
242
|
+
landingPage: options.browser.landingPage,
|
|
243
|
+
redirectUri: options.browser.redirectUri,
|
|
244
|
+
signal: options.browser.signal === undefined ? signal : signal === undefined ? options.browser.signal : AbortSignal.any([signal, options.browser.signal]),
|
|
245
|
+
timeoutMs: options.browser.timeoutMs
|
|
246
|
+
});
|
|
247
|
+
let resolvedClient = null;
|
|
248
|
+
try {
|
|
249
|
+
resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch, signal);
|
|
250
|
+
const sessionWithoutTokens = {
|
|
251
|
+
resource,
|
|
252
|
+
authorizationServer: discovery.authorizationServer,
|
|
253
|
+
client: resolvedClient.client,
|
|
254
|
+
discovery: toStoredDiscovery(discovery)
|
|
255
|
+
};
|
|
256
|
+
await saveSession(resource, sessionWithoutTokens);
|
|
257
|
+
const verifier = generateCodeVerifier();
|
|
258
|
+
const challenge = generateCodeChallenge(verifier);
|
|
259
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
260
|
+
metadata: discovery.authorizationServerMetadata,
|
|
261
|
+
resource,
|
|
262
|
+
clientId: resolvedClient.client.clientId,
|
|
263
|
+
redirectUri: loopback.redirectUri,
|
|
264
|
+
codeChallenge: challenge,
|
|
265
|
+
clientMetadata: getClientMetadata(options.client)
|
|
250
266
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
redirectUri: loopback.redirectUri,
|
|
279
|
-
resource,
|
|
280
|
-
fetch, signal,
|
|
281
|
-
now
|
|
282
|
-
});
|
|
283
|
-
const session = {
|
|
284
|
-
...sessionWithoutTokens,
|
|
285
|
-
tokens
|
|
286
|
-
};
|
|
287
|
-
await saveSession(resource, session);
|
|
288
|
-
return session;
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
signal?.throwIfAborted();
|
|
292
|
-
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
293
|
-
reRegistrationAttempted = true;
|
|
294
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
295
|
-
await clearSession(resource);
|
|
296
|
-
currentSession = null;
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
300
|
-
transientRetryAttempted = true;
|
|
301
|
-
await clearSession(resource);
|
|
302
|
-
currentSession = null;
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
throw error;
|
|
267
|
+
const code = await loopback.waitForCode(authorizationUrl);
|
|
268
|
+
const tokens = await exchangeAuthorizationCode({
|
|
269
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
270
|
+
clientId: resolvedClient.client.clientId,
|
|
271
|
+
clientSecret: resolvedClient.client.clientSecret,
|
|
272
|
+
code,
|
|
273
|
+
codeVerifier: verifier,
|
|
274
|
+
redirectUri: loopback.redirectUri,
|
|
275
|
+
resource,
|
|
276
|
+
fetch, signal,
|
|
277
|
+
now
|
|
278
|
+
});
|
|
279
|
+
const session = {
|
|
280
|
+
...sessionWithoutTokens,
|
|
281
|
+
tokens
|
|
282
|
+
};
|
|
283
|
+
await saveSession(resource, session);
|
|
284
|
+
return session;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
signal?.throwIfAborted();
|
|
288
|
+
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
289
|
+
reRegistrationAttempted = true;
|
|
290
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
291
|
+
await clearSession(resource);
|
|
292
|
+
currentSession = null;
|
|
293
|
+
continue;
|
|
306
294
|
}
|
|
307
|
-
|
|
308
|
-
|
|
295
|
+
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
296
|
+
transientRetryAttempted = true;
|
|
297
|
+
await clearSession(resource);
|
|
298
|
+
currentSession = null;
|
|
299
|
+
continue;
|
|
309
300
|
}
|
|
301
|
+
throw error;
|
|
310
302
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
authorizationPromises.set(resource, finalPromise);
|
|
316
|
-
return finalPromise;
|
|
303
|
+
finally {
|
|
304
|
+
loopback.close();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
317
307
|
}
|
|
318
308
|
async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
|
|
319
309
|
parentSignal?.throwIfAborted();
|
|
@@ -480,6 +470,7 @@ function sameTokenGrant(left, right) {
|
|
|
480
470
|
function clearSessionTokens(session) {
|
|
481
471
|
const nextSession = { ...session };
|
|
482
472
|
delete nextSession.tokens;
|
|
473
|
+
delete nextSession.refreshState;
|
|
483
474
|
return nextSession;
|
|
484
475
|
}
|
|
485
476
|
function hasCachedAccessToken(session) {
|
|
@@ -489,6 +480,9 @@ function normalizeLoadedSession(session) {
|
|
|
489
480
|
if (session === null) {
|
|
490
481
|
return null;
|
|
491
482
|
}
|
|
483
|
+
const refreshState = getOwnEntry(session, "refreshState");
|
|
484
|
+
if (refreshState !== undefined && (refreshState !== "pending" || getOwnEntry(session, "tokens") !== undefined))
|
|
485
|
+
throw new Error("Stored OAuth refresh state is invalid");
|
|
492
486
|
const client = normalizeStoredClient(getOwnEntry(session, "client"));
|
|
493
487
|
if (client === null) {
|
|
494
488
|
return { ...session, client: { clientId: "" }, tokens: undefined };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OAuthSessionStore } from "./types.js";
|
|
2
|
+
/** Serialize the complete read/redeem/write operation, with independent cancellation for waiters. */
|
|
3
|
+
export declare function withOAuthSessionTransaction<T>(store: OAuthSessionStore, resource: string, operation: () => Promise<T>, options?: {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}): Promise<T>;
|
package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/session-transaction.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const queues = new WeakMap();
|
|
2
|
+
/** Serialize the complete read/redeem/write operation, with independent cancellation for waiters. */
|
|
3
|
+
export async function withOAuthSessionTransaction(store, resource, operation, options = {}) {
|
|
4
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
5
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647)
|
|
6
|
+
throw new Error("sessionLockTimeoutMs must be an integer from 1 to 2147483647 milliseconds");
|
|
7
|
+
options.signal?.throwIfAborted();
|
|
8
|
+
const started = performance.now();
|
|
9
|
+
const pending = queues.get(store) ?? new Map();
|
|
10
|
+
queues.set(store, pending);
|
|
11
|
+
const previous = pending.get(resource) ?? Promise.resolve();
|
|
12
|
+
let release;
|
|
13
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
14
|
+
const tail = previous.then(() => current);
|
|
15
|
+
pending.set(resource, tail);
|
|
16
|
+
try {
|
|
17
|
+
let timer;
|
|
18
|
+
let rejectWait;
|
|
19
|
+
const waiting = new Promise((resolve, reject) => { rejectWait = reject; previous.then(resolve, reject); });
|
|
20
|
+
const abort = () => rejectWait(options.signal?.reason);
|
|
21
|
+
try {
|
|
22
|
+
timer = setTimeout(() => rejectWait(new Error("Timed out waiting for OAuth session transaction lock")), timeoutMs);
|
|
23
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
24
|
+
if (options.signal?.aborted)
|
|
25
|
+
abort();
|
|
26
|
+
await waiting;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
options.signal?.removeEventListener("abort", abort);
|
|
31
|
+
}
|
|
32
|
+
options.signal?.throwIfAborted();
|
|
33
|
+
return store.withLock === undefined ? await operation() : await store.withLock(resource, operation, {
|
|
34
|
+
signal: options.signal, timeoutMs: Math.max(0, timeoutMs - (performance.now() - started))
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
release();
|
|
39
|
+
void tail.then(() => { if (pending.get(resource) === tail)
|
|
40
|
+
pending.delete(resource); });
|
|
41
|
+
}
|
|
42
|
+
}
|
package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ export declare class OAuthError extends Error {
|
|
|
13
13
|
readonly status: number;
|
|
14
14
|
readonly retryable: boolean;
|
|
15
15
|
readonly terminal: boolean;
|
|
16
|
-
|
|
16
|
+
/** True only when a complete OAuth error response establishes rejection. */
|
|
17
|
+
readonly outcomeKnown: boolean;
|
|
18
|
+
constructor(shape: OAuthErrorShape, status: number, outcomeKnown?: boolean);
|
|
17
19
|
}
|
|
18
20
|
export declare function isRetryableOAuthError(error: unknown): error is OAuthError;
|
|
19
21
|
export declare function exchangeAuthorizationCode(input: {
|
package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.js
CHANGED
|
@@ -11,7 +11,9 @@ export class OAuthError extends Error {
|
|
|
11
11
|
status;
|
|
12
12
|
retryable;
|
|
13
13
|
terminal;
|
|
14
|
-
|
|
14
|
+
/** True only when a complete OAuth error response establishes rejection. */
|
|
15
|
+
outcomeKnown;
|
|
16
|
+
constructor(shape, status, outcomeKnown = true) {
|
|
15
17
|
super(shape.error_description ?? shape.error);
|
|
16
18
|
this.name = "OAuthError";
|
|
17
19
|
this.error = shape.error;
|
|
@@ -22,6 +24,7 @@ export class OAuthError extends Error {
|
|
|
22
24
|
this.status = status;
|
|
23
25
|
this.retryable = isRetryableOAuthError(this);
|
|
24
26
|
this.terminal = !this.retryable;
|
|
27
|
+
this.outcomeKnown = outcomeKnown;
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
export function isRetryableOAuthError(error) {
|
|
@@ -147,7 +150,8 @@ export async function readOAuthJsonObjectResponse(response, signal) {
|
|
|
147
150
|
}
|
|
148
151
|
const record = payload;
|
|
149
152
|
if (!response.ok) {
|
|
150
|
-
|
|
153
|
+
const error = getOwnEntry(record, "error");
|
|
154
|
+
throw new OAuthError(readOAuthError(record, fallbackError.error), response.status, typeof error === "string" && error.trim().length > 0);
|
|
151
155
|
}
|
|
152
156
|
return record;
|
|
153
157
|
}
|
|
@@ -166,7 +170,7 @@ function getOwnEntry(record, key) {
|
|
|
166
170
|
}
|
|
167
171
|
function createFallbackOAuthError(status) {
|
|
168
172
|
const error = status === 503 ? "temporarily_unavailable" : "server_error";
|
|
169
|
-
return new OAuthError({ error }, status);
|
|
173
|
+
return new OAuthError({ error }, status, false);
|
|
170
174
|
}
|
|
171
175
|
function normalizeBearerTokenType(value) {
|
|
172
176
|
if (typeof value !== "string") {
|
|
@@ -78,6 +78,8 @@ export interface StoredOAuthSession {
|
|
|
78
78
|
clientSecret?: string;
|
|
79
79
|
};
|
|
80
80
|
tokens?: StoredOAuthTokens;
|
|
81
|
+
/** A refresh was begun; its winning response may not have been persisted. */
|
|
82
|
+
refreshState?: "pending";
|
|
81
83
|
discovery: {
|
|
82
84
|
resourceMetadataUrl: string;
|
|
83
85
|
resourceMetadata: Record<string, unknown>;
|
|
@@ -88,6 +90,11 @@ export interface OAuthSessionStore {
|
|
|
88
90
|
load(resource: string): Promise<StoredOAuthSession | null>;
|
|
89
91
|
save(resource: string, session: StoredOAuthSession): Promise<void>;
|
|
90
92
|
clear(resource: string): Promise<void>;
|
|
93
|
+
/** Backend-wide lock covering a complete read/redeem/write transaction. */
|
|
94
|
+
withLock?<T>(resource: string, operation: () => Promise<T>, options: {
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
timeoutMs: number;
|
|
97
|
+
}): Promise<T>;
|
|
91
98
|
}
|
|
92
99
|
export interface DefaultOAuthClientProviderOptions {
|
|
93
100
|
client: {
|
|
@@ -103,6 +110,8 @@ export interface DefaultOAuthClientProviderOptions {
|
|
|
103
110
|
};
|
|
104
111
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
105
112
|
allowInteractive?: boolean;
|
|
113
|
+
/** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
|
|
114
|
+
sessionLockTimeoutMs?: number;
|
|
106
115
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
107
116
|
initialGrant?: {
|
|
108
117
|
resource: string;
|
|
@@ -20,11 +20,31 @@ interface McpSubscription {
|
|
|
20
20
|
cancel(): void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
interface SecretStoreLockFileSystem {
|
|
24
|
+
mkdir(path: string, options?: {
|
|
25
|
+
recursive?: boolean;
|
|
26
|
+
mode?: number;
|
|
27
|
+
}): Promise<unknown>;
|
|
28
|
+
readdir(path: string): Promise<string[]>;
|
|
29
|
+
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
30
|
+
writeFile(path: string, value: string, options: {
|
|
31
|
+
encoding: BufferEncoding;
|
|
32
|
+
flag: string;
|
|
33
|
+
mode: number;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
rename(from: string, to: string): Promise<void>;
|
|
36
|
+
unlink(path: string): Promise<void>;
|
|
37
|
+
lstat(path: string): Promise<{
|
|
38
|
+
isSymbolicLink(): boolean;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
interface MachineIdentity {
|
|
24
43
|
hostname: string;
|
|
25
44
|
username: string;
|
|
26
45
|
}
|
|
27
46
|
interface EncryptedFileStoreFileSystem {
|
|
47
|
+
readdir?(path: string): Promise<string[]>;
|
|
28
48
|
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
29
49
|
writeFile(path: string, data: string | NodeJS.ArrayBufferView, options?: {
|
|
30
50
|
encoding?: BufferEncoding;
|
|
@@ -50,6 +70,8 @@ interface EncryptedFileStoreInput {
|
|
|
50
70
|
getMachineIdentity?: () => MachineIdentity | Promise<MachineIdentity>;
|
|
51
71
|
getHomeDirectory?: () => string;
|
|
52
72
|
getRandomBytes?: (size: number) => Buffer;
|
|
73
|
+
/** Fail closed instead of treating malformed or unauthenticated documents as absent. */
|
|
74
|
+
throwOnInvalidDocument?: boolean;
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
interface KeychainCommandResult {
|
|
@@ -65,6 +87,10 @@ interface KeychainStoreInput {
|
|
|
65
87
|
runCommand?: KeychainCommandRunner;
|
|
66
88
|
service: string;
|
|
67
89
|
account: string;
|
|
90
|
+
lock?: {
|
|
91
|
+
fs?: SecretStoreLockFileSystem;
|
|
92
|
+
directory?: string;
|
|
93
|
+
};
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
type StoreBackend = "file" | "keychain";
|
|
@@ -155,6 +181,8 @@ interface StoredOAuthSession {
|
|
|
155
181
|
clientSecret?: string;
|
|
156
182
|
};
|
|
157
183
|
tokens?: StoredOAuthTokens;
|
|
184
|
+
/** A refresh was begun; its winning response may not have been persisted. */
|
|
185
|
+
refreshState?: "pending";
|
|
158
186
|
discovery: {
|
|
159
187
|
resourceMetadataUrl: string;
|
|
160
188
|
resourceMetadata: Record<string, unknown>;
|
|
@@ -165,6 +193,11 @@ interface OAuthSessionStore {
|
|
|
165
193
|
load(resource: string): Promise<StoredOAuthSession | null>;
|
|
166
194
|
save(resource: string, session: StoredOAuthSession): Promise<void>;
|
|
167
195
|
clear(resource: string): Promise<void>;
|
|
196
|
+
/** Backend-wide lock covering a complete read/redeem/write transaction. */
|
|
197
|
+
withLock?<T>(resource: string, operation: () => Promise<T>, options: {
|
|
198
|
+
signal?: AbortSignal;
|
|
199
|
+
timeoutMs: number;
|
|
200
|
+
}): Promise<T>;
|
|
168
201
|
}
|
|
169
202
|
interface DefaultOAuthClientProviderOptions {
|
|
170
203
|
client: {
|
|
@@ -180,6 +213,8 @@ interface DefaultOAuthClientProviderOptions {
|
|
|
180
213
|
};
|
|
181
214
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
182
215
|
allowInteractive?: boolean;
|
|
216
|
+
/** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
|
|
217
|
+
sessionLockTimeoutMs?: number;
|
|
183
218
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
184
219
|
initialGrant?: {
|
|
185
220
|
resource: string;
|