@mcp-ts/sdk 2.3.4 → 2.4.0

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.
Files changed (82) hide show
  1. package/README.md +113 -30
  2. package/dist/adapters/agui-adapter.d.mts +3 -3
  3. package/dist/adapters/agui-adapter.d.ts +3 -3
  4. package/dist/adapters/agui-middleware.d.mts +3 -3
  5. package/dist/adapters/agui-middleware.d.ts +3 -3
  6. package/dist/adapters/ai-adapter.d.mts +3 -3
  7. package/dist/adapters/ai-adapter.d.ts +3 -3
  8. package/dist/adapters/langchain-adapter.d.mts +3 -3
  9. package/dist/adapters/langchain-adapter.d.ts +3 -3
  10. package/dist/adapters/mastra-adapter.d.mts +1 -1
  11. package/dist/adapters/mastra-adapter.d.ts +1 -1
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/index.js +2 -2
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +2 -2
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +9 -8
  19. package/dist/client/react.d.ts +9 -8
  20. package/dist/client/react.js +66 -26
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +67 -27
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +6 -5
  25. package/dist/client/vue.d.ts +6 -5
  26. package/dist/client/vue.js +27 -17
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +27 -17
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{index-Cfjsme-a.d.mts → index-ByIjEReo.d.mts} +2 -2
  31. package/dist/{index-CmjMd2ac.d.ts → index-CtXvKl8N.d.ts} +2 -2
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +728 -306
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +722 -304
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-C7hGqzgM.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -33
  39. package/dist/{multi-session-client-C_kPHpD5.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -33
  40. package/dist/server/index.d.mts +45 -17
  41. package/dist/server/index.d.ts +45 -17
  42. package/dist/server/index.js +720 -300
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +716 -299
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +9 -8
  47. package/dist/shared/index.d.ts +9 -8
  48. package/dist/shared/index.js +7 -5
  49. package/dist/shared/index.js.map +1 -1
  50. package/dist/shared/index.mjs +5 -4
  51. package/dist/shared/index.mjs.map +1 -1
  52. package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
  53. package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
  54. package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
  55. package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
  56. package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
  57. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
  58. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
  59. package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
  60. package/package.json +13 -3
  61. package/src/client/core/sse-client.ts +2 -2
  62. package/src/client/react/index.ts +1 -1
  63. package/src/client/react/oauth-popup.tsx +34 -13
  64. package/src/client/react/use-mcp.ts +19 -45
  65. package/src/client/utils/session-state.ts +43 -0
  66. package/src/client/vue/use-mcp.ts +18 -47
  67. package/src/server/handlers/sse-handler.ts +9 -4
  68. package/src/server/mcp/multi-session-client.ts +2 -6
  69. package/src/server/mcp/oauth-client.ts +73 -101
  70. package/src/server/mcp/storage-oauth-provider.ts +79 -50
  71. package/src/server/storage/file-backend.ts +74 -18
  72. package/src/server/storage/index.ts +2 -4
  73. package/src/server/storage/memory-backend.ts +49 -13
  74. package/src/server/storage/neon-backend.ts +201 -68
  75. package/src/server/storage/redis-backend.ts +81 -23
  76. package/src/server/storage/session-lifecycle.ts +78 -0
  77. package/src/server/storage/sqlite-backend.ts +89 -15
  78. package/src/server/storage/supabase-backend.ts +188 -63
  79. package/src/server/storage/types.ts +49 -16
  80. package/src/shared/constants.ts +5 -6
  81. package/src/shared/types.ts +5 -3
  82. package/src/shared/utils.ts +26 -0
@@ -26,15 +26,13 @@ import {
26
26
  ReadResourceResultSchema,
27
27
  } from '@modelcontextprotocol/sdk/types.js';
28
28
  import { StorageOAuthClientProvider, type AgentsOAuthProvider } from './storage-oauth-provider.js';
29
- import { sanitizeServerLabel } from '../../shared/utils.js';
30
29
  import { Emitter, type McpConnectionEvent, type McpObservabilityEvent, type McpConnectionState } from '../../shared/events.js';
31
30
  import { UnauthorizedError } from '../../shared/errors.js';
32
31
  import { sessions } from '../storage/index.js';
32
+ import type { Session, SessionStatus } from '../storage/types.js';
33
33
  import {
34
34
  MCP_CLIENT_NAME,
35
35
  MCP_CLIENT_VERSION,
36
- SESSION_TTL_SECONDS,
37
- STATE_EXPIRATION_MS,
38
36
  } from '../../shared/constants.js';
39
37
 
40
38
  /**
@@ -278,23 +276,25 @@ export class MCPClient {
278
276
  this.emitStateChange('INITIALIZING');
279
277
  this.emitProgress('Loading session configuration...');
280
278
 
279
+ let existingSession: Session | null = null;
280
+
281
281
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
282
- const sessionData = await sessions.get(this.userId, this.sessionId);
283
- if (!sessionData) {
282
+ existingSession = await sessions.get(this.userId, this.sessionId);
283
+ if (!existingSession) {
284
284
  throw new Error(`Session not found: ${this.sessionId}`);
285
285
  }
286
286
 
287
- this.serverUrl = this.serverUrl || sessionData.serverUrl;
288
- this.callbackUrl = this.callbackUrl || sessionData.callbackUrl;
287
+ this.serverUrl = this.serverUrl || existingSession.serverUrl;
288
+ this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
289
289
  /**
290
290
  * Do NOT load transportType from session if not explicitly provided.
291
291
  * We want to re-negotiate (try streamable -> sse) on new connections if in "Auto" mode.
292
292
  * this.transportType = this.transportType || sessionData.transportType;
293
293
  */
294
- this.serverName = this.serverName || sessionData.serverName;
295
- this.serverId = this.serverId || sessionData.serverId || 'unknown';
296
- this.headers = this.headers || sessionData.headers;
297
- this.createdAt = sessionData.createdAt;
294
+ this.serverName = this.serverName || existingSession.serverName;
295
+ this.serverId = this.serverId || existingSession.serverId || 'unknown';
296
+ this.headers = this.headers || existingSession.headers;
297
+ this.createdAt = existingSession.createdAt;
298
298
  }
299
299
 
300
300
  if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
@@ -345,10 +345,13 @@ export class MCPClient {
345
345
  // Create session in the session store if it doesn't exist yet
346
346
  // This is needed BEFORE OAuth flow starts because the OAuth provider
347
347
  // will call saveCodeVerifier() which requires the session to exist
348
- const existingSession = await sessions.get(this.userId, this.sessionId);
348
+ if (existingSession === null) {
349
+ existingSession = await sessions.get(this.userId, this.sessionId);
350
+ }
349
351
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
350
352
  this.createdAt = Date.now();
351
- console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
353
+ const updatedAt = this.createdAt;
354
+ console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
352
355
  await sessions.create({
353
356
  sessionId: this.sessionId,
354
357
  userId: this.userId,
@@ -359,21 +362,21 @@ export class MCPClient {
359
362
  transportType: this.transportType || 'streamable-http',
360
363
  headers: this.headers,
361
364
  createdAt: this.createdAt,
362
- active: false,
363
- }, Math.floor(STATE_EXPIRATION_MS / 1000)); // Short TTL until connection succeeds
365
+ updatedAt,
366
+ status: 'pending',
367
+ });
364
368
  }
365
369
  }
366
370
 
367
371
  /**
368
372
  * Saves current session state to the session store
369
373
  * Creates new session if it doesn't exist, updates if it does
370
- * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
371
- * @param active - Session status marker used to avoid unnecessary TTL rewrites
374
+ * @param status - Session lifecycle status used by storage cleanup
372
375
  * @private
373
376
  */
374
377
  private async saveSession(
375
- ttl: number = SESSION_TTL_SECONDS,
376
- active: boolean = true
378
+ status: SessionStatus = 'active',
379
+ existingSession?: Session | null
377
380
  ): Promise<void> {
378
381
  if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
379
382
  return;
@@ -389,15 +392,33 @@ export class MCPClient {
389
392
  transportType: (this.transportType || 'streamable-http') as TransportType,
390
393
  headers: this.headers,
391
394
  createdAt: this.createdAt || Date.now(),
392
- active,
395
+ updatedAt: Date.now(),
396
+ status,
393
397
  };
398
+ if (status === 'active') {
399
+ (sessionData as typeof sessionData & { authUrl: null }).authUrl = null;
400
+ }
394
401
 
395
402
  // Try to update first, create if doesn't exist
396
- const existingSession = await sessions.get(this.userId, this.sessionId);
403
+ if (existingSession === undefined) {
404
+ existingSession = await sessions.get(this.userId, this.sessionId);
405
+ }
397
406
  if (existingSession) {
398
- await sessions.update(this.userId, this.sessionId, sessionData, ttl);
407
+ await sessions.update(this.userId, this.sessionId, sessionData);
399
408
  } else {
400
- await sessions.create(sessionData, ttl);
409
+ await sessions.create(sessionData);
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Removes transient setup/auth sessions without masking the original error.
415
+ * @private
416
+ */
417
+ private async deleteTransientSession(): Promise<void> {
418
+ try {
419
+ await sessions.delete(this.userId, this.sessionId);
420
+ } catch {
421
+ // Best effort only: preserve the original connection/auth error.
401
422
  }
402
423
  }
403
424
 
@@ -499,8 +520,8 @@ export class MCPClient {
499
520
 
500
521
  // Refresh session metadata on every successful connect so active sessions
501
522
  // record ongoing usage and don't look dormant to session cleanup jobs.
502
- console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
503
- await this.saveSession(SESSION_TTL_SECONDS, true);
523
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
524
+ await this.saveSession('active');
504
525
  } catch (error) {
505
526
  /** Handle Authentication Errors */
506
527
  if (
@@ -529,23 +550,16 @@ export class MCPClient {
529
550
  this.emitError(message, 'auth');
530
551
  this.emitStateChange('FAILED');
531
552
 
532
- // Proactive Cleanup: This session has reached a terminal failure state.
533
- // We remove it now to ensure the database remains lean, bypassing the
534
- // automated lifecycle sweep.
535
- try {
536
- await sessions.delete(this.userId, this.sessionId);
537
- } catch {
538
- // Non-blocking: Proactive cleanup failures are suppressed to prioritize
539
- // the original error context.
540
- }
553
+ // Remove terminal setup failures immediately. Active sessions are not
554
+ // deleted here because this branch only runs before OAuth is available.
555
+ await this.deleteTransientSession();
541
556
 
542
557
  throw new Error(message);
543
558
  }
544
559
 
545
560
  this.emitStateChange('AUTHENTICATING');
546
- // Save session with 10min TTL for OAuth pending state
547
- console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
548
- await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
561
+ console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
562
+ await this.saveSession('pending');
549
563
 
550
564
  if (this.serverId) {
551
565
  this._onConnectionEvent.fire({
@@ -569,16 +583,15 @@ export class MCPClient {
569
583
  this.emitError(errorMessage, 'connection');
570
584
  this.emitStateChange('FAILED');
571
585
 
572
- // Terminal Handshake Failure: only purge transient sessions. Active
573
- // sessions may still hold valid credentials for a later reconnect.
586
+ // Remove transient sessions that failed before becoming restorable.
587
+ // Existing active sessions may still hold usable credentials for reconnect.
574
588
  try {
575
589
  const existingSession = await sessions.get(this.userId, this.sessionId);
576
- if (!existingSession || existingSession.active !== true) {
590
+ if (!existingSession || existingSession.status !== 'active') {
577
591
  await sessions.delete(this.userId, this.sessionId);
578
592
  }
579
593
  } catch {
580
- // Non-blocking: Cleanup is performed on a best-effort basis and should
581
- // not interfere with the primary error propagation.
594
+ // Best effort only: preserve the original connection error.
582
595
  }
583
596
 
584
597
  throw error;
@@ -593,7 +606,7 @@ export class MCPClient {
593
606
  */
594
607
 
595
608
  // TODO: needs to be optimized
596
- async finishAuth(authCode: string): Promise<void> {
609
+ async finishAuth(authCode: string, state?: string): Promise<void> {
597
610
  this.emitStateChange('AUTHENTICATING');
598
611
  this.emitProgress('Exchanging authorization code for tokens...');
599
612
 
@@ -606,6 +619,18 @@ export class MCPClient {
606
619
  throw new Error(error);
607
620
  }
608
621
 
622
+ if (state) {
623
+ const stateCheck = await this.oauthProvider.checkState(state);
624
+ if (!stateCheck.valid) {
625
+ const error = stateCheck.error || 'Invalid OAuth state';
626
+ this.emitError(error, 'auth');
627
+ this.emitStateChange('FAILED');
628
+ throw new Error(error);
629
+ }
630
+
631
+ await this.oauthProvider.consumeState(state);
632
+ }
633
+
609
634
  /**
610
635
  * Determine which transports to try for finishing auth
611
636
  * If transportType is set, use only that. Otherwise try streamable_http then sse.
@@ -666,9 +691,8 @@ export class MCPClient {
666
691
  this.transportType = currentType;
667
692
 
668
693
  this.emitStateChange('CONNECTED');
669
- // Update session with 12hr TTL after successful OAuth
670
- console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
671
- await this.saveSession(SESSION_TTL_SECONDS, true);
694
+ console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
695
+ await this.saveSession('active');
672
696
 
673
697
  return; // Success, exit function
674
698
 
@@ -689,6 +713,7 @@ export class MCPClient {
689
713
  const msg = error instanceof Error ? error.message : 'Authentication failed';
690
714
  this.emitError(msg, 'auth');
691
715
  this.emitStateChange('FAILED');
716
+ await this.deleteTransientSession();
692
717
  throw error;
693
718
  }
694
719
 
@@ -696,6 +721,7 @@ export class MCPClient {
696
721
  const msg = error instanceof Error ? error.message : 'Authentication failed';
697
722
  this.emitError(msg, 'auth');
698
723
  this.emitStateChange('FAILED');
724
+ await this.deleteTransientSession();
699
725
  throw error;
700
726
  }
701
727
 
@@ -708,6 +734,7 @@ export class MCPClient {
708
734
  const errorMessage = lastError instanceof Error ? lastError.message : 'Authentication failed';
709
735
  this.emitError(errorMessage, 'auth');
710
736
  this.emitStateChange('FAILED');
737
+ await this.deleteTransientSession();
711
738
  throw lastError;
712
739
  }
713
740
  }
@@ -1080,59 +1107,4 @@ export class MCPClient {
1080
1107
  return this.sessionId;
1081
1108
  }
1082
1109
 
1083
- /**
1084
- * Gets MCP server configuration for all active user sessions
1085
- * Loads sessions from storage and returns server connection metadata.
1086
- * OAuth refresh is handled by SDK transports through their authProvider.
1087
- * @deprecated This returns legacy connection metadata only and does not
1088
- * include OAuth tokens or generated Authorization headers. Prefer
1089
- * MultiSessionClient or explicit MCPClient instances so SDK transports can
1090
- * own OAuth refresh and reauthorization.
1091
- * @param userId - User ID to fetch sessions for
1092
- * @returns Object keyed by sanitized server labels containing transport and url.
1093
- * @static
1094
- */
1095
- static async getMcpServerConfig(userId: string): Promise<Record<string, any>> {
1096
- const mcpConfig: Record<string, any> = {};
1097
- const sessionList = await sessions.list(userId);
1098
-
1099
- await Promise.all(
1100
- sessionList.map(async (sessionData) => {
1101
- const { sessionId } = sessionData;
1102
-
1103
- try {
1104
- // Validate session - remove if missing required fields
1105
- if (
1106
- !sessionData.serverId ||
1107
- !sessionData.transportType ||
1108
- !sessionData.serverUrl ||
1109
- !sessionData.callbackUrl
1110
- ) {
1111
- await sessions.delete(userId, sessionId);
1112
- return;
1113
- }
1114
-
1115
- // Build server config
1116
- const label = sanitizeServerLabel(
1117
- sessionData.serverName || sessionData.serverId || 'server'
1118
- );
1119
-
1120
- mcpConfig[label] = {
1121
- transport: sessionData.transportType,
1122
- url: sessionData.serverUrl,
1123
- ...(sessionData.serverName && {
1124
- serverName: sessionData.serverName,
1125
- serverLabel: label,
1126
- }),
1127
- };
1128
- } catch (error) {
1129
- await sessions.delete(userId, sessionId);
1130
- console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
1131
- }
1132
- })
1133
- );
1134
-
1135
- return mcpConfig;
1136
- }
1137
-
1138
1110
  }
@@ -5,7 +5,8 @@ import type {
5
5
  OAuthClientMetadata,
6
6
  OAuthTokens
7
7
  } from "@modelcontextprotocol/sdk/shared/auth.js";
8
- import { sessions, type Session } from "../storage/index.js";
8
+ import { nanoid } from "nanoid";
9
+ import { sessions, type SessionCredentials } from "../storage/index.js";
9
10
  import {
10
11
  DEFAULT_CLIENT_NAME,
11
12
  DEFAULT_CLIENT_URI,
@@ -13,8 +14,9 @@ import {
13
14
  DEFAULT_POLICY_URI,
14
15
  SOFTWARE_ID,
15
16
  SOFTWARE_VERSION,
16
- TOKEN_EXPIRY_BUFFER_MS,
17
+ STATE_EXPIRATION_MS,
17
18
  } from '../../shared/constants.js';
19
+ import { formatOAuthState, parseOAuthState } from '../../shared/utils.js';
18
20
 
19
21
  /**
20
22
  * Extension of OAuthClientProvider interface with additional methods
@@ -29,8 +31,6 @@ export interface AgentsOAuthProvider extends OAuthClientProvider {
29
31
  ): Promise<{ valid: boolean; serverId?: string; error?: string }>;
30
32
  consumeState(state: string): Promise<void>;
31
33
  deleteCodeVerifier(): Promise<void>;
32
- isTokenExpired(): boolean;
33
- setTokenExpiresAt(expiresAt: number): void;
34
34
  }
35
35
 
36
36
  export interface StorageOAuthClientProviderOptions {
@@ -66,7 +66,6 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
66
66
  private _authUrl: string | undefined;
67
67
  private _clientId: string | undefined;
68
68
  private onRedirectCallback?: (url: string) => void;
69
- private tokenExpiresAt?: number;
70
69
 
71
70
  /**
72
71
  * Creates a new session-backed OAuth provider
@@ -110,32 +109,32 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
110
109
  }
111
110
 
112
111
  /**
113
- * Loads OAuth data from the session store
112
+ * Loads OAuth credentials from the session store
114
113
  * @private
115
114
  */
116
- private async getSessionData(): Promise<Session> {
117
- const data = await sessions.get(this.userId, this.sessionId);
115
+ private async getCredentials(): Promise<SessionCredentials> {
116
+ const data = await sessions.getCredentials(this.userId, this.sessionId);
118
117
  if (!data) {
119
- return {} as Session;
118
+ return { userId: this.userId, sessionId: this.sessionId };
120
119
  }
121
120
  return data;
122
121
  }
123
122
 
124
123
  /**
125
- * Saves OAuth data to the session store
126
- * @param data - Partial OAuth data to save
124
+ * Saves OAuth credentials to the session store
125
+ * @param data - Partial OAuth credentials to save
127
126
  * @private
128
127
  * @throws Error if session doesn't exist (session must be created by controller layer)
129
128
  */
130
- private async saveSessionData(data: Partial<Session>): Promise<void> {
131
- await sessions.update(this.userId, this.sessionId, data);
129
+ private async patchCredentials(data: Partial<SessionCredentials>): Promise<void> {
130
+ await sessions.patchCredentials(this.userId, this.sessionId, data);
132
131
  }
133
132
 
134
133
  /**
135
134
  * Retrieves stored OAuth client information
136
135
  */
137
136
  async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
138
- const data = await this.getSessionData();
137
+ const data = await this.getCredentials();
139
138
 
140
139
  if (data.clientId && !this._clientId) {
141
140
  this._clientId = data.clientId;
@@ -159,7 +158,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
159
158
  * Stores OAuth client information
160
159
  */
161
160
  async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void> {
162
- await this.saveSessionData({
161
+ await this.patchCredentials({
163
162
  clientInformation,
164
163
  clientId: clientInformation.client_id
165
164
  });
@@ -170,13 +169,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
170
169
  * Stores OAuth tokens
171
170
  */
172
171
  async saveTokens(tokens: OAuthTokens): Promise<void> {
173
- const data: Partial<Session> = { tokens };
174
-
175
- if (tokens.expires_in) {
176
- this.tokenExpiresAt = Date.now() + (tokens.expires_in * 1000) - TOKEN_EXPIRY_BUFFER_MS;
177
- }
178
-
179
- await this.saveSessionData(data);
172
+ await this.patchCredentials({ tokens });
180
173
  }
181
174
 
182
175
  get authUrl() {
@@ -184,25 +177,67 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
184
177
  }
185
178
 
186
179
  async state(): Promise<string> {
187
- return this.sessionId;
180
+ const nonce = nanoid(32);
181
+ await this.patchCredentials({
182
+ oauthState: {
183
+ nonce,
184
+ sessionId: this.sessionId,
185
+ serverId: this.serverId,
186
+ createdAt: Date.now(),
187
+ },
188
+ codeVerifier: null,
189
+ });
190
+ return formatOAuthState(nonce, this.sessionId);
188
191
  }
189
192
 
190
- async checkState(_state: string): Promise<{ valid: boolean; serverId?: string; error?: string }> {
191
- const data = await sessions.get(this.userId, this.sessionId);
193
+ async checkState(state: string): Promise<{ valid: boolean; serverId?: string; error?: string }> {
194
+ const parsed = parseOAuthState(state);
195
+ if (!parsed) {
196
+ return { valid: false, error: "Invalid OAuth state" };
197
+ }
198
+
199
+ if (parsed.sessionId !== this.sessionId) {
200
+ return { valid: false, error: "OAuth state mismatch" };
201
+ }
202
+
203
+ const data = await sessions.getCredentials(this.userId, parsed.sessionId);
192
204
 
193
205
  if (!data) {
194
206
  return { valid: false, error: "Session not found" };
195
207
  }
196
208
 
197
- return { valid: true, serverId: this.serverId };
209
+ const oauthState = data.oauthState;
210
+ if (!oauthState) {
211
+ return { valid: false, error: "OAuth state not found" };
212
+ }
213
+
214
+ if (
215
+ oauthState.nonce !== parsed.nonce ||
216
+ oauthState.sessionId !== parsed.sessionId ||
217
+ oauthState.serverId !== this.serverId
218
+ ) {
219
+ return { valid: false, error: "OAuth state mismatch" };
220
+ }
221
+
222
+ if (Date.now() - oauthState.createdAt > STATE_EXPIRATION_MS) {
223
+ return { valid: false, error: "OAuth state expired" };
224
+ }
225
+
226
+ return { valid: true, serverId: oauthState.serverId };
198
227
  }
199
228
 
200
- async consumeState(_state: string): Promise<void> {
201
- // No-op
229
+ async consumeState(state: string): Promise<void> {
230
+ const result = await this.checkState(state);
231
+ if (!result.valid) {
232
+ throw new Error(result.error || "Invalid OAuth state");
233
+ }
234
+
235
+ await this.patchCredentials({ oauthState: null });
202
236
  }
203
237
 
204
238
  async redirectToAuthorization(authUrl: URL): Promise<void> {
205
239
  this._authUrl = authUrl.toString();
240
+ await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
206
241
  if (this.onRedirectCallback) {
207
242
  this.onRedirectCallback(authUrl.toString());
208
243
  }
@@ -214,26 +249,31 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
214
249
  if (scope === "all") {
215
250
  await sessions.delete(this.userId, this.sessionId);
216
251
  } else {
217
- const updates: Partial<Session> = {};
252
+ const updates: Partial<SessionCredentials> = {};
218
253
 
219
254
  if (scope === "client") {
220
- updates.clientInformation = undefined;
221
- updates.clientId = undefined;
255
+ updates.clientInformation = null;
256
+ updates.clientId = null;
222
257
  } else if (scope === "tokens") {
223
- updates.tokens = undefined;
258
+ updates.tokens = null;
224
259
  } else if (scope === "verifier") {
225
- updates.codeVerifier = undefined;
260
+ updates.codeVerifier = null;
226
261
  }
227
- await this.saveSessionData(updates);
262
+ await this.patchCredentials(updates);
228
263
  }
229
264
  }
230
265
 
231
266
  async saveCodeVerifier(verifier: string): Promise<void> {
232
- await this.saveSessionData({ codeVerifier: verifier });
267
+ const data = await this.getCredentials();
268
+ if (data.codeVerifier) {
269
+ return;
270
+ }
271
+
272
+ await this.patchCredentials({ codeVerifier: verifier });
233
273
  }
234
274
 
235
275
  async codeVerifier(): Promise<string> {
236
- const data = await this.getSessionData();
276
+ const data = await this.getCredentials();
237
277
 
238
278
  if (data.clientId && !this._clientId) {
239
279
  this._clientId = data.clientId;
@@ -246,27 +286,16 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
246
286
  }
247
287
 
248
288
  async deleteCodeVerifier(): Promise<void> {
249
- await this.saveSessionData({ codeVerifier: undefined });
289
+ await this.patchCredentials({ codeVerifier: null });
250
290
  }
251
291
 
252
292
  async tokens(): Promise<OAuthTokens | undefined> {
253
- const data = await this.getSessionData();
293
+ const data = await this.getCredentials();
254
294
 
255
295
  if (data.clientId && !this._clientId) {
256
296
  this._clientId = data.clientId;
257
297
  }
258
298
 
259
- return data.tokens;
260
- }
261
-
262
- isTokenExpired(): boolean {
263
- if (!this.tokenExpiresAt) {
264
- return false;
265
- }
266
- return Date.now() >= this.tokenExpiresAt;
267
- }
268
-
269
- setTokenExpiresAt(expiresAt: number): void {
270
- this.tokenExpiresAt = expiresAt;
299
+ return data.tokens ?? undefined;
271
300
  }
272
301
  }