@mcp-ts/sdk 1.6.2 → 2.0.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 +12 -6
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- package/dist/adapters/agui-adapter.js +4 -5
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +4 -5
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- 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 +9 -3
- package/dist/adapters/ai-adapter.d.ts +9 -3
- package/dist/adapters/ai-adapter.js +20 -6
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +20 -6
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.js +9 -6
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +9 -6
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.js +5 -1
- package/dist/adapters/mastra-adapter.js.map +1 -1
- package/dist/adapters/mastra-adapter.mjs +5 -1
- package/dist/adapters/mastra-adapter.mjs.map +1 -1
- package/dist/bin/mcp-ts.js +7 -1
- package/dist/bin/mcp-ts.js.map +1 -1
- package/dist/bin/mcp-ts.mjs +7 -1
- package/dist/bin/mcp-ts.mjs.map +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +9 -13
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +9 -13
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +7 -7
- package/dist/client/react.d.ts +7 -7
- package/dist/client/react.js +15 -19
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +15 -19
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +7 -7
- package/dist/client/vue.d.ts +7 -7
- package/dist/client/vue.js +14 -18
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +14 -18
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-DhA-OEAe.d.ts → index-C9gvpxy5.d.ts} +5 -5
- package/dist/{index-bFL4ZF2N.d.mts → index-eaH14_5u.d.mts} +5 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +616 -370
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +615 -370
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CHE8QpVE.d.ts → multi-session-client-BYtguGJm.d.ts} +22 -22
- package/dist/{multi-session-client-CQsRbxYI.d.mts → multi-session-client-DYNe6az3.d.mts} +22 -22
- package/dist/server/index.d.mts +31 -34
- package/dist/server/index.d.ts +31 -34
- package/dist/server/index.js +531 -256
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +530 -256
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +5 -5
- package/dist/shared/index.d.ts +5 -5
- package/dist/shared/index.js +76 -101
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +76 -101
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-Dh2804tM.d.ts → tool-router-Ddtybmr0.d.ts} +71 -73
- package/dist/{tool-router-BVaV1udm.d.mts → tool-router-Dnd6IOKC.d.mts} +71 -73
- package/dist/{types-rIuN1CQi.d.mts → types-BCAG20P6.d.mts} +4 -4
- package/dist/{types-rIuN1CQi.d.ts → types-BCAG20P6.d.ts} +4 -4
- package/dist/{utils-0qmYrqoa.d.mts → utils-DELRKQPU.d.mts} +1 -1
- package/dist/{utils-0qmYrqoa.d.ts → utils-DELRKQPU.d.ts} +1 -1
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +69 -0
- package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
- package/{supabase/migrations → migrations/supabase}/20260330195700_install_mcp_sessions.sql +7 -9
- package/package.json +14 -5
- package/src/adapters/ai-adapter.ts +30 -1
- package/src/adapters/langchain-adapter.ts +6 -2
- package/src/adapters/mastra-adapter.ts +6 -2
- package/src/bin/mcp-ts.ts +8 -1
- package/src/client/core/app-host.ts +1 -1
- package/src/client/core/sse-client.ts +12 -14
- package/src/client/core/types.ts +1 -1
- package/src/client/react/use-mcp-apps.tsx +1 -1
- package/src/client/react/use-mcp.ts +11 -11
- package/src/client/vue/use-mcp.ts +10 -10
- package/src/server/handlers/nextjs-handler.ts +18 -15
- package/src/server/handlers/sse-handler.ts +29 -29
- package/src/server/index.ts +1 -1
- package/src/server/mcp/multi-session-client.ts +17 -17
- package/src/server/mcp/oauth-client.ts +37 -37
- package/src/server/mcp/storage-oauth-provider.ts +17 -17
- package/src/server/storage/file-backend.ts +25 -25
- package/src/server/storage/index.ts +67 -10
- package/src/server/storage/memory-backend.ts +34 -34
- package/src/server/storage/neon-backend.ts +281 -0
- package/src/server/storage/redis-backend.ts +64 -64
- package/src/server/storage/sqlite-backend.ts +33 -33
- package/src/server/storage/supabase-backend.ts +23 -24
- package/src/server/storage/types.ts +18 -21
- package/src/shared/errors.ts +1 -1
- package/src/shared/index.ts +1 -2
- package/src/shared/meta-tools.ts +4 -6
- package/src/shared/schema-compressor.ts +2 -42
- package/src/shared/tool-index.ts +89 -84
- package/src/shared/tool-router.ts +0 -24
- package/src/shared/types.ts +4 -4
- /package/{supabase/migrations → migrations/supabase}/20260421010000_add_session_cleanup_cron.sql +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { SessionStore, Session } from './types.js';
|
|
2
|
+
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
3
|
+
import { generateSessionId } from '../../shared/utils.js';
|
|
4
|
+
import { encryptObject, decryptObject } from './crypto.js';
|
|
5
|
+
|
|
6
|
+
export interface NeonStorageOptions {
|
|
7
|
+
schema?: string;
|
|
8
|
+
table?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type NeonSql = {
|
|
12
|
+
query(queryWithPlaceholders: string, params?: unknown[]): Promise<any[]>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type NeonSessionRow = {
|
|
16
|
+
session_id: string;
|
|
17
|
+
server_id?: string | null;
|
|
18
|
+
server_name?: string | null;
|
|
19
|
+
server_url: string;
|
|
20
|
+
transport_type: 'sse' | 'streamable-http';
|
|
21
|
+
callback_url: string;
|
|
22
|
+
created_at: string | Date;
|
|
23
|
+
user_id: string;
|
|
24
|
+
headers?: unknown;
|
|
25
|
+
active?: boolean | null;
|
|
26
|
+
client_information?: unknown;
|
|
27
|
+
tokens?: unknown;
|
|
28
|
+
code_verifier?: string | null;
|
|
29
|
+
client_id?: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class NeonStorageBackend implements SessionStore {
|
|
33
|
+
private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
|
|
34
|
+
private readonly tableName: string;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly sql: NeonSql,
|
|
38
|
+
options: NeonStorageOptions = {}
|
|
39
|
+
) {
|
|
40
|
+
const schema = options.schema || 'public';
|
|
41
|
+
const table = options.table || 'mcp_sessions';
|
|
42
|
+
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async init(): Promise<void> {
|
|
46
|
+
const [{ exists } = { exists: null }] = await this.sql.query(
|
|
47
|
+
'SELECT to_regclass($1) AS exists',
|
|
48
|
+
[this.tableName.replace(/"/g, '')]
|
|
49
|
+
) as Array<{ exists: string | null }>;
|
|
50
|
+
|
|
51
|
+
if (!exists) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'[NeonStorage] Table "mcp_sessions" not found in your database. ' +
|
|
54
|
+
'Please create it using the Neon storage guide in docs/storage-backends/neon.md.'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('[mcp-ts][Storage] Neon: "mcp_sessions" table verified.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
generateSessionId(): string {
|
|
62
|
+
return generateSessionId();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private quoteIdentifier(identifier: string): string {
|
|
66
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
67
|
+
throw new Error(`Invalid Neon storage identifier: ${identifier}`);
|
|
68
|
+
}
|
|
69
|
+
return `"${identifier}"`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private mapRowToSessionData(row: NeonSessionRow): Session {
|
|
73
|
+
return {
|
|
74
|
+
sessionId: row.session_id,
|
|
75
|
+
serverId: row.server_id ?? undefined,
|
|
76
|
+
serverName: row.server_name ?? undefined,
|
|
77
|
+
serverUrl: row.server_url,
|
|
78
|
+
transportType: row.transport_type,
|
|
79
|
+
callbackUrl: row.callback_url,
|
|
80
|
+
createdAt: new Date(row.created_at).getTime(),
|
|
81
|
+
userId: row.user_id,
|
|
82
|
+
headers: decryptObject(row.headers),
|
|
83
|
+
active: row.active ?? false,
|
|
84
|
+
clientInformation: row.client_information as Session['clientInformation'],
|
|
85
|
+
tokens: decryptObject(row.tokens),
|
|
86
|
+
codeVerifier: row.code_verifier ?? undefined,
|
|
87
|
+
clientId: row.client_id ?? undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async create(session: Session, ttl?: number): Promise<void> {
|
|
92
|
+
const { sessionId, userId } = session;
|
|
93
|
+
if (!sessionId || !userId) throw new Error('userId and sessionId required');
|
|
94
|
+
|
|
95
|
+
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
96
|
+
const expiresAt = new Date(Date.now() + effectiveTtl * 1000).toISOString();
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await this.sql.query(
|
|
100
|
+
`INSERT INTO ${this.tableName} (
|
|
101
|
+
session_id,
|
|
102
|
+
user_id,
|
|
103
|
+
server_id,
|
|
104
|
+
server_name,
|
|
105
|
+
server_url,
|
|
106
|
+
transport_type,
|
|
107
|
+
callback_url,
|
|
108
|
+
created_at,
|
|
109
|
+
headers,
|
|
110
|
+
active,
|
|
111
|
+
client_information,
|
|
112
|
+
tokens,
|
|
113
|
+
code_verifier,
|
|
114
|
+
client_id,
|
|
115
|
+
expires_at
|
|
116
|
+
) VALUES (
|
|
117
|
+
$1, $2, $3, $4, $5, $6, $7, $8,
|
|
118
|
+
$9, $10, $11, $12, $13, $14, $15
|
|
119
|
+
)`,
|
|
120
|
+
[
|
|
121
|
+
sessionId,
|
|
122
|
+
userId,
|
|
123
|
+
session.serverId,
|
|
124
|
+
session.serverName,
|
|
125
|
+
session.serverUrl,
|
|
126
|
+
session.transportType,
|
|
127
|
+
session.callbackUrl,
|
|
128
|
+
new Date(session.createdAt || Date.now()).toISOString(),
|
|
129
|
+
encryptObject(session.headers),
|
|
130
|
+
session.active ?? false,
|
|
131
|
+
session.clientInformation,
|
|
132
|
+
encryptObject(session.tokens),
|
|
133
|
+
session.codeVerifier,
|
|
134
|
+
session.clientId,
|
|
135
|
+
expiresAt,
|
|
136
|
+
]
|
|
137
|
+
);
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
if (error.code === '23505') {
|
|
140
|
+
throw new Error(`Session ${sessionId} already exists`);
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Failed to create session in Neon: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
|
|
147
|
+
const currentSession = await this.get(userId, sessionId);
|
|
148
|
+
if (!currentSession) {
|
|
149
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const updatedSession = { ...currentSession, ...data };
|
|
153
|
+
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
154
|
+
const expiresAt = new Date(Date.now() + effectiveTtl * 1000).toISOString();
|
|
155
|
+
|
|
156
|
+
const updatedRows = await this.sql.query(
|
|
157
|
+
`UPDATE ${this.tableName}
|
|
158
|
+
SET
|
|
159
|
+
server_id = $1,
|
|
160
|
+
server_name = $2,
|
|
161
|
+
server_url = $3,
|
|
162
|
+
transport_type = $4,
|
|
163
|
+
callback_url = $5,
|
|
164
|
+
active = $6,
|
|
165
|
+
headers = $7,
|
|
166
|
+
client_information = $8,
|
|
167
|
+
tokens = $9,
|
|
168
|
+
code_verifier = $10,
|
|
169
|
+
client_id = $11,
|
|
170
|
+
expires_at = $12,
|
|
171
|
+
updated_at = now()
|
|
172
|
+
WHERE user_id = $13 AND session_id = $14
|
|
173
|
+
RETURNING id`,
|
|
174
|
+
[
|
|
175
|
+
updatedSession.serverId,
|
|
176
|
+
updatedSession.serverName,
|
|
177
|
+
updatedSession.serverUrl,
|
|
178
|
+
updatedSession.transportType,
|
|
179
|
+
updatedSession.callbackUrl,
|
|
180
|
+
updatedSession.active ?? false,
|
|
181
|
+
encryptObject(updatedSession.headers),
|
|
182
|
+
updatedSession.clientInformation,
|
|
183
|
+
encryptObject(updatedSession.tokens),
|
|
184
|
+
updatedSession.codeVerifier,
|
|
185
|
+
updatedSession.clientId,
|
|
186
|
+
expiresAt,
|
|
187
|
+
userId,
|
|
188
|
+
sessionId,
|
|
189
|
+
]
|
|
190
|
+
) as Array<{ id: string }>;
|
|
191
|
+
|
|
192
|
+
if (updatedRows.length === 0) {
|
|
193
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async get(userId: string, sessionId: string): Promise<Session | null> {
|
|
198
|
+
try {
|
|
199
|
+
const rows = await this.sql.query(
|
|
200
|
+
`SELECT * FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
201
|
+
[userId, sessionId]
|
|
202
|
+
) as NeonSessionRow[];
|
|
203
|
+
return rows[0] ? this.mapRowToSessionData(rows[0]) : null;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('[NeonStorage] Failed to get session:', error);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async list(userId: string): Promise<Session[]> {
|
|
211
|
+
try {
|
|
212
|
+
const rows = await this.sql.query(
|
|
213
|
+
`SELECT * FROM ${this.tableName} WHERE user_id = $1`,
|
|
214
|
+
[userId]
|
|
215
|
+
) as NeonSessionRow[];
|
|
216
|
+
return rows.map((row) => this.mapRowToSessionData(row));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(`[NeonStorage] Failed to get session data for ${userId}:`, error);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async delete(userId: string, sessionId: string): Promise<void> {
|
|
224
|
+
try {
|
|
225
|
+
await this.sql.query(
|
|
226
|
+
`DELETE FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
227
|
+
[userId, sessionId]
|
|
228
|
+
);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('[NeonStorage] Failed to remove session:', error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async listIds(userId: string): Promise<string[]> {
|
|
235
|
+
try {
|
|
236
|
+
const rows = await this.sql.query(
|
|
237
|
+
`SELECT session_id FROM ${this.tableName} WHERE user_id = $1`,
|
|
238
|
+
[userId]
|
|
239
|
+
) as Array<{ session_id: string }>;
|
|
240
|
+
return rows.map((row) => row.session_id);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(`[NeonStorage] Failed to get sessions for ${userId}:`, error);
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async listAllIds(): Promise<string[]> {
|
|
248
|
+
try {
|
|
249
|
+
const rows = await this.sql.query(
|
|
250
|
+
`SELECT session_id FROM ${this.tableName}`
|
|
251
|
+
) as Array<{ session_id: string }>;
|
|
252
|
+
return rows.map((row) => row.session_id);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error('[NeonStorage] Failed to get all sessions:', error);
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async clearAll(): Promise<void> {
|
|
260
|
+
try {
|
|
261
|
+
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('[NeonStorage] Failed to clear sessions:', error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async cleanupExpired(): Promise<void> {
|
|
268
|
+
try {
|
|
269
|
+
await this.sql.query(
|
|
270
|
+
`DELETE FROM ${this.tableName} WHERE expires_at < $1`,
|
|
271
|
+
[new Date().toISOString()]
|
|
272
|
+
);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error('[NeonStorage] Failed to cleanup expired sessions:', error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async disconnect(): Promise<void> {
|
|
279
|
+
// Neon HTTP queries do not hold a persistent connection.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Redis } from 'ioredis';
|
|
2
|
-
import {
|
|
2
|
+
import type { SessionStore, Session } from './types.js';
|
|
3
3
|
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
4
4
|
import { generateSessionId } from '../../shared/utils.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Redis implementation of
|
|
7
|
+
* Redis implementation of SessionStore
|
|
8
8
|
*/
|
|
9
|
-
export class RedisStorageBackend implements
|
|
9
|
+
export class RedisStorageBackend implements SessionStore {
|
|
10
10
|
private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
|
|
11
11
|
private readonly KEY_PREFIX = 'mcp:session:';
|
|
12
|
-
private readonly
|
|
13
|
-
private readonly
|
|
12
|
+
private readonly USER_ID_KEY_PREFIX = 'mcp:userId:';
|
|
13
|
+
private readonly USER_ID_KEY_SUFFIX = ':sessions';
|
|
14
14
|
|
|
15
15
|
constructor(private redis: Redis) { }
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
19
19
|
await this.redis.ping();
|
|
20
20
|
console.log('[mcp-ts][Storage] Redis: ✓ Connected to server.');
|
|
21
21
|
} catch (error: any) {
|
|
22
|
-
throw new Error(`[
|
|
22
|
+
throw new Error(`[RedisStorageBackend] Failed to connect to Redis: ${error.message}`);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -27,22 +27,22 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
27
27
|
* Generates Redis key for a specific session
|
|
28
28
|
* @private
|
|
29
29
|
*/
|
|
30
|
-
private getSessionKey(
|
|
31
|
-
return `${this.KEY_PREFIX}${
|
|
30
|
+
private getSessionKey(userId: string, sessionId: string): string {
|
|
31
|
+
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Generates Redis key for tracking all sessions for
|
|
35
|
+
* Generates Redis key for tracking all sessions for a user
|
|
36
36
|
* @private
|
|
37
37
|
*/
|
|
38
|
-
private
|
|
39
|
-
return `${this.
|
|
38
|
+
private getUserIdKey(userId: string): string {
|
|
39
|
+
return `${this.USER_ID_KEY_PREFIX}${userId}${this.USER_ID_KEY_SUFFIX}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
private
|
|
43
|
-
return
|
|
44
|
-
this.
|
|
45
|
-
|
|
42
|
+
private parseUserIdFromKey(userIdKey: string): string {
|
|
43
|
+
return userIdKey.slice(
|
|
44
|
+
this.USER_ID_KEY_PREFIX.length,
|
|
45
|
+
userIdKey.length - this.USER_ID_KEY_SUFFIX.length
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -67,7 +67,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
67
67
|
}
|
|
68
68
|
} while (cursor !== '0');
|
|
69
69
|
} catch (error) {
|
|
70
|
-
console.warn('[
|
|
70
|
+
console.warn('[RedisStorageBackend] SCAN failed, falling back to KEYS:', error);
|
|
71
71
|
return await this.redis.keys(pattern);
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -78,12 +78,12 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
78
78
|
return generateSessionId();
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async
|
|
82
|
-
const { sessionId,
|
|
83
|
-
if (!sessionId || !
|
|
81
|
+
async create(session: Session, ttl?: number): Promise<void> {
|
|
82
|
+
const { sessionId, userId } = session;
|
|
83
|
+
if (!sessionId || !userId) throw new Error('userId and sessionId required');
|
|
84
84
|
|
|
85
|
-
const sessionKey = this.getSessionKey(
|
|
86
|
-
const
|
|
85
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
86
|
+
const userIdKey = this.getUserIdKey(userId);
|
|
87
87
|
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
88
88
|
|
|
89
89
|
/** ioredis syntax: set(key, val, 'EX', ttl, 'NX') */
|
|
@@ -99,10 +99,10 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
99
99
|
throw new Error(`Session ${sessionId} already exists`);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
await this.redis.sadd(
|
|
102
|
+
await this.redis.sadd(userIdKey, sessionId);
|
|
103
103
|
}
|
|
104
|
-
async
|
|
105
|
-
const sessionKey = this.getSessionKey(
|
|
104
|
+
async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
|
|
105
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
106
106
|
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
107
107
|
|
|
108
108
|
/** Lua script for atomic parsing, merging, and saving */
|
|
@@ -132,70 +132,70 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
132
132
|
);
|
|
133
133
|
|
|
134
134
|
if (result === 0) {
|
|
135
|
-
throw new Error(`Session ${sessionId} not found for
|
|
135
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async
|
|
139
|
+
async get(userId: string, sessionId: string): Promise<Session | null> {
|
|
140
140
|
try {
|
|
141
|
-
const sessionKey = this.getSessionKey(
|
|
141
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
142
142
|
const sessionDataStr = await this.redis.get(sessionKey);
|
|
143
143
|
|
|
144
144
|
if (!sessionDataStr) {
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
const
|
|
149
|
-
return
|
|
148
|
+
const Session: Session = JSON.parse(sessionDataStr);
|
|
149
|
+
return Session;
|
|
150
150
|
} catch (error) {
|
|
151
|
-
console.error('[
|
|
151
|
+
console.error('[RedisStorageBackend] Failed to get session:', error);
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
async
|
|
157
|
-
const sessions = await this.
|
|
156
|
+
async listIds(userId: string): Promise<string[]> {
|
|
157
|
+
const sessions = await this.list(userId);
|
|
158
158
|
return sessions.map((session) => session.sessionId);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
async
|
|
161
|
+
async list(userId: string): Promise<Session[]> {
|
|
162
162
|
try {
|
|
163
|
-
const
|
|
164
|
-
const sessionIds = await this.redis.smembers(
|
|
163
|
+
const userIdKey = this.getUserIdKey(userId);
|
|
164
|
+
const sessionIds = await this.redis.smembers(userIdKey);
|
|
165
165
|
if (sessionIds.length === 0) return [];
|
|
166
166
|
|
|
167
167
|
const results = await Promise.all(
|
|
168
168
|
sessionIds.map(async (sessionId) => {
|
|
169
|
-
const data = await this.redis.get(this.getSessionKey(
|
|
170
|
-
return data ? (JSON.parse(data) as
|
|
169
|
+
const data = await this.redis.get(this.getSessionKey(userId, sessionId));
|
|
170
|
+
return data ? (JSON.parse(data) as Session) : null;
|
|
171
171
|
})
|
|
172
172
|
);
|
|
173
173
|
|
|
174
174
|
const staleSessionIds = sessionIds.filter((_, index) => results[index] === null);
|
|
175
175
|
if (staleSessionIds.length > 0) {
|
|
176
|
-
await this.redis.srem(
|
|
176
|
+
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
return results.filter((session): session is
|
|
179
|
+
return results.filter((session): session is Session => session !== null);
|
|
180
180
|
} catch (error) {
|
|
181
|
-
console.error(`[
|
|
181
|
+
console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
|
|
182
182
|
return [];
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
async
|
|
186
|
+
async delete(userId: string, sessionId: string): Promise<void> {
|
|
187
187
|
try {
|
|
188
|
-
const sessionKey = this.getSessionKey(
|
|
189
|
-
const
|
|
188
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
189
|
+
const userIdKey = this.getUserIdKey(userId);
|
|
190
190
|
|
|
191
|
-
await this.redis.srem(
|
|
191
|
+
await this.redis.srem(userIdKey, sessionId);
|
|
192
192
|
await this.redis.del(sessionKey);
|
|
193
193
|
} catch (error) {
|
|
194
|
-
console.error('[
|
|
194
|
+
console.error('[RedisStorageBackend] Failed to remove session:', error);
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
async
|
|
198
|
+
async listAllIds(): Promise<string[]> {
|
|
199
199
|
try {
|
|
200
200
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
201
201
|
const sessions = await Promise.all(
|
|
@@ -206,9 +206,9 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
try {
|
|
209
|
-
return (JSON.parse(data) as
|
|
209
|
+
return (JSON.parse(data) as Session).sessionId;
|
|
210
210
|
} catch (error) {
|
|
211
|
-
console.error('[
|
|
211
|
+
console.error('[RedisStorageBackend] Failed to parse session while listing all session IDs:', error);
|
|
212
212
|
return null;
|
|
213
213
|
}
|
|
214
214
|
})
|
|
@@ -216,7 +216,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
216
216
|
|
|
217
217
|
return sessions.filter((sessionId): sessionId is string => sessionId !== null);
|
|
218
218
|
} catch (error) {
|
|
219
|
-
console.error('[
|
|
219
|
+
console.error('[RedisStorageBackend] Failed to get all sessions:', error);
|
|
220
220
|
return [];
|
|
221
221
|
}
|
|
222
222
|
}
|
|
@@ -224,45 +224,45 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
224
224
|
async clearAll(): Promise<void> {
|
|
225
225
|
try {
|
|
226
226
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
227
|
-
const
|
|
228
|
-
const allKeys = [...keys, ...
|
|
227
|
+
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
228
|
+
const allKeys = [...keys, ...userIdKeys];
|
|
229
229
|
if (allKeys.length > 0) {
|
|
230
230
|
await this.redis.del(...allKeys);
|
|
231
231
|
}
|
|
232
232
|
} catch (error) {
|
|
233
|
-
console.error('[
|
|
233
|
+
console.error('[RedisStorageBackend] Failed to clear sessions:', error);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
async
|
|
237
|
+
async cleanupExpired(): Promise<void> {
|
|
238
238
|
try {
|
|
239
|
-
const
|
|
239
|
+
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
240
240
|
|
|
241
|
-
for (const
|
|
242
|
-
const
|
|
243
|
-
const sessionIds = await this.redis.smembers(
|
|
241
|
+
for (const userIdKey of userIdKeys) {
|
|
242
|
+
const userId = this.parseUserIdFromKey(userIdKey);
|
|
243
|
+
const sessionIds = await this.redis.smembers(userIdKey);
|
|
244
244
|
|
|
245
245
|
if (sessionIds.length === 0) {
|
|
246
|
-
await this.redis.del(
|
|
246
|
+
await this.redis.del(userIdKey);
|
|
247
247
|
continue;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
const existenceChecks = await Promise.all(
|
|
251
|
-
sessionIds.map((sessionId) => this.redis.exists(this.getSessionKey(
|
|
251
|
+
sessionIds.map((sessionId) => this.redis.exists(this.getSessionKey(userId, sessionId)))
|
|
252
252
|
);
|
|
253
253
|
|
|
254
254
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
255
255
|
if (staleSessionIds.length > 0) {
|
|
256
|
-
await this.redis.srem(
|
|
256
|
+
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
const remainingCount = await this.redis.scard(
|
|
259
|
+
const remainingCount = await this.redis.scard(userIdKey);
|
|
260
260
|
if (remainingCount === 0) {
|
|
261
|
-
await this.redis.del(
|
|
261
|
+
await this.redis.del(userIdKey);
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
} catch (error) {
|
|
265
|
-
console.error('[
|
|
265
|
+
console.error('[RedisStorageBackend] Failed to cleanup expired sessions:', error);
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -270,7 +270,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
270
270
|
try {
|
|
271
271
|
await this.redis.quit();
|
|
272
272
|
} catch (error) {
|
|
273
|
-
console.error('[
|
|
273
|
+
console.error('[RedisStorageBackend] Failed to disconnect:', error);
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
}
|