@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.
- package/README.md +113 -30
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- package/dist/adapters/agui-middleware.d.mts +3 -3
- package/dist/adapters/agui-middleware.d.ts +3 -3
- package/dist/adapters/ai-adapter.d.mts +3 -3
- package/dist/adapters/ai-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +2 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +9 -8
- package/dist/client/react.d.ts +9 -8
- package/dist/client/react.js +66 -26
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +67 -27
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +6 -5
- package/dist/client/vue.d.ts +6 -5
- package/dist/client/vue.js +27 -17
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +27 -17
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-Cfjsme-a.d.mts → index-ByIjEReo.d.mts} +2 -2
- package/dist/{index-CmjMd2ac.d.ts → index-CtXvKl8N.d.ts} +2 -2
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +728 -306
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +722 -304
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-C7hGqzgM.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -33
- package/dist/{multi-session-client-C_kPHpD5.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -33
- package/dist/server/index.d.mts +45 -17
- package/dist/server/index.d.ts +45 -17
- package/dist/server/index.js +720 -300
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +716 -299
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +9 -8
- package/dist/shared/index.d.ts +9 -8
- package/dist/shared/index.js +7 -5
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +5 -4
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-BMzhoNYt.d.ts → tool-router-CZMrOG8J.d.ts} +1 -1
- package/dist/{tool-router-BhHsvBfP.d.mts → tool-router-CuApsDiV.d.mts} +1 -1
- package/dist/{types-CxFaaZrM.d.mts → types-DCk_IF4L.d.mts} +5 -3
- package/dist/{types-CxFaaZrM.d.ts → types-DCk_IF4L.d.ts} +5 -3
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +40 -3
- package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +4 -4
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +67 -3
- package/migrations/supabase/20260421010000_add_session_cleanup_cron.sql +6 -6
- package/package.json +13 -3
- package/src/client/core/sse-client.ts +2 -2
- package/src/client/react/index.ts +1 -1
- package/src/client/react/oauth-popup.tsx +34 -13
- package/src/client/react/use-mcp.ts +19 -45
- package/src/client/utils/session-state.ts +43 -0
- package/src/client/vue/use-mcp.ts +18 -47
- package/src/server/handlers/sse-handler.ts +9 -4
- package/src/server/mcp/multi-session-client.ts +2 -6
- package/src/server/mcp/oauth-client.ts +73 -101
- package/src/server/mcp/storage-oauth-provider.ts +79 -50
- package/src/server/storage/file-backend.ts +74 -18
- package/src/server/storage/index.ts +2 -4
- package/src/server/storage/memory-backend.ts +49 -13
- package/src/server/storage/neon-backend.ts +201 -68
- package/src/server/storage/redis-backend.ts +81 -23
- package/src/server/storage/session-lifecycle.ts +78 -0
- package/src/server/storage/sqlite-backend.ts +89 -15
- package/src/server/storage/supabase-backend.ts +188 -63
- package/src/server/storage/types.ts +49 -16
- package/src/shared/constants.ts +5 -6
- package/src/shared/types.ts +5 -3
- 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
|
-
|
|
283
|
-
if (!
|
|
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 ||
|
|
288
|
-
this.callbackUrl = this.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 ||
|
|
295
|
-
this.serverId = this.serverId ||
|
|
296
|
-
this.headers = this.headers ||
|
|
297
|
-
this.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
407
|
+
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
399
408
|
} else {
|
|
400
|
-
await sessions.create(sessionData
|
|
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}
|
|
503
|
-
await this.saveSession(
|
|
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
|
-
//
|
|
533
|
-
//
|
|
534
|
-
|
|
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
|
-
|
|
547
|
-
|
|
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
|
-
//
|
|
573
|
-
// sessions may still hold
|
|
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.
|
|
590
|
+
if (!existingSession || existingSession.status !== 'active') {
|
|
577
591
|
await sessions.delete(this.userId, this.sessionId);
|
|
578
592
|
}
|
|
579
593
|
} catch {
|
|
580
|
-
//
|
|
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
|
-
|
|
670
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
112
|
+
* Loads OAuth credentials from the session store
|
|
114
113
|
* @private
|
|
115
114
|
*/
|
|
116
|
-
private async
|
|
117
|
-
const data = await sessions.
|
|
115
|
+
private async getCredentials(): Promise<SessionCredentials> {
|
|
116
|
+
const data = await sessions.getCredentials(this.userId, this.sessionId);
|
|
118
117
|
if (!data) {
|
|
119
|
-
return {
|
|
118
|
+
return { userId: this.userId, sessionId: this.sessionId };
|
|
120
119
|
}
|
|
121
120
|
return data;
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
/**
|
|
125
|
-
* Saves OAuth
|
|
126
|
-
* @param data - Partial OAuth
|
|
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
|
|
131
|
-
await sessions.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
191
|
-
const
|
|
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
|
-
|
|
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(
|
|
201
|
-
|
|
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<
|
|
252
|
+
const updates: Partial<SessionCredentials> = {};
|
|
218
253
|
|
|
219
254
|
if (scope === "client") {
|
|
220
|
-
updates.clientInformation =
|
|
221
|
-
updates.clientId =
|
|
255
|
+
updates.clientInformation = null;
|
|
256
|
+
updates.clientId = null;
|
|
222
257
|
} else if (scope === "tokens") {
|
|
223
|
-
updates.tokens =
|
|
258
|
+
updates.tokens = null;
|
|
224
259
|
} else if (scope === "verifier") {
|
|
225
|
-
updates.codeVerifier =
|
|
260
|
+
updates.codeVerifier = null;
|
|
226
261
|
}
|
|
227
|
-
await this.
|
|
262
|
+
await this.patchCredentials(updates);
|
|
228
263
|
}
|
|
229
264
|
}
|
|
230
265
|
|
|
231
266
|
async saveCodeVerifier(verifier: string): Promise<void> {
|
|
232
|
-
await this.
|
|
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.
|
|
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.
|
|
289
|
+
await this.patchCredentials({ codeVerifier: null });
|
|
250
290
|
}
|
|
251
291
|
|
|
252
292
|
async tokens(): Promise<OAuthTokens | undefined> {
|
|
253
|
-
const data = await this.
|
|
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
|
}
|