tiny-http-mcp-server 0.1.25 → 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.
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.25",
21
+ "version": "0.1.26",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -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
@@ -106,6 +107,16 @@ Input tokens are copied and invalid expiry values fail before authorization.
106
107
 
107
108
  `createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
108
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
+
109
120
  ## Environment Variables
110
121
 
111
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 {
@@ -115,205 +114,183 @@ 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
- let session = await loadSession(canonicalResource);
119
- if (session !== null && initialGrant?.resource === canonicalResource)
120
- initialGrantConsumed = true;
121
- signal?.throwIfAborted();
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
- 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)) {
144
- return session;
145
- }
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)) {
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)) {
151
144
  return session;
152
145
  }
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);
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 });
164
165
  }
165
166
  async function refreshSession(resource, session, discovery, fetch, signal) {
166
167
  signal?.throwIfAborted();
167
168
  assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
168
- const inFlight = refreshPromises.get(resource);
169
- if (inFlight !== undefined) {
170
- return inFlight;
169
+ if (session.tokens?.refreshToken === undefined) {
170
+ return session;
171
171
  }
172
- const promise = (async () => {
172
+ let refreshAttempted = false;
173
+ let refreshedTokens;
174
+ while (true) {
173
175
  try {
174
- if (session.tokens?.refreshToken === undefined) {
175
- return session;
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;
176
193
  }
177
- let refreshAttempted = false;
178
- let refreshedTokens;
179
- while (true) {
180
- try {
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
- }
194
+ if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
195
+ await clearRegisteredClient(discovery.authorizationServer);
196
+ await clearSession(resource);
197
+ return null;
210
198
  }
211
- const updatedSession = {
212
- ...session,
213
- tokens: {
214
- ...refreshedTokens,
215
- refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
216
- },
217
- discovery: toStoredDiscovery(discovery)
218
- };
219
- await saveSession(resource, updatedSession);
220
- return updatedSession;
221
- }
222
- finally {
223
- refreshPromises.delete(resource);
199
+ if (!refreshAttempted && isRetryableOAuthError(error)) {
200
+ refreshAttempted = true;
201
+ continue;
202
+ }
203
+ throw error;
224
204
  }
225
- })();
226
- refreshPromises.set(resource, promise);
227
- return promise;
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;
228
216
  }
229
217
  async function authorizeSession(resource, existingSession, discovery, fetch, signal) {
230
218
  signal?.throwIfAborted();
231
- const inFlight = authorizationPromises.get(resource);
232
- if (inFlight !== undefined) {
233
- return inFlight;
234
- }
235
- const promise = (async () => {
236
- assertS256PkceSupport(discovery.authorizationServerMetadata);
237
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
238
- let currentSession = existingSession;
239
- let transientRetryAttempted = false;
240
- let reRegistrationAttempted = false;
241
- while (true) {
242
- const loopback = await createLoopbackAuthorizationSession({
243
- openBrowser: options.browser.openBrowser,
244
- readLine: options.browser.readLine,
245
- createServer: options.browser.createServer,
246
- landingPage: options.browser.landingPage,
247
- redirectUri: options.browser.redirectUri,
248
- signal: options.browser.signal === undefined ? signal : signal === undefined ? options.browser.signal : AbortSignal.any([signal, options.browser.signal]),
249
- timeoutMs: options.browser.timeoutMs
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)
250
253
  });
251
- let resolvedClient = null;
252
- try {
253
- resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch, signal);
254
- const sessionWithoutTokens = {
255
- resource,
256
- authorizationServer: discovery.authorizationServer,
257
- client: resolvedClient.client,
258
- discovery: toStoredDiscovery(discovery)
259
- };
260
- await saveSession(resource, sessionWithoutTokens);
261
- const verifier = generateCodeVerifier();
262
- const challenge = generateCodeChallenge(verifier);
263
- const authorizationUrl = buildAuthorizationUrl({
264
- metadata: discovery.authorizationServerMetadata,
265
- resource,
266
- clientId: resolvedClient.client.clientId,
267
- redirectUri: loopback.redirectUri,
268
- codeChallenge: challenge,
269
- clientMetadata: getClientMetadata(options.client)
270
- });
271
- const code = await loopback.waitForCode(authorizationUrl);
272
- const tokens = await exchangeAuthorizationCode({
273
- tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
274
- clientId: resolvedClient.client.clientId,
275
- clientSecret: resolvedClient.client.clientSecret,
276
- code,
277
- codeVerifier: verifier,
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;
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;
306
281
  }
307
- finally {
308
- loopback.close();
282
+ if (!transientRetryAttempted && isRetryableOAuthError(error)) {
283
+ transientRetryAttempted = true;
284
+ await clearSession(resource);
285
+ currentSession = null;
286
+ continue;
309
287
  }
288
+ throw error;
310
289
  }
311
- })();
312
- const finalPromise = promise.finally(() => {
313
- authorizationPromises.delete(resource);
314
- });
315
- authorizationPromises.set(resource, finalPromise);
316
- return finalPromise;
290
+ finally {
291
+ loopback.close();
292
+ }
293
+ }
317
294
  }
318
295
  async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
319
296
  parentSignal?.throwIfAborted();
@@ -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
+ }
@@ -88,6 +88,11 @@ export interface OAuthSessionStore {
88
88
  load(resource: string): Promise<StoredOAuthSession | null>;
89
89
  save(resource: string, session: StoredOAuthSession): Promise<void>;
90
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>;
91
96
  }
92
97
  export interface DefaultOAuthClientProviderOptions {
93
98
  client: {
@@ -103,6 +108,8 @@ export interface DefaultOAuthClientProviderOptions {
103
108
  };
104
109
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
105
110
  allowInteractive?: boolean;
111
+ /** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
112
+ sessionLockTimeoutMs?: number;
106
113
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
107
114
  initialGrant?: {
108
115
  resource: string;
@@ -165,6 +165,11 @@ interface OAuthSessionStore {
165
165
  load(resource: string): Promise<StoredOAuthSession | null>;
166
166
  save(resource: string, session: StoredOAuthSession): Promise<void>;
167
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>;
168
173
  }
169
174
  interface DefaultOAuthClientProviderOptions {
170
175
  client: {
@@ -180,6 +185,8 @@ interface DefaultOAuthClientProviderOptions {
180
185
  };
181
186
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
182
187
  allowInteractive?: boolean;
188
+ /** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
189
+ sessionLockTimeoutMs?: number;
183
190
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
184
191
  initialGrant?: {
185
192
  resource: string;
@@ -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 {
@@ -4687,203 +4734,182 @@ function createDefaultOAuthClientProvider(options) {
4687
4734
  async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
4688
4735
  signal?.throwIfAborted();
4689
4736
  const canonicalResource = canonicalizeResourceIndicator(resource);
4690
- let session = await loadSession(canonicalResource);
4691
- if (session !== null && initialGrant?.resource === canonicalResource)
4692
- initialGrantConsumed = true;
4693
- signal?.throwIfAborted();
4694
- if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
4695
- throw new Error("OAuth discovery authorization-server issuer mismatch");
4696
- }
4697
- if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource || getOwnString2(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer || discovery !== void 0 && discovery.authorizationServer !== session.authorizationServer)) {
4698
- await clearSession(canonicalResource);
4699
- session = null;
4700
- }
4701
- if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
4702
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
4703
- session = {
4704
- resource: canonicalResource,
4705
- authorizationServer: discovery.authorizationServer,
4706
- client: initialGrant.client,
4707
- tokens: initialGrant.tokens,
4708
- discovery: toStoredDiscovery(discovery)
4709
- };
4710
- await saveSession(canonicalResource, session);
4711
- initialGrantConsumed = true;
4737
+ return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
4738
+ let session = await loadSession(canonicalResource);
4739
+ if (session !== null && initialGrant?.resource === canonicalResource)
4740
+ initialGrantConsumed = true;
4712
4741
  signal?.throwIfAborted();
4713
- }
4714
- if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
4715
- forceRefresh = false;
4716
- const sessionDiscovery = resolveDiscovery(discovery, session);
4717
- if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
4718
- return session;
4719
- }
4720
- if (session?.tokens?.refreshToken !== void 0 && sessionDiscovery !== void 0 && (forceRefresh || isExpired(session.tokens, now))) {
4721
- session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch2, signal);
4722
- if (session?.tokens !== void 0 && !isExpired(session.tokens, now)) {
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)) {
4723
4766
  return session;
4724
4767
  }
4725
- }
4726
- if (forceRefresh && session?.tokens !== void 0) {
4727
- session = clearSessionTokens(session);
4728
- await saveSession(canonicalResource, session);
4729
- }
4730
- if (!allowInteractive || sessionDiscovery === void 0) {
4731
- return session;
4732
- }
4733
- if (options.allowInteractive === false)
4734
- throw new Error("OAuth interactive authorization is disabled");
4735
- return authorizeSession(canonicalResource, session, sessionDiscovery, fetch2, signal);
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 });
4736
4785
  }
4737
4786
  async function refreshSession(resource, session, discovery, fetch2, signal) {
4738
4787
  signal?.throwIfAborted();
4739
4788
  assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
4740
- const inFlight = refreshPromises.get(resource);
4741
- if (inFlight !== void 0) {
4742
- return inFlight;
4789
+ if (session.tokens?.refreshToken === void 0) {
4790
+ return session;
4743
4791
  }
4744
- const promise = (async () => {
4792
+ let refreshAttempted = false;
4793
+ let refreshedTokens;
4794
+ while (true) {
4745
4795
  try {
4746
- if (session.tokens?.refreshToken === void 0) {
4747
- return session;
4748
- }
4749
- let refreshAttempted = false;
4750
- let refreshedTokens;
4751
- while (true) {
4752
- try {
4753
- refreshedTokens = await refreshAccessToken({
4754
- tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
4755
- clientId: session.client.clientId,
4756
- clientSecret: session.client.clientSecret,
4757
- refreshToken: session.tokens.refreshToken,
4758
- resource,
4759
- fetch: fetch2,
4760
- signal,
4761
- now
4762
- });
4763
- break;
4764
- } catch (error) {
4765
- signal?.throwIfAborted();
4766
- if (error instanceof OAuthError && error.error === "invalid_grant") {
4767
- const clearedSession = clearSessionTokens(session);
4768
- await saveSession(resource, clearedSession);
4769
- return clearedSession;
4770
- }
4771
- if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
4772
- await clearRegisteredClient(discovery.authorizationServer);
4773
- await clearSession(resource);
4774
- return null;
4775
- }
4776
- if (!refreshAttempted && isRetryableOAuthError(error)) {
4777
- refreshAttempted = true;
4778
- continue;
4779
- }
4780
- throw error;
4781
- }
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;
4782
4822
  }
4783
- const updatedSession = {
4784
- ...session,
4785
- tokens: {
4786
- ...refreshedTokens,
4787
- refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
4788
- },
4789
- discovery: toStoredDiscovery(discovery)
4790
- };
4791
- await saveSession(resource, updatedSession);
4792
- return updatedSession;
4793
- } finally {
4794
- refreshPromises.delete(resource);
4823
+ throw error;
4795
4824
  }
4796
- })();
4797
- refreshPromises.set(resource, promise);
4798
- return promise;
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;
4799
4836
  }
4800
4837
  async function authorizeSession(resource, existingSession, discovery, fetch2, signal) {
4801
4838
  signal?.throwIfAborted();
4802
- const inFlight = authorizationPromises.get(resource);
4803
- if (inFlight !== void 0) {
4804
- return inFlight;
4805
- }
4806
- const promise = (async () => {
4807
- assertS256PkceSupport(discovery.authorizationServerMetadata);
4808
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
4809
- let currentSession = existingSession;
4810
- let transientRetryAttempted = false;
4811
- let reRegistrationAttempted = false;
4812
- while (true) {
4813
- const loopback = await createLoopbackAuthorizationSession({
4814
- openBrowser: options.browser.openBrowser,
4815
- readLine: options.browser.readLine,
4816
- createServer: options.browser.createServer,
4817
- landingPage: options.browser.landingPage,
4818
- redirectUri: options.browser.redirectUri,
4819
- signal: options.browser.signal === void 0 ? signal : signal === void 0 ? options.browser.signal : AbortSignal.any([signal, options.browser.signal]),
4820
- timeoutMs: options.browser.timeoutMs
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)
4821
4873
  });
4822
- let resolvedClient = null;
4823
- try {
4824
- resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch2, signal);
4825
- const sessionWithoutTokens = {
4826
- resource,
4827
- authorizationServer: discovery.authorizationServer,
4828
- client: resolvedClient.client,
4829
- discovery: toStoredDiscovery(discovery)
4830
- };
4831
- await saveSession(resource, sessionWithoutTokens);
4832
- const verifier = generateCodeVerifier();
4833
- const challenge = generateCodeChallenge(verifier);
4834
- const authorizationUrl = buildAuthorizationUrl({
4835
- metadata: discovery.authorizationServerMetadata,
4836
- resource,
4837
- clientId: resolvedClient.client.clientId,
4838
- redirectUri: loopback.redirectUri,
4839
- codeChallenge: challenge,
4840
- clientMetadata: getClientMetadata(options.client)
4841
- });
4842
- const code = await loopback.waitForCode(authorizationUrl);
4843
- const tokens = await exchangeAuthorizationCode({
4844
- tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
4845
- clientId: resolvedClient.client.clientId,
4846
- clientSecret: resolvedClient.client.clientSecret,
4847
- code,
4848
- codeVerifier: verifier,
4849
- redirectUri: loopback.redirectUri,
4850
- resource,
4851
- fetch: fetch2,
4852
- signal,
4853
- now
4854
- });
4855
- const session = {
4856
- ...sessionWithoutTokens,
4857
- tokens
4858
- };
4859
- await saveSession(resource, session);
4860
- return session;
4861
- } catch (error) {
4862
- signal?.throwIfAborted();
4863
- if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
4864
- reRegistrationAttempted = true;
4865
- await clearRegisteredClient(discovery.authorizationServer);
4866
- await clearSession(resource);
4867
- currentSession = null;
4868
- continue;
4869
- }
4870
- if (!transientRetryAttempted && isRetryableOAuthError(error)) {
4871
- transientRetryAttempted = true;
4872
- await clearSession(resource);
4873
- currentSession = null;
4874
- continue;
4875
- }
4876
- throw error;
4877
- } finally {
4878
- 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;
4879
4907
  }
4908
+ throw error;
4909
+ } finally {
4910
+ loopback.close();
4880
4911
  }
4881
- })();
4882
- const finalPromise = promise.finally(() => {
4883
- authorizationPromises.delete(resource);
4884
- });
4885
- authorizationPromises.set(resource, finalPromise);
4886
- return finalPromise;
4912
+ }
4887
4913
  }
4888
4914
  async function resolveClient(existingSession, discovery, redirectUri, fetch2, parentSignal) {
4889
4915
  parentSignal?.throwIfAborted();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",