toolcraft 0.0.309 → 0.0.311
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 +16 -0
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +3 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +7 -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 +33 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +8 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +202 -185
- 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 +14 -1
- package/node_modules/tiny-http-mcp-server/package.json +1 -1
- package/node_modules/tiny-mcp-client/dist/index.d.ts +39 -1
- package/node_modules/tiny-mcp-client/dist/index.js +503 -254
- 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 {
|
|
@@ -58,7 +57,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
58
57
|
if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl &&
|
|
59
58
|
initialGrant.tokens !== undefined && !isExpired(initialGrant.tokens, now)) {
|
|
60
59
|
input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
|
|
61
|
-
return;
|
|
60
|
+
return { ...initialGrant.tokens };
|
|
62
61
|
}
|
|
63
62
|
if (session === null ||
|
|
64
63
|
accessToken === undefined ||
|
|
@@ -68,6 +67,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
68
67
|
}
|
|
69
68
|
assertRequestMatchesResource(requestUrl, session.resource);
|
|
70
69
|
input.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
70
|
+
return { ...session.tokens };
|
|
71
71
|
},
|
|
72
72
|
async handleUnauthorized(input) {
|
|
73
73
|
try {
|
|
@@ -75,12 +75,28 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
75
75
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
76
76
|
const resource = canonicalizeResourceIndicator(input.discovery.resource);
|
|
77
77
|
assertRequestMatchesResource(requestUrl, resource);
|
|
78
|
-
const
|
|
79
|
-
|
|
78
|
+
const cached = await loadSession(resource);
|
|
79
|
+
const currentTokens = cached?.tokens ?? (!initialGrantConsumed && initialGrant?.resource === resource ? initialGrant.tokens : undefined);
|
|
80
|
+
let rejectedCurrentGrant = hasCachedAccessToken(cached) || (!initialGrantConsumed && initialGrant?.resource === resource);
|
|
81
|
+
let presentedTokens = input.presentedTokens;
|
|
82
|
+
if (input.presentedTokens !== undefined) {
|
|
83
|
+
rejectedCurrentGrant = false;
|
|
84
|
+
if (input.presentedTokens !== null) {
|
|
85
|
+
const presented = normalizeStoredTokens(input.presentedTokens);
|
|
86
|
+
const header = input.requestHeaders?.get("Authorization") ?? "";
|
|
87
|
+
const separator = header.indexOf(" ");
|
|
88
|
+
if (presented === undefined || header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
|
|
89
|
+
throw new Error("OAuth rejected-request provenance does not match its authorization header");
|
|
90
|
+
presentedTokens = presented;
|
|
91
|
+
rejectedCurrentGrant = currentTokens !== undefined && sameTokenGrant(currentTokens, presented);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const challengeError = input.challenge?.params.error;
|
|
95
|
+
const forceRefresh = rejectedCurrentGrant && (challengeError === "invalid_token" || (input.presentedTokens !== undefined && challengeError === undefined));
|
|
80
96
|
const session = await ensureAuthorizedSession(resource, {
|
|
81
97
|
...input.discovery,
|
|
82
98
|
resource
|
|
83
|
-
}, input.fetch, true, forceRefresh, input.signal);
|
|
99
|
+
}, input.fetch, true, forceRefresh, input.signal, presentedTokens);
|
|
84
100
|
if (session?.tokens?.accessToken === undefined) {
|
|
85
101
|
return { action: "fail" };
|
|
86
102
|
}
|
|
@@ -95,206 +111,199 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
};
|
|
98
|
-
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal) {
|
|
114
|
+
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
99
115
|
signal?.throwIfAborted();
|
|
100
116
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
106
|
-
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
107
|
-
}
|
|
108
|
-
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
|
|
109
|
-
|| getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
|
|
110
|
-
|| (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
|
|
111
|
-
await clearSession(canonicalResource);
|
|
112
|
-
session = null;
|
|
113
|
-
}
|
|
114
|
-
if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
|
|
115
|
-
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
116
|
-
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
117
|
-
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
118
|
-
client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
|
|
119
|
-
await saveSession(canonicalResource, session);
|
|
120
|
-
initialGrantConsumed = true;
|
|
117
|
+
return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
|
|
118
|
+
let session = await loadSession(canonicalResource);
|
|
119
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
120
|
+
initialGrantConsumed = true;
|
|
121
121
|
signal?.throwIfAborted();
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (session
|
|
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)) {
|
|
132
149
|
return session;
|
|
133
150
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 });
|
|
145
170
|
}
|
|
146
171
|
async function refreshSession(resource, session, discovery, fetch, signal) {
|
|
147
172
|
signal?.throwIfAborted();
|
|
148
173
|
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return inFlight;
|
|
174
|
+
if (session.tokens?.refreshToken === undefined) {
|
|
175
|
+
return session;
|
|
152
176
|
}
|
|
153
|
-
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) {
|
|
154
183
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
157
205
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
refreshedTokens = await refreshAccessToken({
|
|
163
|
-
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
164
|
-
clientId: session.client.clientId,
|
|
165
|
-
clientSecret: session.client.clientSecret,
|
|
166
|
-
refreshToken: session.tokens.refreshToken,
|
|
167
|
-
resource,
|
|
168
|
-
fetch, signal,
|
|
169
|
-
now
|
|
170
|
-
});
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
signal?.throwIfAborted();
|
|
175
|
-
if (error instanceof OAuthError && error.error === "invalid_grant") {
|
|
176
|
-
const clearedSession = clearSessionTokens(session);
|
|
177
|
-
await saveSession(resource, clearedSession);
|
|
178
|
-
return clearedSession;
|
|
179
|
-
}
|
|
180
|
-
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
181
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
182
|
-
await clearSession(resource);
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
186
|
-
refreshAttempted = true;
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
throw error;
|
|
190
|
-
}
|
|
206
|
+
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
207
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
208
|
+
await clearSession(resource);
|
|
209
|
+
return null;
|
|
191
210
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
discovery: toStoredDiscovery(discovery)
|
|
199
|
-
};
|
|
200
|
-
await saveSession(resource, updatedSession);
|
|
201
|
-
return updatedSession;
|
|
202
|
-
}
|
|
203
|
-
finally {
|
|
204
|
-
refreshPromises.delete(resource);
|
|
211
|
+
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
212
|
+
refreshAttempted = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
await saveSession(resource, session);
|
|
216
|
+
throw error;
|
|
205
217
|
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
209
229
|
}
|
|
210
230
|
async function authorizeSession(resource, existingSession, discovery, fetch, signal) {
|
|
211
231
|
signal?.throwIfAborted();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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)
|
|
231
266
|
});
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
redirectUri: loopback.redirectUri,
|
|
260
|
-
resource,
|
|
261
|
-
fetch, signal,
|
|
262
|
-
now
|
|
263
|
-
});
|
|
264
|
-
const session = {
|
|
265
|
-
...sessionWithoutTokens,
|
|
266
|
-
tokens
|
|
267
|
-
};
|
|
268
|
-
await saveSession(resource, session);
|
|
269
|
-
return session;
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
signal?.throwIfAborted();
|
|
273
|
-
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
274
|
-
reRegistrationAttempted = true;
|
|
275
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
276
|
-
await clearSession(resource);
|
|
277
|
-
currentSession = null;
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
281
|
-
transientRetryAttempted = true;
|
|
282
|
-
await clearSession(resource);
|
|
283
|
-
currentSession = null;
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
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;
|
|
287
294
|
}
|
|
288
|
-
|
|
289
|
-
|
|
295
|
+
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
296
|
+
transientRetryAttempted = true;
|
|
297
|
+
await clearSession(resource);
|
|
298
|
+
currentSession = null;
|
|
299
|
+
continue;
|
|
290
300
|
}
|
|
301
|
+
throw error;
|
|
291
302
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
authorizationPromises.set(resource, finalPromise);
|
|
297
|
-
return finalPromise;
|
|
303
|
+
finally {
|
|
304
|
+
loopback.close();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
298
307
|
}
|
|
299
308
|
async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
|
|
300
309
|
parentSignal?.throwIfAborted();
|
|
@@ -454,9 +463,14 @@ function resolveDiscovery(discovery, session) {
|
|
|
454
463
|
authorizationServerMetadata: metadata
|
|
455
464
|
};
|
|
456
465
|
}
|
|
466
|
+
function sameTokenGrant(left, right) {
|
|
467
|
+
return left.accessToken === right.accessToken && left.refreshToken === right.refreshToken &&
|
|
468
|
+
left.tokenType === right.tokenType && left.expiresAt === right.expiresAt && left.scope === right.scope;
|
|
469
|
+
}
|
|
457
470
|
function clearSessionTokens(session) {
|
|
458
471
|
const nextSession = { ...session };
|
|
459
472
|
delete nextSession.tokens;
|
|
473
|
+
delete nextSession.refreshState;
|
|
460
474
|
return nextSession;
|
|
461
475
|
}
|
|
462
476
|
function hasCachedAccessToken(session) {
|
|
@@ -466,6 +480,9 @@ function normalizeLoadedSession(session) {
|
|
|
466
480
|
if (session === null) {
|
|
467
481
|
return null;
|
|
468
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");
|
|
469
486
|
const client = normalizeStoredClient(getOwnEntry(session, "client"));
|
|
470
487
|
if (client === null) {
|
|
471
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") {
|
|
@@ -33,7 +33,7 @@ export interface OAuthClientProvider {
|
|
|
33
33
|
headers: Headers;
|
|
34
34
|
fetch: OAuthMetadataFetch;
|
|
35
35
|
signal?: AbortSignal;
|
|
36
|
-
}): Promise<void> | void;
|
|
36
|
+
}): Promise<StoredOAuthTokens | void> | StoredOAuthTokens | void;
|
|
37
37
|
handleUnauthorized(input: {
|
|
38
38
|
requestUrl: URL;
|
|
39
39
|
response: Response;
|
|
@@ -41,6 +41,10 @@ export interface OAuthClientProvider {
|
|
|
41
41
|
discovery: OAuthDiscoveryResult;
|
|
42
42
|
fetch: OAuthMetadataFetch;
|
|
43
43
|
signal?: AbortSignal;
|
|
44
|
+
/** Headers actually attached to this rejected request. */
|
|
45
|
+
requestHeaders?: Headers;
|
|
46
|
+
/** Owned snapshot returned when this request was authorized, or null if absent. */
|
|
47
|
+
presentedTokens?: StoredOAuthTokens | null;
|
|
44
48
|
}): Promise<{
|
|
45
49
|
action: "retry";
|
|
46
50
|
} | {
|
|
@@ -74,6 +78,8 @@ export interface StoredOAuthSession {
|
|
|
74
78
|
clientSecret?: string;
|
|
75
79
|
};
|
|
76
80
|
tokens?: StoredOAuthTokens;
|
|
81
|
+
/** A refresh was begun; its winning response may not have been persisted. */
|
|
82
|
+
refreshState?: "pending";
|
|
77
83
|
discovery: {
|
|
78
84
|
resourceMetadataUrl: string;
|
|
79
85
|
resourceMetadata: Record<string, unknown>;
|
|
@@ -84,6 +90,11 @@ export interface OAuthSessionStore {
|
|
|
84
90
|
load(resource: string): Promise<StoredOAuthSession | null>;
|
|
85
91
|
save(resource: string, session: StoredOAuthSession): Promise<void>;
|
|
86
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>;
|
|
87
98
|
}
|
|
88
99
|
export interface DefaultOAuthClientProviderOptions {
|
|
89
100
|
client: {
|
|
@@ -99,6 +110,8 @@ export interface DefaultOAuthClientProviderOptions {
|
|
|
99
110
|
};
|
|
100
111
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
101
112
|
allowInteractive?: boolean;
|
|
113
|
+
/** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
|
|
114
|
+
sessionLockTimeoutMs?: number;
|
|
102
115
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
103
116
|
initialGrant?: {
|
|
104
117
|
resource: string;
|