@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.
- 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 +729 -397
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +724 -396
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-DYNe6az3.d.mts → multi-session-client-CIMUGF8S.d.mts} +15 -41
- package/dist/{multi-session-client-BYtguGJm.d.ts → multi-session-client-CnvZEGPY.d.ts} +15 -41
- package/dist/server/index.d.mts +45 -17
- package/dist/server/index.d.ts +45 -17
- package/dist/server/index.js +721 -391
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +718 -391
- 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 -220
- 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
|
@@ -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
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import type { SessionStore, Session,
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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> {
|