@mcp-ts/sdk 2.3.3 → 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 +729 -397
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +724 -396
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-DYNe6az3.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -41
  39. package/dist/{multi-session-client-BYtguGJm.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -41
  40. package/dist/server/index.d.mts +45 -17
  41. package/dist/server/index.d.ts +45 -17
  42. package/dist/server/index.js +721 -391
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +718 -391
  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 -220
  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
@@ -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
  }
@@ -1,7 +1,13 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
- import type { SessionStore, Session, SetClientOptions } from './types.js';
3
+ import type { SessionStore, Session, SessionCredentials } from './types.js';
4
4
  import { generateSessionId } from '../../shared/utils.js';
5
+ import {
6
+ mergeSessionUpdate,
7
+ normalizeNewSession,
8
+ normalizeStoredSession,
9
+ isSessionExpired,
10
+ } from './session-lifecycle.js';
5
11
 
6
12
  /**
7
13
  * File system implementation of SessionStore
@@ -10,6 +16,7 @@ import { generateSessionId } from '../../shared/utils.js';
10
16
  export class FileStorageBackend implements SessionStore {
11
17
  private filePath: string;
12
18
  private memoryCache: Map<string, Session> | null = null;
19
+ private credentialsCache: Map<string, SessionCredentials> | null = null;
13
20
  private initialized = false;
14
21
 
15
22
  /**
@@ -35,15 +42,23 @@ export class FileStorageBackend implements SessionStore {
35
42
  const json = JSON.parse(data);
36
43
 
37
44
  this.memoryCache = new Map();
38
- if (Array.isArray(json)) {
39
- json.forEach((s: Session) => {
40
- this.memoryCache!.set(this.getSessionKey(s.userId || 'unknown', s.sessionId), s);
45
+ this.credentialsCache = new Map();
46
+ if (Array.isArray(json.sessions)) {
47
+ json.sessions.forEach((s: Session) => {
48
+ const session = normalizeStoredSession(s);
49
+ this.memoryCache!.set(this.getSessionKey(session.userId || 'unknown', session.sessionId), session);
50
+ });
51
+ }
52
+ if (Array.isArray(json.credentials)) {
53
+ json.credentials.forEach((c: SessionCredentials) => {
54
+ this.credentialsCache!.set(this.getSessionKey(c.userId, c.sessionId), c);
41
55
  });
42
56
  }
43
57
  } catch (error: any) {
44
58
  if (error.code === 'ENOENT') {
45
59
  // File does not exist, initialize empty
46
60
  this.memoryCache = new Map();
61
+ this.credentialsCache = new Map();
47
62
  await this.flush();
48
63
  } else {
49
64
  console.error('[FileStorage] Failed to load sessions:', error);
@@ -60,9 +75,11 @@ export class FileStorageBackend implements SessionStore {
60
75
  }
61
76
 
62
77
  private async flush(): Promise<void> {
63
- if (!this.memoryCache) return;
64
- const sessions = Array.from(this.memoryCache.values());
65
- await fs.writeFile(this.filePath, JSON.stringify(sessions, null, 2), 'utf-8');
78
+ if (!this.memoryCache || !this.credentialsCache) return;
79
+ await fs.writeFile(this.filePath, JSON.stringify({
80
+ sessions: Array.from(this.memoryCache.values()),
81
+ credentials: Array.from(this.credentialsCache.values()),
82
+ }, null, 2), 'utf-8');
66
83
  }
67
84
 
68
85
  private getSessionKey(userId: string, sessionId: string): string {
@@ -73,7 +90,7 @@ export class FileStorageBackend implements SessionStore {
73
90
  return generateSessionId();
74
91
  }
75
92
 
76
- async create(session: Session, ttl?: number): Promise<void> {
93
+ async create(session: Session): Promise<void> {
77
94
  await this.ensureInitialized();
78
95
  const { sessionId, userId } = session;
79
96
  if (!sessionId || !userId) throw new Error('userId and sessionId required');
@@ -83,12 +100,11 @@ export class FileStorageBackend implements SessionStore {
83
100
  throw new Error(`Session ${sessionId} already exists`);
84
101
  }
85
102
 
86
- this.memoryCache!.set(sessionKey, session);
103
+ this.memoryCache!.set(sessionKey, normalizeNewSession(session));
87
104
  await this.flush();
88
- // Note: TTL is ignored in file backend - sessions don't auto-expire
89
105
  }
90
106
 
91
- async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
107
+ async update(userId: string, sessionId: string, data: Partial<Session>): Promise<void> {
92
108
  await this.ensureInitialized();
93
109
  if (!userId || !sessionId) throw new Error('userId and sessionId required');
94
110
 
@@ -99,14 +115,22 @@ export class FileStorageBackend implements SessionStore {
99
115
  throw new Error(`Session ${sessionId} not found`);
100
116
  }
101
117
 
102
- const updated = {
103
- ...current,
104
- ...data
105
- };
118
+ const updated = mergeSessionUpdate(current, data);
106
119
 
107
120
  this.memoryCache!.set(sessionKey, updated);
108
121
  await this.flush();
109
- // Note: TTL is ignored in file backend - sessions don't auto-expire
122
+ }
123
+
124
+ async patchCredentials(userId: string, sessionId: string, data: Partial<SessionCredentials>): Promise<void> {
125
+ await this.ensureInitialized();
126
+ const sessionKey = this.getSessionKey(userId, sessionId);
127
+ if (!this.memoryCache!.has(sessionKey)) {
128
+ throw new Error(`Session ${sessionId} not found`);
129
+ }
130
+
131
+ const current = this.credentialsCache!.get(sessionKey) ?? { sessionId, userId };
132
+ this.credentialsCache!.set(sessionKey, { ...current, ...data, sessionId, userId });
133
+ await this.flush();
110
134
  }
111
135
 
112
136
  async get(userId: string, sessionId: string): Promise<Session | null> {
@@ -115,6 +139,23 @@ export class FileStorageBackend implements SessionStore {
115
139
  return this.memoryCache!.get(sessionKey) || null;
116
140
  }
117
141
 
142
+ async getCredentials(userId: string, sessionId: string): Promise<SessionCredentials | null> {
143
+ await this.ensureInitialized();
144
+ const sessionKey = this.getSessionKey(userId, sessionId);
145
+ if (!this.memoryCache!.has(sessionKey)) return null;
146
+ return this.credentialsCache!.get(sessionKey) ?? { sessionId, userId };
147
+ }
148
+
149
+ async clearCredentials(userId: string, sessionId: string): Promise<void> {
150
+ await this.patchCredentials(userId, sessionId, {
151
+ clientInformation: null,
152
+ tokens: null,
153
+ codeVerifier: null,
154
+ clientId: null,
155
+ oauthState: null,
156
+ });
157
+ }
158
+
118
159
  async list(userId: string): Promise<Session[]> {
119
160
  await this.ensureInitialized();
120
161
  return Array.from(this.memoryCache!.values()).filter(s => s.userId === userId);
@@ -130,7 +171,9 @@ export class FileStorageBackend implements SessionStore {
130
171
  async delete(userId: string, sessionId: string): Promise<void> {
131
172
  await this.ensureInitialized();
132
173
  const sessionKey = this.getSessionKey(userId, sessionId);
133
- if (this.memoryCache!.delete(sessionKey)) {
174
+ const deleted = this.memoryCache!.delete(sessionKey);
175
+ this.credentialsCache!.delete(sessionKey);
176
+ if (deleted) {
134
177
  await this.flush();
135
178
  }
136
179
  }
@@ -143,12 +186,25 @@ export class FileStorageBackend implements SessionStore {
143
186
  async clearAll(): Promise<void> {
144
187
  await this.ensureInitialized();
145
188
  this.memoryCache!.clear();
189
+ this.credentialsCache!.clear();
146
190
  await this.flush();
147
191
  }
148
192
 
149
193
  async cleanupExpired(): Promise<void> {
150
- // Could implement TTL check here using createdAt
151
194
  await this.ensureInitialized();
195
+ let changed = false;
196
+
197
+ for (const [key, session] of this.memoryCache!.entries()) {
198
+ if (!isSessionExpired(session)) continue;
199
+
200
+ this.memoryCache!.delete(key);
201
+ this.credentialsCache!.delete(key);
202
+ changed = true;
203
+ }
204
+
205
+ if (changed) {
206
+ await this.flush();
207
+ }
152
208
  }
153
209
 
154
210
  async disconnect(): Promise<void> {
@@ -63,27 +63,25 @@ function createSessionMutationEvent(prop: PropertyKey, args: any[]): SessionMuta
63
63
  const timestamp = Date.now();
64
64
 
65
65
  if (prop === 'create') {
66
- const [session, ttl] = args as [Session, number | undefined];
66
+ const [session] = args as [Session];
67
67
  if (!session?.userId || !session?.sessionId) return null;
68
68
  return {
69
69
  type: 'create',
70
70
  userId: session.userId,
71
71
  sessionId: session.sessionId,
72
72
  session,
73
- ttl,
74
73
  timestamp,
75
74
  };
76
75
  }
77
76
 
78
77
  if (prop === 'update') {
79
- const [userId, sessionId, patch, ttl] = args as [string, string, Partial<Session>, number | undefined];
78
+ const [userId, sessionId, patch] = args as [string, string, Partial<Session>];
80
79
  if (!userId || !sessionId) return null;
81
80
  return {
82
81
  type: 'update',
83
82
  userId,
84
83
  sessionId,
85
84
  patch,
86
- ttl,
87
85
  timestamp,
88
86
  };
89
87
  }
@@ -1,5 +1,6 @@
1
- import type { SessionStore, Session, SetClientOptions } from './types.js';
1
+ import type { SessionStore, Session, SessionCredentials } from './types.js';
2
2
  import { generateSessionId } from '../../shared/utils.js';
3
+ import { isSessionExpired, mergeSessionUpdate, normalizeNewSession } from './session-lifecycle.js';
3
4
 
4
5
  /**
5
6
  * In-memory implementation of SessionStore
@@ -8,6 +9,7 @@ import { generateSessionId } from '../../shared/utils.js';
8
9
  export class MemoryStorageBackend implements SessionStore {
9
10
  // Map<userId:sessionId, Session>
10
11
  private sessions = new Map<string, Session>();
12
+ private credentials = new Map<string, SessionCredentials>();
11
13
 
12
14
  // Map<userId, Set<sessionId>>
13
15
  private userIdSessions = new Map<string, Set<string>>();
@@ -26,7 +28,7 @@ export class MemoryStorageBackend implements SessionStore {
26
28
  return generateSessionId();
27
29
  }
28
30
 
29
- async create(session: Session, ttl?: number): Promise<void> {
31
+ async create(session: Session): Promise<void> {
30
32
  const { sessionId, userId } = session;
31
33
  if (!sessionId || !userId) throw new Error('userId and sessionId required');
32
34
 
@@ -35,17 +37,16 @@ export class MemoryStorageBackend implements SessionStore {
35
37
  throw new Error(`Session ${sessionId} already exists`);
36
38
  }
37
39
 
38
- this.sessions.set(sessionKey, session);
40
+ this.sessions.set(sessionKey, normalizeNewSession(session));
39
41
 
40
42
  // Update index
41
43
  if (!this.userIdSessions.has(userId)) {
42
44
  this.userIdSessions.set(userId, new Set());
43
45
  }
44
46
  this.userIdSessions.get(userId)!.add(sessionId);
45
- // Note: TTL is ignored in memory backend - sessions don't auto-expire
46
47
  }
47
48
 
48
- async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
49
+ async update(userId: string, sessionId: string, data: Partial<Session>): Promise<void> {
49
50
  if (!userId || !sessionId) throw new Error('userId and sessionId required');
50
51
 
51
52
  const sessionKey = this.getSessionKey(userId, sessionId);
@@ -55,13 +56,19 @@ export class MemoryStorageBackend implements SessionStore {
55
56
  throw new Error(`Session ${sessionId} not found`);
56
57
  }
57
58
 
58
- const updated = {
59
- ...current,
60
- ...data
61
- };
59
+ const updated = mergeSessionUpdate(current, data);
62
60
 
63
61
  this.sessions.set(sessionKey, updated);
64
- // Note: TTL is ignored in memory backend - sessions don't auto-expire
62
+ }
63
+
64
+ async patchCredentials(userId: string, sessionId: string, data: Partial<SessionCredentials>): Promise<void> {
65
+ const sessionKey = this.getSessionKey(userId, sessionId);
66
+ if (!this.sessions.has(sessionKey)) {
67
+ throw new Error(`Session ${sessionId} not found`);
68
+ }
69
+
70
+ const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
71
+ this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
65
72
  }
66
73
 
67
74
 
@@ -70,6 +77,22 @@ export class MemoryStorageBackend implements SessionStore {
70
77
  return this.sessions.get(sessionKey) || null;
71
78
  }
72
79
 
80
+ async getCredentials(userId: string, sessionId: string): Promise<SessionCredentials | null> {
81
+ const sessionKey = this.getSessionKey(userId, sessionId);
82
+ if (!this.sessions.has(sessionKey)) return null;
83
+ return this.credentials.get(sessionKey) ?? { sessionId, userId };
84
+ }
85
+
86
+ async clearCredentials(userId: string, sessionId: string): Promise<void> {
87
+ await this.patchCredentials(userId, sessionId, {
88
+ clientInformation: null,
89
+ tokens: null,
90
+ codeVerifier: null,
91
+ clientId: null,
92
+ oauthState: null,
93
+ });
94
+ }
95
+
73
96
  async listIds(userId: string): Promise<string[]> {
74
97
  const set = this.userIdSessions.get(userId);
75
98
  return set ? Array.from(set) : [];
@@ -92,6 +115,7 @@ export class MemoryStorageBackend implements SessionStore {
92
115
  async delete(userId: string, sessionId: string): Promise<void> {
93
116
  const sessionKey = this.getSessionKey(userId, sessionId);
94
117
  this.sessions.delete(sessionKey);
118
+ this.credentials.delete(sessionKey);
95
119
 
96
120
  const set = this.userIdSessions.get(userId);
97
121
  if (set) {
@@ -108,13 +132,25 @@ export class MemoryStorageBackend implements SessionStore {
108
132
 
109
133
  async clearAll(): Promise<void> {
110
134
  this.sessions.clear();
135
+ this.credentials.clear();
111
136
  this.userIdSessions.clear();
112
137
  }
113
138
 
114
139
  async cleanupExpired(): Promise<void> {
115
- // In-memory doesn't implement TTL automatically,
116
- // but we could check createdAt + TTL here if needed.
117
- // For now, no-op.
140
+ for (const [key, session] of this.sessions.entries()) {
141
+ if (!isSessionExpired(session)) continue;
142
+
143
+ this.sessions.delete(key);
144
+ this.credentials.delete(key);
145
+
146
+ const set = this.userIdSessions.get(session.userId);
147
+ if (set) {
148
+ set.delete(session.sessionId);
149
+ if (set.size === 0) {
150
+ this.userIdSessions.delete(session.userId);
151
+ }
152
+ }
153
+ }
118
154
  }
119
155
 
120
156
  async disconnect(): Promise<void> {