tiny-http-mcp-server 0.1.24 → 0.1.26
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/dist/composition.json +1 -1
- package/node_modules/mcp-oauth/README.md +20 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +185 -185
- package/node_modules/mcp-oauth/dist/client/session-transaction.d.ts +6 -0
- package/node_modules/mcp-oauth/dist/client/session-transaction.js +42 -0
- package/node_modules/mcp-oauth/dist/client/types.d.ts +12 -1
- package/node_modules/tiny-mcp-client/dist/index.d.ts +13 -1
- package/node_modules/tiny-mcp-client/dist/index.js +246 -189
- package/package.json +1 -1
package/dist/composition.json
CHANGED
|
@@ -41,6 +41,7 @@ const verifier = createJwksTokenVerifier({
|
|
|
41
41
|
- `mode: "dynamic"` with optional `metadata`
|
|
42
42
|
- `mode: "static"` with `clientId`, optional `clientSecret`, optional `metadata`
|
|
43
43
|
- `allowInteractive: false` prevents interactive login while retaining cached tokens and silent refresh
|
|
44
|
+
- `sessionLockTimeoutMs` limits acquisition waits for a session transaction lock (default 30,000 ms; integer from 1 to 2147483647)
|
|
44
45
|
- `initialGrant: { resource, tokens }` optionally imports an existing Bearer grant for one HTTP resource; requires the original client ID
|
|
45
46
|
- `browser.openBrowser(url)` optional
|
|
46
47
|
- `browser.readLine()` optional
|
|
@@ -84,6 +85,15 @@ registration, token requests and bounded token-body reads. Cancellation retains
|
|
|
84
85
|
its original reason and does not retry authorization. Custom providers should
|
|
85
86
|
observe the supplied signal and pass it to any work they start.
|
|
86
87
|
|
|
88
|
+
`authorizeRequest` may return an owned token snapshot for the request it
|
|
89
|
+
authorized. The HTTP client supplies that snapshot as `presentedTokens`, along
|
|
90
|
+
with the actual request's `requestHeaders`, to `handleUnauthorized`. Providers
|
|
91
|
+
that return `void` remain supported. The native provider compares the rejected
|
|
92
|
+
snapshot with persisted credentials: delayed 401s retry with a newer grant
|
|
93
|
+
without redeeming its refresh token again. A proven current token is refreshed
|
|
94
|
+
on 401 even when the server omits `error="invalid_token"`. Invalid provenance
|
|
95
|
+
fails without quoting token values.
|
|
96
|
+
|
|
87
97
|
Configure `client.metadata.scope` to request a precise scope set; broader
|
|
88
98
|
discovery metadata does not override it.
|
|
89
99
|
|
|
@@ -97,6 +107,16 @@ Input tokens are copied and invalid expiry values fail before authorization.
|
|
|
97
107
|
|
|
98
108
|
`createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
|
|
99
109
|
|
|
110
|
+
Providers sharing the same `sessionStore` object serialize the complete session
|
|
111
|
+
read, refresh/authorization and persistence transaction for each resource.
|
|
112
|
+
Waiting requests can cancel or time out independently; they cannot release an
|
|
113
|
+
active owner's lock. Custom stores may implement
|
|
114
|
+
`withLock(resource, operation, { signal, timeoutMs })` to serialize the same
|
|
115
|
+
transaction across store instances or processes. The hook must honor acquisition
|
|
116
|
+
cancellation and keep the lock until the operation settles. The timeout bounds
|
|
117
|
+
acquisition, while token and browser operations retain their own deadlines.
|
|
118
|
+
Cross-process locking for the native secret-store backend is under development.
|
|
119
|
+
|
|
100
120
|
## Environment Variables
|
|
101
121
|
|
|
102
122
|
This package exposes no direct environment variables. When `authStore` is used,
|
|
@@ -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,186 @@ 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?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
132
144
|
return session;
|
|
133
145
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
if (session?.tokens?.refreshToken !== undefined &&
|
|
147
|
+
sessionDiscovery !== undefined &&
|
|
148
|
+
(forceRefresh || isExpired(session.tokens, now))) {
|
|
149
|
+
session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch, signal);
|
|
150
|
+
if (session?.tokens !== undefined && !isExpired(session.tokens, now)) {
|
|
151
|
+
return session;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (forceRefresh && session?.tokens !== undefined) {
|
|
155
|
+
session = clearSessionTokens(session);
|
|
156
|
+
await saveSession(canonicalResource, session);
|
|
157
|
+
}
|
|
158
|
+
if (!allowInteractive || sessionDiscovery === undefined) {
|
|
159
|
+
return session;
|
|
160
|
+
}
|
|
161
|
+
if (options.allowInteractive === false)
|
|
162
|
+
throw new Error("OAuth interactive authorization is disabled");
|
|
163
|
+
return authorizeSession(canonicalResource, session, sessionDiscovery, fetch, signal);
|
|
164
|
+
}, { signal, timeoutMs: options.sessionLockTimeoutMs });
|
|
145
165
|
}
|
|
146
166
|
async function refreshSession(resource, session, discovery, fetch, signal) {
|
|
147
167
|
signal?.throwIfAborted();
|
|
148
168
|
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return inFlight;
|
|
169
|
+
if (session.tokens?.refreshToken === undefined) {
|
|
170
|
+
return session;
|
|
152
171
|
}
|
|
153
|
-
|
|
172
|
+
let refreshAttempted = false;
|
|
173
|
+
let refreshedTokens;
|
|
174
|
+
while (true) {
|
|
154
175
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
176
|
+
refreshedTokens = await refreshAccessToken({
|
|
177
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
178
|
+
clientId: session.client.clientId,
|
|
179
|
+
clientSecret: session.client.clientSecret,
|
|
180
|
+
refreshToken: session.tokens.refreshToken,
|
|
181
|
+
resource,
|
|
182
|
+
fetch, signal,
|
|
183
|
+
now
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
signal?.throwIfAborted();
|
|
189
|
+
if (error instanceof OAuthError && error.error === "invalid_grant") {
|
|
190
|
+
const clearedSession = clearSessionTokens(session);
|
|
191
|
+
await saveSession(resource, clearedSession);
|
|
192
|
+
return clearedSession;
|
|
157
193
|
}
|
|
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
|
-
}
|
|
194
|
+
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
195
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
196
|
+
await clearSession(resource);
|
|
197
|
+
return null;
|
|
191
198
|
}
|
|
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);
|
|
199
|
+
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
200
|
+
refreshAttempted = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
205
204
|
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
}
|
|
206
|
+
const updatedSession = {
|
|
207
|
+
...session,
|
|
208
|
+
tokens: {
|
|
209
|
+
...refreshedTokens,
|
|
210
|
+
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
211
|
+
},
|
|
212
|
+
discovery: toStoredDiscovery(discovery)
|
|
213
|
+
};
|
|
214
|
+
await saveSession(resource, updatedSession);
|
|
215
|
+
return updatedSession;
|
|
209
216
|
}
|
|
210
217
|
async function authorizeSession(resource, existingSession, discovery, fetch, signal) {
|
|
211
218
|
signal?.throwIfAborted();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
219
|
+
assertS256PkceSupport(discovery.authorizationServerMetadata);
|
|
220
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
221
|
+
let currentSession = existingSession;
|
|
222
|
+
let transientRetryAttempted = false;
|
|
223
|
+
let reRegistrationAttempted = false;
|
|
224
|
+
while (true) {
|
|
225
|
+
const loopback = await createLoopbackAuthorizationSession({
|
|
226
|
+
openBrowser: options.browser.openBrowser,
|
|
227
|
+
readLine: options.browser.readLine,
|
|
228
|
+
createServer: options.browser.createServer,
|
|
229
|
+
landingPage: options.browser.landingPage,
|
|
230
|
+
redirectUri: options.browser.redirectUri,
|
|
231
|
+
signal: options.browser.signal === undefined ? signal : signal === undefined ? options.browser.signal : AbortSignal.any([signal, options.browser.signal]),
|
|
232
|
+
timeoutMs: options.browser.timeoutMs
|
|
233
|
+
});
|
|
234
|
+
let resolvedClient = null;
|
|
235
|
+
try {
|
|
236
|
+
resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch, signal);
|
|
237
|
+
const sessionWithoutTokens = {
|
|
238
|
+
resource,
|
|
239
|
+
authorizationServer: discovery.authorizationServer,
|
|
240
|
+
client: resolvedClient.client,
|
|
241
|
+
discovery: toStoredDiscovery(discovery)
|
|
242
|
+
};
|
|
243
|
+
await saveSession(resource, sessionWithoutTokens);
|
|
244
|
+
const verifier = generateCodeVerifier();
|
|
245
|
+
const challenge = generateCodeChallenge(verifier);
|
|
246
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
247
|
+
metadata: discovery.authorizationServerMetadata,
|
|
248
|
+
resource,
|
|
249
|
+
clientId: resolvedClient.client.clientId,
|
|
250
|
+
redirectUri: loopback.redirectUri,
|
|
251
|
+
codeChallenge: challenge,
|
|
252
|
+
clientMetadata: getClientMetadata(options.client)
|
|
231
253
|
});
|
|
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;
|
|
254
|
+
const code = await loopback.waitForCode(authorizationUrl);
|
|
255
|
+
const tokens = await exchangeAuthorizationCode({
|
|
256
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
257
|
+
clientId: resolvedClient.client.clientId,
|
|
258
|
+
clientSecret: resolvedClient.client.clientSecret,
|
|
259
|
+
code,
|
|
260
|
+
codeVerifier: verifier,
|
|
261
|
+
redirectUri: loopback.redirectUri,
|
|
262
|
+
resource,
|
|
263
|
+
fetch, signal,
|
|
264
|
+
now
|
|
265
|
+
});
|
|
266
|
+
const session = {
|
|
267
|
+
...sessionWithoutTokens,
|
|
268
|
+
tokens
|
|
269
|
+
};
|
|
270
|
+
await saveSession(resource, session);
|
|
271
|
+
return session;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
signal?.throwIfAborted();
|
|
275
|
+
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
276
|
+
reRegistrationAttempted = true;
|
|
277
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
278
|
+
await clearSession(resource);
|
|
279
|
+
currentSession = null;
|
|
280
|
+
continue;
|
|
287
281
|
}
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
283
|
+
transientRetryAttempted = true;
|
|
284
|
+
await clearSession(resource);
|
|
285
|
+
currentSession = null;
|
|
286
|
+
continue;
|
|
290
287
|
}
|
|
288
|
+
throw error;
|
|
291
289
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
authorizationPromises.set(resource, finalPromise);
|
|
297
|
-
return finalPromise;
|
|
290
|
+
finally {
|
|
291
|
+
loopback.close();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
298
294
|
}
|
|
299
295
|
async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
|
|
300
296
|
parentSignal?.throwIfAborted();
|
|
@@ -454,6 +450,10 @@ function resolveDiscovery(discovery, session) {
|
|
|
454
450
|
authorizationServerMetadata: metadata
|
|
455
451
|
};
|
|
456
452
|
}
|
|
453
|
+
function sameTokenGrant(left, right) {
|
|
454
|
+
return left.accessToken === right.accessToken && left.refreshToken === right.refreshToken &&
|
|
455
|
+
left.tokenType === right.tokenType && left.expiresAt === right.expiresAt && left.scope === right.scope;
|
|
456
|
+
}
|
|
457
457
|
function clearSessionTokens(session) {
|
|
458
458
|
const nextSession = { ...session };
|
|
459
459
|
delete nextSession.tokens;
|
|
@@ -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>;
|
|
@@ -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
|
+
}
|
|
@@ -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
|
} | {
|
|
@@ -84,6 +88,11 @@ export interface OAuthSessionStore {
|
|
|
84
88
|
load(resource: string): Promise<StoredOAuthSession | null>;
|
|
85
89
|
save(resource: string, session: StoredOAuthSession): Promise<void>;
|
|
86
90
|
clear(resource: string): Promise<void>;
|
|
91
|
+
/** Backend-wide lock covering a complete read/redeem/write transaction. */
|
|
92
|
+
withLock?<T>(resource: string, operation: () => Promise<T>, options: {
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
timeoutMs: number;
|
|
95
|
+
}): Promise<T>;
|
|
87
96
|
}
|
|
88
97
|
export interface DefaultOAuthClientProviderOptions {
|
|
89
98
|
client: {
|
|
@@ -99,6 +108,8 @@ export interface DefaultOAuthClientProviderOptions {
|
|
|
99
108
|
};
|
|
100
109
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
101
110
|
allowInteractive?: boolean;
|
|
111
|
+
/** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
|
|
112
|
+
sessionLockTimeoutMs?: number;
|
|
102
113
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
103
114
|
initialGrant?: {
|
|
104
115
|
resource: string;
|
|
@@ -110,7 +110,7 @@ interface OAuthClientProvider {
|
|
|
110
110
|
headers: Headers;
|
|
111
111
|
fetch: OAuthMetadataFetch;
|
|
112
112
|
signal?: AbortSignal;
|
|
113
|
-
}): Promise<void> | void;
|
|
113
|
+
}): Promise<StoredOAuthTokens | void> | StoredOAuthTokens | void;
|
|
114
114
|
handleUnauthorized(input: {
|
|
115
115
|
requestUrl: URL;
|
|
116
116
|
response: Response;
|
|
@@ -118,6 +118,10 @@ interface OAuthClientProvider {
|
|
|
118
118
|
discovery: OAuthDiscoveryResult;
|
|
119
119
|
fetch: OAuthMetadataFetch;
|
|
120
120
|
signal?: AbortSignal;
|
|
121
|
+
/** Headers actually attached to this rejected request. */
|
|
122
|
+
requestHeaders?: Headers;
|
|
123
|
+
/** Owned snapshot returned when this request was authorized, or null if absent. */
|
|
124
|
+
presentedTokens?: StoredOAuthTokens | null;
|
|
121
125
|
}): Promise<{
|
|
122
126
|
action: "retry";
|
|
123
127
|
} | {
|
|
@@ -161,6 +165,11 @@ interface OAuthSessionStore {
|
|
|
161
165
|
load(resource: string): Promise<StoredOAuthSession | null>;
|
|
162
166
|
save(resource: string, session: StoredOAuthSession): Promise<void>;
|
|
163
167
|
clear(resource: string): Promise<void>;
|
|
168
|
+
/** Backend-wide lock covering a complete read/redeem/write transaction. */
|
|
169
|
+
withLock?<T>(resource: string, operation: () => Promise<T>, options: {
|
|
170
|
+
signal?: AbortSignal;
|
|
171
|
+
timeoutMs: number;
|
|
172
|
+
}): Promise<T>;
|
|
164
173
|
}
|
|
165
174
|
interface DefaultOAuthClientProviderOptions {
|
|
166
175
|
client: {
|
|
@@ -176,6 +185,8 @@ interface DefaultOAuthClientProviderOptions {
|
|
|
176
185
|
};
|
|
177
186
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
178
187
|
allowInteractive?: boolean;
|
|
188
|
+
/** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
|
|
189
|
+
sessionLockTimeoutMs?: number;
|
|
179
190
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
180
191
|
initialGrant?: {
|
|
181
192
|
resource: string;
|
|
@@ -649,6 +660,7 @@ declare class HttpTransport implements McpTransport {
|
|
|
649
660
|
private readonly oauthMetadataDiscovery;
|
|
650
661
|
private readonly inFlightFetchAbortControllers;
|
|
651
662
|
private readonly inFlightOAuthAbortControllers;
|
|
663
|
+
private readonly oauthRequestTokens;
|
|
652
664
|
private readonly openResponseReaders;
|
|
653
665
|
private readonly modernRequests;
|
|
654
666
|
private modernMode;
|
|
@@ -4585,6 +4585,55 @@ function normalizeBearerTokenType(value) {
|
|
|
4585
4585
|
return value.toLowerCase() === "bearer" ? "Bearer" : null;
|
|
4586
4586
|
}
|
|
4587
4587
|
|
|
4588
|
+
// ../mcp-oauth/dist/client/session-transaction.js
|
|
4589
|
+
var queues = /* @__PURE__ */ new WeakMap();
|
|
4590
|
+
async function withOAuthSessionTransaction(store, resource, operation, options = {}) {
|
|
4591
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
4592
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
|
|
4593
|
+
throw new Error("sessionLockTimeoutMs must be an integer from 1 to 2147483647 milliseconds");
|
|
4594
|
+
options.signal?.throwIfAborted();
|
|
4595
|
+
const started = performance.now();
|
|
4596
|
+
const pending = queues.get(store) ?? /* @__PURE__ */ new Map();
|
|
4597
|
+
queues.set(store, pending);
|
|
4598
|
+
const previous = pending.get(resource) ?? Promise.resolve();
|
|
4599
|
+
let release;
|
|
4600
|
+
const current = new Promise((resolve) => {
|
|
4601
|
+
release = resolve;
|
|
4602
|
+
});
|
|
4603
|
+
const tail = previous.then(() => current);
|
|
4604
|
+
pending.set(resource, tail);
|
|
4605
|
+
try {
|
|
4606
|
+
let timer;
|
|
4607
|
+
let rejectWait;
|
|
4608
|
+
const waiting = new Promise((resolve, reject) => {
|
|
4609
|
+
rejectWait = reject;
|
|
4610
|
+
previous.then(resolve, reject);
|
|
4611
|
+
});
|
|
4612
|
+
const abort = () => rejectWait(options.signal?.reason);
|
|
4613
|
+
try {
|
|
4614
|
+
timer = setTimeout(() => rejectWait(new Error("Timed out waiting for OAuth session transaction lock")), timeoutMs);
|
|
4615
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
4616
|
+
if (options.signal?.aborted)
|
|
4617
|
+
abort();
|
|
4618
|
+
await waiting;
|
|
4619
|
+
} finally {
|
|
4620
|
+
clearTimeout(timer);
|
|
4621
|
+
options.signal?.removeEventListener("abort", abort);
|
|
4622
|
+
}
|
|
4623
|
+
options.signal?.throwIfAborted();
|
|
4624
|
+
return store.withLock === void 0 ? await operation() : await store.withLock(resource, operation, {
|
|
4625
|
+
signal: options.signal,
|
|
4626
|
+
timeoutMs: Math.max(0, timeoutMs - (performance.now() - started))
|
|
4627
|
+
});
|
|
4628
|
+
} finally {
|
|
4629
|
+
release();
|
|
4630
|
+
void tail.then(() => {
|
|
4631
|
+
if (pending.get(resource) === tail)
|
|
4632
|
+
pending.delete(resource);
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4588
4637
|
// ../mcp-oauth/dist/client/default-oauth-client-provider.js
|
|
4589
4638
|
var MAX_JS_DATE_MS3 = 864e13;
|
|
4590
4639
|
function createOAuthClientProvider(options) {
|
|
@@ -4599,8 +4648,6 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4599
4648
|
const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore);
|
|
4600
4649
|
const now = options.now ?? Date.now;
|
|
4601
4650
|
const registeredClients = /* @__PURE__ */ new Map();
|
|
4602
|
-
const refreshPromises = /* @__PURE__ */ new Map();
|
|
4603
|
-
const authorizationPromises = /* @__PURE__ */ new Map();
|
|
4604
4651
|
if (options.initialGrant !== void 0) {
|
|
4605
4652
|
let resource;
|
|
4606
4653
|
try {
|
|
@@ -4634,13 +4681,14 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4634
4681
|
const accessToken = session?.tokens?.accessToken;
|
|
4635
4682
|
if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl && initialGrant.tokens !== void 0 && !isExpired(initialGrant.tokens, now)) {
|
|
4636
4683
|
input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
|
|
4637
|
-
return;
|
|
4684
|
+
return { ...initialGrant.tokens };
|
|
4638
4685
|
}
|
|
4639
4686
|
if (session === null || accessToken === void 0 || session.tokens === void 0 || isExpired(session.tokens, now)) {
|
|
4640
4687
|
return;
|
|
4641
4688
|
}
|
|
4642
4689
|
assertRequestMatchesResource(requestUrl, session.resource);
|
|
4643
4690
|
input.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
4691
|
+
return { ...session.tokens };
|
|
4644
4692
|
},
|
|
4645
4693
|
async handleUnauthorized(input) {
|
|
4646
4694
|
try {
|
|
@@ -4648,11 +4696,28 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4648
4696
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
4649
4697
|
const resource = canonicalizeResourceIndicator(input.discovery.resource);
|
|
4650
4698
|
assertRequestMatchesResource(requestUrl, resource);
|
|
4651
|
-
const
|
|
4699
|
+
const cached = await loadSession(resource);
|
|
4700
|
+
const currentTokens = cached?.tokens ?? (!initialGrantConsumed && initialGrant?.resource === resource ? initialGrant.tokens : void 0);
|
|
4701
|
+
let rejectedCurrentGrant = hasCachedAccessToken(cached) || !initialGrantConsumed && initialGrant?.resource === resource;
|
|
4702
|
+
let presentedTokens = input.presentedTokens;
|
|
4703
|
+
if (input.presentedTokens !== void 0) {
|
|
4704
|
+
rejectedCurrentGrant = false;
|
|
4705
|
+
if (input.presentedTokens !== null) {
|
|
4706
|
+
const presented = normalizeStoredTokens(input.presentedTokens);
|
|
4707
|
+
const header = input.requestHeaders?.get("Authorization") ?? "";
|
|
4708
|
+
const separator = header.indexOf(" ");
|
|
4709
|
+
if (presented === void 0 || header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
|
|
4710
|
+
throw new Error("OAuth rejected-request provenance does not match its authorization header");
|
|
4711
|
+
presentedTokens = presented;
|
|
4712
|
+
rejectedCurrentGrant = currentTokens !== void 0 && sameTokenGrant(currentTokens, presented);
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
const challengeError = input.challenge?.params.error;
|
|
4716
|
+
const forceRefresh = rejectedCurrentGrant && (challengeError === "invalid_token" || input.presentedTokens !== void 0 && challengeError === void 0);
|
|
4652
4717
|
const session = await ensureAuthorizedSession(resource, {
|
|
4653
4718
|
...input.discovery,
|
|
4654
4719
|
resource
|
|
4655
|
-
}, input.fetch, true, forceRefresh, input.signal);
|
|
4720
|
+
}, input.fetch, true, forceRefresh, input.signal, presentedTokens);
|
|
4656
4721
|
if (session?.tokens?.accessToken === void 0) {
|
|
4657
4722
|
return { action: "fail" };
|
|
4658
4723
|
}
|
|
@@ -4666,204 +4731,185 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4666
4731
|
}
|
|
4667
4732
|
}
|
|
4668
4733
|
};
|
|
4669
|
-
async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal) {
|
|
4734
|
+
async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
4670
4735
|
signal?.throwIfAborted();
|
|
4671
4736
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
4677
|
-
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
4678
|
-
}
|
|
4679
|
-
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource || getOwnString2(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer || discovery !== void 0 && discovery.authorizationServer !== session.authorizationServer)) {
|
|
4680
|
-
await clearSession(canonicalResource);
|
|
4681
|
-
session = null;
|
|
4682
|
-
}
|
|
4683
|
-
if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
|
|
4684
|
-
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4685
|
-
session = {
|
|
4686
|
-
resource: canonicalResource,
|
|
4687
|
-
authorizationServer: discovery.authorizationServer,
|
|
4688
|
-
client: initialGrant.client,
|
|
4689
|
-
tokens: initialGrant.tokens,
|
|
4690
|
-
discovery: toStoredDiscovery(discovery)
|
|
4691
|
-
};
|
|
4692
|
-
await saveSession(canonicalResource, session);
|
|
4693
|
-
initialGrantConsumed = true;
|
|
4737
|
+
return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
|
|
4738
|
+
let session = await loadSession(canonicalResource);
|
|
4739
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
4740
|
+
initialGrantConsumed = true;
|
|
4694
4741
|
signal?.throwIfAborted();
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
if (session
|
|
4742
|
+
if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
4743
|
+
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
4744
|
+
}
|
|
4745
|
+
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource || getOwnString2(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer || discovery !== void 0 && discovery.authorizationServer !== session.authorizationServer)) {
|
|
4746
|
+
await clearSession(canonicalResource);
|
|
4747
|
+
session = null;
|
|
4748
|
+
}
|
|
4749
|
+
if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
|
|
4750
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4751
|
+
session = {
|
|
4752
|
+
resource: canonicalResource,
|
|
4753
|
+
authorizationServer: discovery.authorizationServer,
|
|
4754
|
+
client: initialGrant.client,
|
|
4755
|
+
tokens: initialGrant.tokens,
|
|
4756
|
+
discovery: toStoredDiscovery(discovery)
|
|
4757
|
+
};
|
|
4758
|
+
await saveSession(canonicalResource, session);
|
|
4759
|
+
initialGrantConsumed = true;
|
|
4760
|
+
signal?.throwIfAborted();
|
|
4761
|
+
}
|
|
4762
|
+
if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
4763
|
+
forceRefresh = false;
|
|
4764
|
+
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
4765
|
+
if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
4703
4766
|
return session;
|
|
4704
4767
|
}
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4768
|
+
if (session?.tokens?.refreshToken !== void 0 && sessionDiscovery !== void 0 && (forceRefresh || isExpired(session.tokens, now))) {
|
|
4769
|
+
session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch2, signal);
|
|
4770
|
+
if (session?.tokens !== void 0 && !isExpired(session.tokens, now)) {
|
|
4771
|
+
return session;
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
if (forceRefresh && session?.tokens !== void 0) {
|
|
4775
|
+
session = clearSessionTokens(session);
|
|
4776
|
+
await saveSession(canonicalResource, session);
|
|
4777
|
+
}
|
|
4778
|
+
if (!allowInteractive || sessionDiscovery === void 0) {
|
|
4779
|
+
return session;
|
|
4780
|
+
}
|
|
4781
|
+
if (options.allowInteractive === false)
|
|
4782
|
+
throw new Error("OAuth interactive authorization is disabled");
|
|
4783
|
+
return authorizeSession(canonicalResource, session, sessionDiscovery, fetch2, signal);
|
|
4784
|
+
}, { signal, timeoutMs: options.sessionLockTimeoutMs });
|
|
4716
4785
|
}
|
|
4717
4786
|
async function refreshSession(resource, session, discovery, fetch2, signal) {
|
|
4718
4787
|
signal?.throwIfAborted();
|
|
4719
4788
|
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
return inFlight;
|
|
4789
|
+
if (session.tokens?.refreshToken === void 0) {
|
|
4790
|
+
return session;
|
|
4723
4791
|
}
|
|
4724
|
-
|
|
4792
|
+
let refreshAttempted = false;
|
|
4793
|
+
let refreshedTokens;
|
|
4794
|
+
while (true) {
|
|
4725
4795
|
try {
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
4753
|
-
await clearSession(resource);
|
|
4754
|
-
return null;
|
|
4755
|
-
}
|
|
4756
|
-
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
4757
|
-
refreshAttempted = true;
|
|
4758
|
-
continue;
|
|
4759
|
-
}
|
|
4760
|
-
throw error;
|
|
4761
|
-
}
|
|
4796
|
+
refreshedTokens = await refreshAccessToken({
|
|
4797
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
4798
|
+
clientId: session.client.clientId,
|
|
4799
|
+
clientSecret: session.client.clientSecret,
|
|
4800
|
+
refreshToken: session.tokens.refreshToken,
|
|
4801
|
+
resource,
|
|
4802
|
+
fetch: fetch2,
|
|
4803
|
+
signal,
|
|
4804
|
+
now
|
|
4805
|
+
});
|
|
4806
|
+
break;
|
|
4807
|
+
} catch (error) {
|
|
4808
|
+
signal?.throwIfAborted();
|
|
4809
|
+
if (error instanceof OAuthError && error.error === "invalid_grant") {
|
|
4810
|
+
const clearedSession = clearSessionTokens(session);
|
|
4811
|
+
await saveSession(resource, clearedSession);
|
|
4812
|
+
return clearedSession;
|
|
4813
|
+
}
|
|
4814
|
+
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
4815
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
4816
|
+
await clearSession(resource);
|
|
4817
|
+
return null;
|
|
4818
|
+
}
|
|
4819
|
+
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
4820
|
+
refreshAttempted = true;
|
|
4821
|
+
continue;
|
|
4762
4822
|
}
|
|
4763
|
-
|
|
4764
|
-
...session,
|
|
4765
|
-
tokens: {
|
|
4766
|
-
...refreshedTokens,
|
|
4767
|
-
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
4768
|
-
},
|
|
4769
|
-
discovery: toStoredDiscovery(discovery)
|
|
4770
|
-
};
|
|
4771
|
-
await saveSession(resource, updatedSession);
|
|
4772
|
-
return updatedSession;
|
|
4773
|
-
} finally {
|
|
4774
|
-
refreshPromises.delete(resource);
|
|
4823
|
+
throw error;
|
|
4775
4824
|
}
|
|
4776
|
-
}
|
|
4777
|
-
|
|
4778
|
-
|
|
4825
|
+
}
|
|
4826
|
+
const updatedSession = {
|
|
4827
|
+
...session,
|
|
4828
|
+
tokens: {
|
|
4829
|
+
...refreshedTokens,
|
|
4830
|
+
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
4831
|
+
},
|
|
4832
|
+
discovery: toStoredDiscovery(discovery)
|
|
4833
|
+
};
|
|
4834
|
+
await saveSession(resource, updatedSession);
|
|
4835
|
+
return updatedSession;
|
|
4779
4836
|
}
|
|
4780
4837
|
async function authorizeSession(resource, existingSession, discovery, fetch2, signal) {
|
|
4781
4838
|
signal?.throwIfAborted();
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4839
|
+
assertS256PkceSupport(discovery.authorizationServerMetadata);
|
|
4840
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4841
|
+
let currentSession = existingSession;
|
|
4842
|
+
let transientRetryAttempted = false;
|
|
4843
|
+
let reRegistrationAttempted = false;
|
|
4844
|
+
while (true) {
|
|
4845
|
+
const loopback = await createLoopbackAuthorizationSession({
|
|
4846
|
+
openBrowser: options.browser.openBrowser,
|
|
4847
|
+
readLine: options.browser.readLine,
|
|
4848
|
+
createServer: options.browser.createServer,
|
|
4849
|
+
landingPage: options.browser.landingPage,
|
|
4850
|
+
redirectUri: options.browser.redirectUri,
|
|
4851
|
+
signal: options.browser.signal === void 0 ? signal : signal === void 0 ? options.browser.signal : AbortSignal.any([signal, options.browser.signal]),
|
|
4852
|
+
timeoutMs: options.browser.timeoutMs
|
|
4853
|
+
});
|
|
4854
|
+
let resolvedClient = null;
|
|
4855
|
+
try {
|
|
4856
|
+
resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch2, signal);
|
|
4857
|
+
const sessionWithoutTokens = {
|
|
4858
|
+
resource,
|
|
4859
|
+
authorizationServer: discovery.authorizationServer,
|
|
4860
|
+
client: resolvedClient.client,
|
|
4861
|
+
discovery: toStoredDiscovery(discovery)
|
|
4862
|
+
};
|
|
4863
|
+
await saveSession(resource, sessionWithoutTokens);
|
|
4864
|
+
const verifier = generateCodeVerifier();
|
|
4865
|
+
const challenge = generateCodeChallenge(verifier);
|
|
4866
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
4867
|
+
metadata: discovery.authorizationServerMetadata,
|
|
4868
|
+
resource,
|
|
4869
|
+
clientId: resolvedClient.client.clientId,
|
|
4870
|
+
redirectUri: loopback.redirectUri,
|
|
4871
|
+
codeChallenge: challenge,
|
|
4872
|
+
clientMetadata: getClientMetadata(options.client)
|
|
4801
4873
|
});
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
const session = {
|
|
4836
|
-
...sessionWithoutTokens,
|
|
4837
|
-
tokens
|
|
4838
|
-
};
|
|
4839
|
-
await saveSession(resource, session);
|
|
4840
|
-
return session;
|
|
4841
|
-
} catch (error) {
|
|
4842
|
-
signal?.throwIfAborted();
|
|
4843
|
-
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
4844
|
-
reRegistrationAttempted = true;
|
|
4845
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
4846
|
-
await clearSession(resource);
|
|
4847
|
-
currentSession = null;
|
|
4848
|
-
continue;
|
|
4849
|
-
}
|
|
4850
|
-
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
4851
|
-
transientRetryAttempted = true;
|
|
4852
|
-
await clearSession(resource);
|
|
4853
|
-
currentSession = null;
|
|
4854
|
-
continue;
|
|
4855
|
-
}
|
|
4856
|
-
throw error;
|
|
4857
|
-
} finally {
|
|
4858
|
-
loopback.close();
|
|
4874
|
+
const code = await loopback.waitForCode(authorizationUrl);
|
|
4875
|
+
const tokens = await exchangeAuthorizationCode({
|
|
4876
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
4877
|
+
clientId: resolvedClient.client.clientId,
|
|
4878
|
+
clientSecret: resolvedClient.client.clientSecret,
|
|
4879
|
+
code,
|
|
4880
|
+
codeVerifier: verifier,
|
|
4881
|
+
redirectUri: loopback.redirectUri,
|
|
4882
|
+
resource,
|
|
4883
|
+
fetch: fetch2,
|
|
4884
|
+
signal,
|
|
4885
|
+
now
|
|
4886
|
+
});
|
|
4887
|
+
const session = {
|
|
4888
|
+
...sessionWithoutTokens,
|
|
4889
|
+
tokens
|
|
4890
|
+
};
|
|
4891
|
+
await saveSession(resource, session);
|
|
4892
|
+
return session;
|
|
4893
|
+
} catch (error) {
|
|
4894
|
+
signal?.throwIfAborted();
|
|
4895
|
+
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
4896
|
+
reRegistrationAttempted = true;
|
|
4897
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
4898
|
+
await clearSession(resource);
|
|
4899
|
+
currentSession = null;
|
|
4900
|
+
continue;
|
|
4901
|
+
}
|
|
4902
|
+
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
4903
|
+
transientRetryAttempted = true;
|
|
4904
|
+
await clearSession(resource);
|
|
4905
|
+
currentSession = null;
|
|
4906
|
+
continue;
|
|
4859
4907
|
}
|
|
4908
|
+
throw error;
|
|
4909
|
+
} finally {
|
|
4910
|
+
loopback.close();
|
|
4860
4911
|
}
|
|
4861
|
-
}
|
|
4862
|
-
const finalPromise = promise.finally(() => {
|
|
4863
|
-
authorizationPromises.delete(resource);
|
|
4864
|
-
});
|
|
4865
|
-
authorizationPromises.set(resource, finalPromise);
|
|
4866
|
-
return finalPromise;
|
|
4912
|
+
}
|
|
4867
4913
|
}
|
|
4868
4914
|
async function resolveClient(existingSession, discovery, redirectUri, fetch2, parentSignal) {
|
|
4869
4915
|
parentSignal?.throwIfAborted();
|
|
@@ -5014,6 +5060,9 @@ function resolveDiscovery(discovery, session) {
|
|
|
5014
5060
|
authorizationServerMetadata: metadata
|
|
5015
5061
|
};
|
|
5016
5062
|
}
|
|
5063
|
+
function sameTokenGrant(left, right) {
|
|
5064
|
+
return left.accessToken === right.accessToken && left.refreshToken === right.refreshToken && left.tokenType === right.tokenType && left.expiresAt === right.expiresAt && left.scope === right.scope;
|
|
5065
|
+
}
|
|
5017
5066
|
function clearSessionTokens(session) {
|
|
5018
5067
|
const nextSession = { ...session };
|
|
5019
5068
|
delete nextSession.tokens;
|
|
@@ -6789,6 +6838,7 @@ var HttpTransport = class {
|
|
|
6789
6838
|
oauthMetadataDiscovery;
|
|
6790
6839
|
inFlightFetchAbortControllers = /* @__PURE__ */ new Set();
|
|
6791
6840
|
inFlightOAuthAbortControllers = /* @__PURE__ */ new Set();
|
|
6841
|
+
oauthRequestTokens = /* @__PURE__ */ new WeakMap();
|
|
6792
6842
|
openResponseReaders = /* @__PURE__ */ new Set();
|
|
6793
6843
|
modernRequests = /* @__PURE__ */ new Map();
|
|
6794
6844
|
modernMode = false;
|
|
@@ -7059,7 +7109,7 @@ var HttpTransport = class {
|
|
|
7059
7109
|
}
|
|
7060
7110
|
async authorizeRequestHeaders(headers, signal) {
|
|
7061
7111
|
signal?.throwIfAborted();
|
|
7062
|
-
await this.oauthProvider?.authorizeRequest?.({
|
|
7112
|
+
const tokens = await this.oauthProvider?.authorizeRequest?.({
|
|
7063
7113
|
requestUrl: new URL(this.url),
|
|
7064
7114
|
headers,
|
|
7065
7115
|
signal,
|
|
@@ -7068,6 +7118,7 @@ var HttpTransport = class {
|
|
|
7068
7118
|
signal: signal === void 0 ? init?.signal : init?.signal == null ? signal : AbortSignal.any([signal, init.signal])
|
|
7069
7119
|
})
|
|
7070
7120
|
});
|
|
7121
|
+
if (tokens !== void 0) this.oauthRequestTokens.set(headers, { ...tokens });
|
|
7071
7122
|
signal?.throwIfAborted();
|
|
7072
7123
|
return headers;
|
|
7073
7124
|
}
|
|
@@ -7211,7 +7262,7 @@ var HttpTransport = class {
|
|
|
7211
7262
|
const message = responseBody.length === 0 ? `HTTP transport POST failed (${statusDescriptor})` : `HTTP transport POST failed (${statusDescriptor}): ${responseBody}`;
|
|
7212
7263
|
throw new HttpTransportError(message, response.status, "POST");
|
|
7213
7264
|
}
|
|
7214
|
-
async maybeHandleUnauthorizedResponse(response, signal) {
|
|
7265
|
+
async maybeHandleUnauthorizedResponse(response, signal, requestHeaders, presentedTokens) {
|
|
7215
7266
|
if (response.status !== 401 || this.oauthProvider === void 0) {
|
|
7216
7267
|
return false;
|
|
7217
7268
|
}
|
|
@@ -7232,6 +7283,8 @@ var HttpTransport = class {
|
|
|
7232
7283
|
challenge,
|
|
7233
7284
|
discovery,
|
|
7234
7285
|
signal,
|
|
7286
|
+
requestHeaders: new Headers(requestHeaders),
|
|
7287
|
+
presentedTokens,
|
|
7235
7288
|
fetch: (url, init) => fetchMcpResponse(this.fetchImpl, url, {
|
|
7236
7289
|
...init,
|
|
7237
7290
|
signal: init?.signal == null ? signal : AbortSignal.any([signal, init.signal])
|
|
@@ -7372,16 +7425,20 @@ var HttpTransport = class {
|
|
|
7372
7425
|
const request = async () => {
|
|
7373
7426
|
controller.signal.throwIfAborted();
|
|
7374
7427
|
const headers = await input.createHeaders(controller.signal);
|
|
7428
|
+
const headerSnapshot = new Headers(headers);
|
|
7429
|
+
const tokens = this.oauthRequestTokens.get(headers) ?? null;
|
|
7375
7430
|
controller.signal.throwIfAborted();
|
|
7376
|
-
|
|
7431
|
+
const response = await this.fetchWithAbort(input.url ?? this.url, {
|
|
7377
7432
|
method: input.method,
|
|
7378
7433
|
headers,
|
|
7379
7434
|
body: input.body
|
|
7380
7435
|
}, controller);
|
|
7436
|
+
return { response, headers: headerSnapshot, tokens };
|
|
7381
7437
|
};
|
|
7382
7438
|
try {
|
|
7383
|
-
let
|
|
7384
|
-
if (await this.maybeHandleUnauthorizedResponse(response, controller.signal))
|
|
7439
|
+
let attempt = await request();
|
|
7440
|
+
if (await this.maybeHandleUnauthorizedResponse(attempt.response, controller.signal, attempt.headers, attempt.tokens)) attempt = await request();
|
|
7441
|
+
const response = attempt.response;
|
|
7385
7442
|
const oauthError = this.oauthProvider === void 0 ? null : this.readOAuthChallengeError(response);
|
|
7386
7443
|
if (oauthError !== null) {
|
|
7387
7444
|
void response.body?.cancel().catch(() => void 0);
|