@frontmcp/sdk 0.6.2 → 0.6.3
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/auth/session/index.d.ts +4 -2
- package/auth/session/redis-session.store.d.ts +26 -3
- package/auth/session/session-crypto.d.ts +86 -0
- package/auth/session/session-rate-limiter.d.ts +113 -0
- package/auth/session/transport-session.types.d.ts +51 -34
- package/auth/session/vercel-kv-session.store.d.ts +22 -2
- package/esm/index.mjs +644 -89
- package/esm/package.json +25 -17
- package/index.js +660 -104
- package/package.json +12 -2
- package/transport/adapters/sse-transport.d.ts +65 -0
- package/transport/adapters/streamable-http-transport.d.ts +69 -0
- package/transport/adapters/transport.local.adapter.d.ts +15 -1
- package/transport/adapters/transport.sse.adapter.d.ts +16 -3
- package/transport/adapters/transport.streamable-http.adapter.d.ts +12 -3
- package/transport/index.d.ts +21 -0
- package/transport/transport.local.d.ts +6 -0
- package/transport/transport.registry.d.ts +7 -1
- package/transport/transport.remote.d.ts +1 -0
- package/transport/transport.types.d.ts +6 -0
package/esm/index.mjs
CHANGED
|
@@ -512,7 +512,7 @@ var init_http_options = __esm({
|
|
|
512
512
|
|
|
513
513
|
// libs/sdk/src/auth/session/transport-session.types.ts
|
|
514
514
|
import { z as z6 } from "zod";
|
|
515
|
-
var transportProtocolSchema, sseTransportStateSchema, streamableHttpTransportStateSchema, statefulHttpTransportStateSchema, statelessHttpTransportStateSchema, legacySseTransportStateSchema, transportStateSchema, transportSessionSchema, sessionJwtPayloadSchema,
|
|
515
|
+
var transportProtocolSchema, sseTransportStateSchema, streamableHttpTransportStateSchema, statefulHttpTransportStateSchema, statelessHttpTransportStateSchema, legacySseTransportStateSchema, transportStateSchema, transportSessionSchema, sessionJwtPayloadSchema, encryptedBlobSchema, storedSessionSchema, redisConfigSchema;
|
|
516
516
|
var init_transport_session_types = __esm({
|
|
517
517
|
"libs/sdk/src/auth/session/transport-session.types.ts"() {
|
|
518
518
|
"use strict";
|
|
@@ -577,10 +577,6 @@ var init_transport_session_types = __esm({
|
|
|
577
577
|
iat: z6.number(),
|
|
578
578
|
exp: z6.number().optional()
|
|
579
579
|
});
|
|
580
|
-
statelessSessionJwtPayloadSchema = sessionJwtPayloadSchema.extend({
|
|
581
|
-
state: z6.string().optional(),
|
|
582
|
-
tokens: z6.string().optional()
|
|
583
|
-
});
|
|
584
580
|
encryptedBlobSchema = z6.object({
|
|
585
581
|
alg: z6.literal("A256GCM"),
|
|
586
582
|
kid: z6.string().optional(),
|
|
@@ -595,7 +591,9 @@ var init_transport_session_types = __esm({
|
|
|
595
591
|
authorizationId: z6.string(),
|
|
596
592
|
tokens: z6.record(z6.string(), encryptedBlobSchema).optional(),
|
|
597
593
|
createdAt: z6.number(),
|
|
598
|
-
lastAccessedAt: z6.number()
|
|
594
|
+
lastAccessedAt: z6.number(),
|
|
595
|
+
initialized: z6.boolean().optional(),
|
|
596
|
+
maxLifetimeAt: z6.number().optional()
|
|
599
597
|
});
|
|
600
598
|
redisConfigSchema = z6.object({
|
|
601
599
|
host: z6.string().min(1),
|
|
@@ -607,15 +605,6 @@ var init_transport_session_types = __esm({
|
|
|
607
605
|
defaultTtlMs: z6.number().int().positive().optional().default(36e5)
|
|
608
606
|
// 1 hour default
|
|
609
607
|
});
|
|
610
|
-
statefulStorageSchema = z6.discriminatedUnion("store", [
|
|
611
|
-
z6.object({ store: z6.literal("memory") }),
|
|
612
|
-
z6.object({ store: z6.literal("redis"), config: redisConfigSchema })
|
|
613
|
-
]);
|
|
614
|
-
sessionStorageConfigSchema = z6.union([
|
|
615
|
-
z6.object({ mode: z6.literal("stateless") }),
|
|
616
|
-
z6.object({ mode: z6.literal("stateful") }).merge(statefulStorageSchema.options[0]),
|
|
617
|
-
z6.object({ mode: z6.literal("stateful") }).merge(statefulStorageSchema.options[1])
|
|
618
|
-
]);
|
|
619
608
|
}
|
|
620
609
|
});
|
|
621
610
|
|
|
@@ -15741,8 +15730,19 @@ var init_http_request_flow = __esm({
|
|
|
15741
15730
|
try {
|
|
15742
15731
|
const { request } = this.rawInput;
|
|
15743
15732
|
this.logger.verbose(`[${this.requestId}] router: check request decision`);
|
|
15744
|
-
const transport = this.scope.auth.transport;
|
|
15733
|
+
const transport = this.scope.metadata.transport ?? this.scope.auth.transport;
|
|
15734
|
+
this.logger.debug(`[${this.requestId}] transport config`, {
|
|
15735
|
+
enableLegacySSE: transport.enableLegacySSE,
|
|
15736
|
+
enableStreamableHttp: transport.enableStreamableHttp,
|
|
15737
|
+
path: request.path,
|
|
15738
|
+
accept: request.headers?.["accept"]
|
|
15739
|
+
});
|
|
15745
15740
|
const decision = decideIntent(request, { ...transport, tolerateMissingAccept: true });
|
|
15741
|
+
this.logger.debug(`[${this.requestId}] decision result`, {
|
|
15742
|
+
intent: decision.intent,
|
|
15743
|
+
reasons: decision.reasons,
|
|
15744
|
+
debug: decision.debug
|
|
15745
|
+
});
|
|
15746
15746
|
if (request.method.toUpperCase() === "DELETE") {
|
|
15747
15747
|
this.logger.verbose(`[${this.requestId}] DELETE request, using decision intent: ${decision.intent}`);
|
|
15748
15748
|
if (decision.intent === "unknown") {
|
|
@@ -16056,6 +16056,8 @@ var init_transport_remote = __esm({
|
|
|
16056
16056
|
async destroy(_reason) {
|
|
16057
16057
|
throw new Error("RemoteTransporter: destroy() not implemented.");
|
|
16058
16058
|
}
|
|
16059
|
+
markAsInitialized() {
|
|
16060
|
+
}
|
|
16059
16061
|
};
|
|
16060
16062
|
}
|
|
16061
16063
|
});
|
|
@@ -16235,6 +16237,79 @@ data: ${JSON.stringify(message)}
|
|
|
16235
16237
|
}
|
|
16236
16238
|
});
|
|
16237
16239
|
|
|
16240
|
+
// libs/sdk/src/transport/adapters/sse-transport.ts
|
|
16241
|
+
var RecreateableSSEServerTransport;
|
|
16242
|
+
var init_sse_transport = __esm({
|
|
16243
|
+
"libs/sdk/src/transport/adapters/sse-transport.ts"() {
|
|
16244
|
+
"use strict";
|
|
16245
|
+
init_legacy_sse_tranporter();
|
|
16246
|
+
RecreateableSSEServerTransport = class extends SSEServerTransport {
|
|
16247
|
+
_isRecreatedSession = false;
|
|
16248
|
+
constructor(endpoint, res, options) {
|
|
16249
|
+
super(endpoint, res, options);
|
|
16250
|
+
if (options?.initialEventId !== void 0 && this.isValidEventId(options.initialEventId)) {
|
|
16251
|
+
this.setEventIdCounter(options.initialEventId);
|
|
16252
|
+
this._isRecreatedSession = true;
|
|
16253
|
+
}
|
|
16254
|
+
}
|
|
16255
|
+
/**
|
|
16256
|
+
* Validates that an event ID is a valid non-negative integer.
|
|
16257
|
+
* Protects against negative values, NaN, Infinity, and non-integer values.
|
|
16258
|
+
*/
|
|
16259
|
+
isValidEventId(eventId) {
|
|
16260
|
+
return Number.isInteger(eventId) && eventId >= 0 && eventId <= Number.MAX_SAFE_INTEGER;
|
|
16261
|
+
}
|
|
16262
|
+
/**
|
|
16263
|
+
* Returns whether this is a recreated session.
|
|
16264
|
+
*/
|
|
16265
|
+
get isRecreatedSession() {
|
|
16266
|
+
return this._isRecreatedSession;
|
|
16267
|
+
}
|
|
16268
|
+
/**
|
|
16269
|
+
* Returns the current event ID counter value.
|
|
16270
|
+
* Alias for lastEventId for consistency with the recreation API.
|
|
16271
|
+
*/
|
|
16272
|
+
get eventIdCounter() {
|
|
16273
|
+
return this.lastEventId;
|
|
16274
|
+
}
|
|
16275
|
+
/**
|
|
16276
|
+
* Sets the event ID counter for session recreation.
|
|
16277
|
+
* Use this when recreating a session from stored state to maintain
|
|
16278
|
+
* event ID continuity for SSE reconnection support.
|
|
16279
|
+
*
|
|
16280
|
+
* @param eventId - The event ID to restore (must be a non-negative integer)
|
|
16281
|
+
*/
|
|
16282
|
+
setEventIdCounter(eventId) {
|
|
16283
|
+
if (!this.isValidEventId(eventId)) {
|
|
16284
|
+
console.warn(
|
|
16285
|
+
`[RecreateableSSEServerTransport] Invalid eventId: ${eventId}. Must be a non-negative integer. Ignoring.`
|
|
16286
|
+
);
|
|
16287
|
+
return;
|
|
16288
|
+
}
|
|
16289
|
+
this._eventIdCounter = eventId;
|
|
16290
|
+
}
|
|
16291
|
+
/**
|
|
16292
|
+
* Sets the transport to a recreated session state.
|
|
16293
|
+
* Use this when recreating a transport from a stored session.
|
|
16294
|
+
*
|
|
16295
|
+
* @param sessionId - The session ID (for verification, should match constructor)
|
|
16296
|
+
* @param lastEventId - The last event ID that was sent to the client
|
|
16297
|
+
*/
|
|
16298
|
+
setSessionState(sessionId, lastEventId) {
|
|
16299
|
+
if (this.sessionId !== sessionId) {
|
|
16300
|
+
console.warn(
|
|
16301
|
+
`RecreateableSSEServerTransport: session ID mismatch. Expected ${sessionId}, got ${this.sessionId}. Using constructor value.`
|
|
16302
|
+
);
|
|
16303
|
+
}
|
|
16304
|
+
if (lastEventId !== void 0) {
|
|
16305
|
+
this.setEventIdCounter(lastEventId);
|
|
16306
|
+
}
|
|
16307
|
+
this._isRecreatedSession = true;
|
|
16308
|
+
}
|
|
16309
|
+
};
|
|
16310
|
+
}
|
|
16311
|
+
});
|
|
16312
|
+
|
|
16238
16313
|
// libs/sdk/src/transport/transport.event-store.ts
|
|
16239
16314
|
var InMemoryEventStore;
|
|
16240
16315
|
var init_transport_event_store = __esm({
|
|
@@ -16729,6 +16804,12 @@ var init_transport_local_adapter = __esm({
|
|
|
16729
16804
|
#requestId = 1;
|
|
16730
16805
|
ready;
|
|
16731
16806
|
server;
|
|
16807
|
+
/**
|
|
16808
|
+
* Marks this transport as pre-initialized for session recreation.
|
|
16809
|
+
* Override in subclasses that need to set the MCP SDK's _initialized flag.
|
|
16810
|
+
*/
|
|
16811
|
+
markAsInitialized() {
|
|
16812
|
+
}
|
|
16732
16813
|
connectServer() {
|
|
16733
16814
|
const { info } = this.scope.metadata;
|
|
16734
16815
|
const hasPrompts = this.scope.prompts.hasAny();
|
|
@@ -16862,22 +16943,45 @@ var TransportSSEAdapter;
|
|
|
16862
16943
|
var init_transport_sse_adapter = __esm({
|
|
16863
16944
|
"libs/sdk/src/transport/adapters/transport.sse.adapter.ts"() {
|
|
16864
16945
|
"use strict";
|
|
16865
|
-
|
|
16946
|
+
init_sse_transport();
|
|
16866
16947
|
init_transport_local_adapter();
|
|
16867
16948
|
init_transport_error();
|
|
16868
16949
|
TransportSSEAdapter = class extends LocalTransportAdapter {
|
|
16869
16950
|
sessionId;
|
|
16951
|
+
/**
|
|
16952
|
+
* Configures common error and close handlers for SSE transports.
|
|
16953
|
+
*/
|
|
16954
|
+
configureTransportHandlers(transport) {
|
|
16955
|
+
transport.onerror = (error) => {
|
|
16956
|
+
console.error("SSE error:", error instanceof Error ? error.message : "Unknown error");
|
|
16957
|
+
};
|
|
16958
|
+
transport.onclose = this.destroy.bind(this);
|
|
16959
|
+
}
|
|
16870
16960
|
createTransport(sessionId, res) {
|
|
16871
16961
|
this.sessionId = sessionId;
|
|
16872
16962
|
this.logger.info(`new transport session: ${sessionId.slice(0, 40)}`);
|
|
16873
|
-
const
|
|
16874
|
-
const transport = new SSEServerTransport(`${scopePath}/message`, res, {
|
|
16963
|
+
const transport = new RecreateableSSEServerTransport(`${this.scope.fullPath}/message`, res, {
|
|
16875
16964
|
sessionId
|
|
16876
16965
|
});
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16966
|
+
this.configureTransportHandlers(transport);
|
|
16967
|
+
return transport;
|
|
16968
|
+
}
|
|
16969
|
+
/**
|
|
16970
|
+
* Recreates a transport with preserved session state.
|
|
16971
|
+
* Use this when restoring a session from Redis or other storage.
|
|
16972
|
+
*
|
|
16973
|
+
* @param sessionId - The session ID to restore
|
|
16974
|
+
* @param res - The new response stream for SSE
|
|
16975
|
+
* @param lastEventId - The last event ID that was sent (for reconnection support)
|
|
16976
|
+
*/
|
|
16977
|
+
createTransportFromSession(sessionId, res, lastEventId) {
|
|
16978
|
+
this.sessionId = sessionId;
|
|
16979
|
+
this.logger.info(`recreating transport session: ${sessionId.slice(0, 40)}, lastEventId: ${lastEventId ?? "none"}`);
|
|
16980
|
+
const transport = new RecreateableSSEServerTransport(`${this.scope.fullPath}/message`, res, {
|
|
16981
|
+
sessionId,
|
|
16982
|
+
initialEventId: lastEventId
|
|
16983
|
+
});
|
|
16984
|
+
this.configureTransportHandlers(transport);
|
|
16881
16985
|
return transport;
|
|
16882
16986
|
}
|
|
16883
16987
|
initialize(req, res) {
|
|
@@ -16924,8 +17028,58 @@ var init_transport_sse_adapter = __esm({
|
|
|
16924
17028
|
}
|
|
16925
17029
|
});
|
|
16926
17030
|
|
|
16927
|
-
// libs/sdk/src/transport/adapters/
|
|
17031
|
+
// libs/sdk/src/transport/adapters/streamable-http-transport.ts
|
|
16928
17032
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
17033
|
+
var RecreateableStreamableHTTPServerTransport;
|
|
17034
|
+
var init_streamable_http_transport = __esm({
|
|
17035
|
+
"libs/sdk/src/transport/adapters/streamable-http-transport.ts"() {
|
|
17036
|
+
"use strict";
|
|
17037
|
+
RecreateableStreamableHTTPServerTransport = class extends StreamableHTTPServerTransport {
|
|
17038
|
+
constructor(options = {}) {
|
|
17039
|
+
super(options);
|
|
17040
|
+
}
|
|
17041
|
+
/**
|
|
17042
|
+
* Returns whether the transport has been initialized.
|
|
17043
|
+
*/
|
|
17044
|
+
get isInitialized() {
|
|
17045
|
+
return this._webStandardTransport?._initialized ?? false;
|
|
17046
|
+
}
|
|
17047
|
+
/**
|
|
17048
|
+
* Sets the transport to an initialized state with the given session ID.
|
|
17049
|
+
* Use this when recreating a transport from a stored session.
|
|
17050
|
+
*
|
|
17051
|
+
* This method allows you to "restore" a session without replaying the
|
|
17052
|
+
* initialization handshake. After calling this method, the transport
|
|
17053
|
+
* will accept requests with the given session ID.
|
|
17054
|
+
*
|
|
17055
|
+
* @param sessionId - The session ID that was previously assigned to this session
|
|
17056
|
+
* @throws Error if sessionId is empty or invalid
|
|
17057
|
+
*/
|
|
17058
|
+
setInitializationState(sessionId) {
|
|
17059
|
+
if (!sessionId || typeof sessionId !== "string" || sessionId.trim() === "") {
|
|
17060
|
+
throw new Error("[RecreateableStreamableHTTPServerTransport] sessionId cannot be empty");
|
|
17061
|
+
}
|
|
17062
|
+
const webTransport = this._webStandardTransport;
|
|
17063
|
+
if (!webTransport) {
|
|
17064
|
+
console.warn(
|
|
17065
|
+
"[RecreateableStreamableHTTPServerTransport] Internal transport not found. This may indicate an incompatible MCP SDK version."
|
|
17066
|
+
);
|
|
17067
|
+
return;
|
|
17068
|
+
}
|
|
17069
|
+
if (!("_initialized" in webTransport) || !("sessionId" in webTransport)) {
|
|
17070
|
+
console.warn(
|
|
17071
|
+
"[RecreateableStreamableHTTPServerTransport] Expected fields not found on internal transport. This may indicate an incompatible MCP SDK version."
|
|
17072
|
+
);
|
|
17073
|
+
return;
|
|
17074
|
+
}
|
|
17075
|
+
webTransport._initialized = true;
|
|
17076
|
+
webTransport.sessionId = sessionId;
|
|
17077
|
+
}
|
|
17078
|
+
};
|
|
17079
|
+
}
|
|
17080
|
+
});
|
|
17081
|
+
|
|
17082
|
+
// libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts
|
|
16929
17083
|
import { toJSONSchema as toJSONSchema3 } from "zod/v4";
|
|
16930
17084
|
var resolveSessionIdGenerator, TransportStreamableHttpAdapter;
|
|
16931
17085
|
var init_transport_streamable_http_adapter = __esm({
|
|
@@ -16933,13 +17087,14 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
16933
17087
|
"use strict";
|
|
16934
17088
|
init_transport_local_adapter();
|
|
16935
17089
|
init_transport_error();
|
|
17090
|
+
init_streamable_http_transport();
|
|
16936
17091
|
resolveSessionIdGenerator = (transportType, sessionId) => {
|
|
16937
17092
|
return transportType === "stateless-http" ? void 0 : () => sessionId;
|
|
16938
17093
|
};
|
|
16939
17094
|
TransportStreamableHttpAdapter = class extends LocalTransportAdapter {
|
|
16940
17095
|
createTransport(sessionId, response) {
|
|
16941
17096
|
const sessionIdGenerator = resolveSessionIdGenerator(this.key.type, sessionId);
|
|
16942
|
-
return new
|
|
17097
|
+
return new RecreateableStreamableHTTPServerTransport({
|
|
16943
17098
|
sessionIdGenerator,
|
|
16944
17099
|
onsessionclosed: () => {
|
|
16945
17100
|
},
|
|
@@ -17018,6 +17173,21 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
17018
17173
|
};
|
|
17019
17174
|
});
|
|
17020
17175
|
}
|
|
17176
|
+
/**
|
|
17177
|
+
* Marks this transport as pre-initialized for session recreation.
|
|
17178
|
+
* This is needed when recreating a transport from Redis because the
|
|
17179
|
+
* original initialize request was processed by a different transport instance.
|
|
17180
|
+
*
|
|
17181
|
+
* Uses the RecreateableStreamableHTTPServerTransport's public API to set
|
|
17182
|
+
* initialization state, avoiding access to private properties.
|
|
17183
|
+
*/
|
|
17184
|
+
markAsInitialized() {
|
|
17185
|
+
this.transport.setInitializationState(this.key.sessionId);
|
|
17186
|
+
this.logger.info("[StreamableHttpAdapter] Marked transport as pre-initialized for session recreation", {
|
|
17187
|
+
sessionId: this.key.sessionId?.slice(0, 20),
|
|
17188
|
+
isInitialized: this.transport.isInitialized
|
|
17189
|
+
});
|
|
17190
|
+
}
|
|
17021
17191
|
};
|
|
17022
17192
|
}
|
|
17023
17193
|
});
|
|
@@ -17076,6 +17246,14 @@ var init_transport_local = __esm({
|
|
|
17076
17246
|
res.status(500).json(rpcError("Internal error"));
|
|
17077
17247
|
}
|
|
17078
17248
|
}
|
|
17249
|
+
/**
|
|
17250
|
+
* Marks this transport as pre-initialized for session recreation.
|
|
17251
|
+
* This is needed when recreating a transport from Redis because the
|
|
17252
|
+
* original initialize request was processed by a different transport instance.
|
|
17253
|
+
*/
|
|
17254
|
+
markAsInitialized() {
|
|
17255
|
+
this.adapter.markAsInitialized();
|
|
17256
|
+
}
|
|
17079
17257
|
async destroy(reason) {
|
|
17080
17258
|
try {
|
|
17081
17259
|
await this.adapter.destroy(reason);
|
|
@@ -17213,14 +17391,27 @@ var init_handle_streamable_http_flow = __esm({
|
|
|
17213
17391
|
const logger = this.scopeLogger.child("handle:streamable-http:onMessage");
|
|
17214
17392
|
const { request, response } = this.rawInput;
|
|
17215
17393
|
const { token, session } = this.state.required;
|
|
17394
|
+
logger.info("onMessage: starting", {
|
|
17395
|
+
sessionId: session.id?.slice(0, 20),
|
|
17396
|
+
hasToken: !!token
|
|
17397
|
+
});
|
|
17216
17398
|
let transport = await transportService.getTransporter("streamable-http", token, session.id);
|
|
17399
|
+
logger.info("onMessage: getTransporter result", { found: !!transport });
|
|
17217
17400
|
if (!transport) {
|
|
17218
17401
|
try {
|
|
17402
|
+
logger.info("onMessage: transport not in memory, checking Redis", {
|
|
17403
|
+
sessionId: session.id?.slice(0, 20)
|
|
17404
|
+
});
|
|
17219
17405
|
const storedSession = await transportService.getStoredSession("streamable-http", token, session.id);
|
|
17406
|
+
logger.info("onMessage: getStoredSession result", {
|
|
17407
|
+
found: !!storedSession,
|
|
17408
|
+
initialized: storedSession?.initialized
|
|
17409
|
+
});
|
|
17220
17410
|
if (storedSession) {
|
|
17221
17411
|
logger.info("Recreating transport from Redis session", {
|
|
17222
17412
|
sessionId: session.id?.slice(0, 20),
|
|
17223
|
-
createdAt: storedSession.createdAt
|
|
17413
|
+
createdAt: storedSession.createdAt,
|
|
17414
|
+
initialized: storedSession.initialized
|
|
17224
17415
|
});
|
|
17225
17416
|
transport = await transportService.recreateTransporter(
|
|
17226
17417
|
"streamable-http",
|
|
@@ -17229,6 +17420,7 @@ var init_handle_streamable_http_flow = __esm({
|
|
|
17229
17420
|
storedSession,
|
|
17230
17421
|
response
|
|
17231
17422
|
);
|
|
17423
|
+
logger.info("onMessage: transport recreated successfully");
|
|
17232
17424
|
}
|
|
17233
17425
|
} catch (error) {
|
|
17234
17426
|
logger.warn("Failed to recreate transport from stored session", {
|
|
@@ -17625,6 +17817,210 @@ var init_store_helpers = __esm({
|
|
|
17625
17817
|
}
|
|
17626
17818
|
});
|
|
17627
17819
|
|
|
17820
|
+
// libs/sdk/src/auth/session/session-crypto.ts
|
|
17821
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
17822
|
+
function getSigningSecret(config) {
|
|
17823
|
+
const secret = config?.secret || process.env["MCP_SESSION_SECRET"];
|
|
17824
|
+
if (!secret) {
|
|
17825
|
+
if (process.env["NODE_ENV"] === "production") {
|
|
17826
|
+
throw new Error(
|
|
17827
|
+
"[SessionCrypto] MCP_SESSION_SECRET is required in production for session signing. Set this environment variable to a secure random string."
|
|
17828
|
+
);
|
|
17829
|
+
}
|
|
17830
|
+
console.warn("[SessionCrypto] MCP_SESSION_SECRET not set. Using insecure default for development only.");
|
|
17831
|
+
return "insecure-dev-secret-do-not-use-in-production";
|
|
17832
|
+
}
|
|
17833
|
+
return secret;
|
|
17834
|
+
}
|
|
17835
|
+
function computeSignature(data, secret) {
|
|
17836
|
+
return createHmac("sha256", secret).update(data, "utf8").digest("base64url");
|
|
17837
|
+
}
|
|
17838
|
+
function signSession(session, config) {
|
|
17839
|
+
const secret = getSigningSecret(config);
|
|
17840
|
+
const data = JSON.stringify(session);
|
|
17841
|
+
const sig = computeSignature(data, secret);
|
|
17842
|
+
const signed = {
|
|
17843
|
+
data: session,
|
|
17844
|
+
sig,
|
|
17845
|
+
v: 1
|
|
17846
|
+
};
|
|
17847
|
+
return JSON.stringify(signed);
|
|
17848
|
+
}
|
|
17849
|
+
function verifySession(signedData, config) {
|
|
17850
|
+
try {
|
|
17851
|
+
const secret = getSigningSecret(config);
|
|
17852
|
+
const parsed = JSON.parse(signedData);
|
|
17853
|
+
if (!parsed || typeof parsed !== "object" || !("sig" in parsed)) {
|
|
17854
|
+
return null;
|
|
17855
|
+
}
|
|
17856
|
+
const signed = parsed;
|
|
17857
|
+
if (signed.v !== 1) {
|
|
17858
|
+
console.warn("[SessionCrypto] Unknown signature version:", signed.v);
|
|
17859
|
+
return null;
|
|
17860
|
+
}
|
|
17861
|
+
const data = JSON.stringify(signed.data);
|
|
17862
|
+
const expectedSig = computeSignature(data, secret);
|
|
17863
|
+
const sigBuffer = Buffer.from(signed.sig, "base64url");
|
|
17864
|
+
const expectedBuffer = Buffer.from(expectedSig, "base64url");
|
|
17865
|
+
if (sigBuffer.length !== expectedBuffer.length) {
|
|
17866
|
+
console.warn("[SessionCrypto] Signature length mismatch - possible tampering");
|
|
17867
|
+
return null;
|
|
17868
|
+
}
|
|
17869
|
+
if (!timingSafeEqual(sigBuffer, expectedBuffer)) {
|
|
17870
|
+
console.warn("[SessionCrypto] HMAC verification failed - session data may be tampered");
|
|
17871
|
+
return null;
|
|
17872
|
+
}
|
|
17873
|
+
return signed.data;
|
|
17874
|
+
} catch (error) {
|
|
17875
|
+
console.warn("[SessionCrypto] Failed to verify session:", error.message);
|
|
17876
|
+
return null;
|
|
17877
|
+
}
|
|
17878
|
+
}
|
|
17879
|
+
function isSignedSession(data) {
|
|
17880
|
+
try {
|
|
17881
|
+
const parsed = JSON.parse(data);
|
|
17882
|
+
return parsed && typeof parsed === "object" && "sig" in parsed && "v" in parsed;
|
|
17883
|
+
} catch {
|
|
17884
|
+
return false;
|
|
17885
|
+
}
|
|
17886
|
+
}
|
|
17887
|
+
function verifyOrParseSession(data, config) {
|
|
17888
|
+
if (isSignedSession(data)) {
|
|
17889
|
+
return verifySession(data, config);
|
|
17890
|
+
}
|
|
17891
|
+
try {
|
|
17892
|
+
return JSON.parse(data);
|
|
17893
|
+
} catch {
|
|
17894
|
+
return null;
|
|
17895
|
+
}
|
|
17896
|
+
}
|
|
17897
|
+
var init_session_crypto = __esm({
|
|
17898
|
+
"libs/sdk/src/auth/session/session-crypto.ts"() {
|
|
17899
|
+
"use strict";
|
|
17900
|
+
}
|
|
17901
|
+
});
|
|
17902
|
+
|
|
17903
|
+
// libs/sdk/src/auth/session/session-rate-limiter.ts
|
|
17904
|
+
var SessionRateLimiter, defaultSessionRateLimiter;
|
|
17905
|
+
var init_session_rate_limiter = __esm({
|
|
17906
|
+
"libs/sdk/src/auth/session/session-rate-limiter.ts"() {
|
|
17907
|
+
"use strict";
|
|
17908
|
+
SessionRateLimiter = class {
|
|
17909
|
+
windowMs;
|
|
17910
|
+
maxRequests;
|
|
17911
|
+
requests = /* @__PURE__ */ new Map();
|
|
17912
|
+
cleanupTimer = null;
|
|
17913
|
+
constructor(config = {}) {
|
|
17914
|
+
this.windowMs = config.windowMs ?? 6e4;
|
|
17915
|
+
this.maxRequests = config.maxRequests ?? 100;
|
|
17916
|
+
const cleanupIntervalMs = config.cleanupIntervalMs ?? 6e4;
|
|
17917
|
+
if (cleanupIntervalMs > 0) {
|
|
17918
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), cleanupIntervalMs);
|
|
17919
|
+
this.cleanupTimer.unref();
|
|
17920
|
+
}
|
|
17921
|
+
}
|
|
17922
|
+
/**
|
|
17923
|
+
* Check if a request is allowed for the given key.
|
|
17924
|
+
*
|
|
17925
|
+
* @param key - Identifier for rate limiting (e.g., client IP, session ID prefix)
|
|
17926
|
+
* @returns Rate limit result with allowed status and metadata
|
|
17927
|
+
*/
|
|
17928
|
+
check(key) {
|
|
17929
|
+
const now = Date.now();
|
|
17930
|
+
const windowStart = now - this.windowMs;
|
|
17931
|
+
let timestamps = this.requests.get(key);
|
|
17932
|
+
if (!timestamps) {
|
|
17933
|
+
timestamps = [];
|
|
17934
|
+
this.requests.set(key, timestamps);
|
|
17935
|
+
}
|
|
17936
|
+
const validTimestamps = timestamps.filter((t) => t > windowStart);
|
|
17937
|
+
const oldestInWindow = validTimestamps[0] ?? now;
|
|
17938
|
+
const resetAt = oldestInWindow + this.windowMs;
|
|
17939
|
+
if (validTimestamps.length >= this.maxRequests) {
|
|
17940
|
+
return {
|
|
17941
|
+
allowed: false,
|
|
17942
|
+
remaining: 0,
|
|
17943
|
+
resetAt,
|
|
17944
|
+
retryAfterMs: resetAt - now
|
|
17945
|
+
};
|
|
17946
|
+
}
|
|
17947
|
+
validTimestamps.push(now);
|
|
17948
|
+
this.requests.set(key, validTimestamps);
|
|
17949
|
+
return {
|
|
17950
|
+
allowed: true,
|
|
17951
|
+
remaining: this.maxRequests - validTimestamps.length,
|
|
17952
|
+
resetAt
|
|
17953
|
+
};
|
|
17954
|
+
}
|
|
17955
|
+
/**
|
|
17956
|
+
* Check if a request would be allowed without recording it.
|
|
17957
|
+
* Useful for pre-checking without consuming quota.
|
|
17958
|
+
*
|
|
17959
|
+
* @param key - Identifier for rate limiting
|
|
17960
|
+
* @returns true if request would be allowed
|
|
17961
|
+
*/
|
|
17962
|
+
wouldAllow(key) {
|
|
17963
|
+
const now = Date.now();
|
|
17964
|
+
const windowStart = now - this.windowMs;
|
|
17965
|
+
const timestamps = this.requests.get(key);
|
|
17966
|
+
if (!timestamps) return true;
|
|
17967
|
+
const validCount = timestamps.filter((t) => t > windowStart).length;
|
|
17968
|
+
return validCount < this.maxRequests;
|
|
17969
|
+
}
|
|
17970
|
+
/**
|
|
17971
|
+
* Reset rate limit for a specific key.
|
|
17972
|
+
* Useful for testing or after successful authentication.
|
|
17973
|
+
*
|
|
17974
|
+
* @param key - Identifier to reset
|
|
17975
|
+
*/
|
|
17976
|
+
reset(key) {
|
|
17977
|
+
this.requests.delete(key);
|
|
17978
|
+
}
|
|
17979
|
+
/**
|
|
17980
|
+
* Clean up expired entries from all keys.
|
|
17981
|
+
* Called automatically on configured interval, but can be called manually.
|
|
17982
|
+
*/
|
|
17983
|
+
cleanup() {
|
|
17984
|
+
const now = Date.now();
|
|
17985
|
+
const windowStart = now - this.windowMs;
|
|
17986
|
+
for (const [key, timestamps] of this.requests.entries()) {
|
|
17987
|
+
const valid = timestamps.filter((t) => t > windowStart);
|
|
17988
|
+
if (valid.length === 0) {
|
|
17989
|
+
this.requests.delete(key);
|
|
17990
|
+
} else if (valid.length < timestamps.length) {
|
|
17991
|
+
this.requests.set(key, valid);
|
|
17992
|
+
}
|
|
17993
|
+
}
|
|
17994
|
+
}
|
|
17995
|
+
/**
|
|
17996
|
+
* Get current statistics for monitoring.
|
|
17997
|
+
*/
|
|
17998
|
+
getStats() {
|
|
17999
|
+
let totalRequests = 0;
|
|
18000
|
+
for (const timestamps of this.requests.values()) {
|
|
18001
|
+
totalRequests += timestamps.length;
|
|
18002
|
+
}
|
|
18003
|
+
return {
|
|
18004
|
+
totalKeys: this.requests.size,
|
|
18005
|
+
totalRequests
|
|
18006
|
+
};
|
|
18007
|
+
}
|
|
18008
|
+
/**
|
|
18009
|
+
* Stop the automatic cleanup timer.
|
|
18010
|
+
* Call this when disposing of the rate limiter.
|
|
18011
|
+
*/
|
|
18012
|
+
dispose() {
|
|
18013
|
+
if (this.cleanupTimer) {
|
|
18014
|
+
clearInterval(this.cleanupTimer);
|
|
18015
|
+
this.cleanupTimer = null;
|
|
18016
|
+
}
|
|
18017
|
+
this.requests.clear();
|
|
18018
|
+
}
|
|
18019
|
+
};
|
|
18020
|
+
defaultSessionRateLimiter = new SessionRateLimiter();
|
|
18021
|
+
}
|
|
18022
|
+
});
|
|
18023
|
+
|
|
17628
18024
|
// libs/sdk/src/auth/session/redis-session.store.ts
|
|
17629
18025
|
var redis_session_store_exports = {};
|
|
17630
18026
|
__export(redis_session_store_exports, {
|
|
@@ -17637,15 +18033,27 @@ var init_redis_session_store = __esm({
|
|
|
17637
18033
|
"libs/sdk/src/auth/session/redis-session.store.ts"() {
|
|
17638
18034
|
"use strict";
|
|
17639
18035
|
init_transport_session_types();
|
|
18036
|
+
init_session_crypto();
|
|
18037
|
+
init_session_rate_limiter();
|
|
17640
18038
|
RedisSessionStore = class {
|
|
17641
18039
|
redis;
|
|
17642
18040
|
keyPrefix;
|
|
17643
18041
|
defaultTtlMs;
|
|
17644
18042
|
logger;
|
|
17645
18043
|
externalInstance = false;
|
|
18044
|
+
// Security features
|
|
18045
|
+
security;
|
|
18046
|
+
rateLimiter;
|
|
17646
18047
|
constructor(config, logger) {
|
|
17647
18048
|
this.defaultTtlMs = ("defaultTtlMs" in config ? config.defaultTtlMs : void 0) ?? 36e5;
|
|
17648
18049
|
this.logger = logger;
|
|
18050
|
+
this.security = ("security" in config ? config.security : void 0) ?? {};
|
|
18051
|
+
if (this.security.enableRateLimiting) {
|
|
18052
|
+
this.rateLimiter = new SessionRateLimiter({
|
|
18053
|
+
windowMs: this.security.rateLimiting?.windowMs,
|
|
18054
|
+
maxRequests: this.security.rateLimiting?.maxRequests
|
|
18055
|
+
});
|
|
18056
|
+
}
|
|
17649
18057
|
if ("redis" in config && config.redis) {
|
|
17650
18058
|
this.redis = config.redis;
|
|
17651
18059
|
this.keyPrefix = config.keyPrefix ?? "mcp:session:";
|
|
@@ -17680,8 +18088,26 @@ var init_redis_session_store = __esm({
|
|
|
17680
18088
|
*
|
|
17681
18089
|
* Note: Uses atomic GETEX to extend TTL while reading, preventing race conditions
|
|
17682
18090
|
* where concurrent readers might resurrect expired sessions.
|
|
17683
|
-
|
|
17684
|
-
|
|
18091
|
+
*
|
|
18092
|
+
* @param sessionId - The session ID to look up
|
|
18093
|
+
* @param options - Optional parameters for rate limiting
|
|
18094
|
+
* @param options.clientIdentifier - Client identifier (e.g., IP address) for rate limiting.
|
|
18095
|
+
* When provided, rate limiting is applied per-client to prevent session enumeration.
|
|
18096
|
+
* If not provided, falls back to sessionId which provides DoS protection per-session.
|
|
18097
|
+
*/
|
|
18098
|
+
async get(sessionId, options) {
|
|
18099
|
+
if (this.rateLimiter) {
|
|
18100
|
+
const rateLimitKey = options?.clientIdentifier || sessionId;
|
|
18101
|
+
const rateLimitResult = this.rateLimiter.check(rateLimitKey);
|
|
18102
|
+
if (!rateLimitResult.allowed) {
|
|
18103
|
+
this.logger?.warn("[RedisSessionStore] Rate limit exceeded for session lookup", {
|
|
18104
|
+
sessionId: sessionId.slice(0, 20),
|
|
18105
|
+
clientIdentifier: options?.clientIdentifier ? options.clientIdentifier.slice(0, 20) : void 0,
|
|
18106
|
+
retryAfterMs: rateLimitResult.retryAfterMs
|
|
18107
|
+
});
|
|
18108
|
+
return null;
|
|
18109
|
+
}
|
|
18110
|
+
}
|
|
17685
18111
|
const key = this.key(sessionId);
|
|
17686
18112
|
let raw;
|
|
17687
18113
|
try {
|
|
@@ -17691,7 +18117,19 @@ var init_redis_session_store = __esm({
|
|
|
17691
18117
|
}
|
|
17692
18118
|
if (!raw) return null;
|
|
17693
18119
|
try {
|
|
17694
|
-
|
|
18120
|
+
let parsed;
|
|
18121
|
+
if (this.security.enableSigning) {
|
|
18122
|
+
parsed = verifyOrParseSession(raw, { secret: this.security.signingSecret });
|
|
18123
|
+
if (!parsed) {
|
|
18124
|
+
this.logger?.warn("[RedisSessionStore] Session signature verification failed", {
|
|
18125
|
+
sessionId: sessionId.slice(0, 20)
|
|
18126
|
+
});
|
|
18127
|
+
this.delete(sessionId).catch(() => void 0);
|
|
18128
|
+
return null;
|
|
18129
|
+
}
|
|
18130
|
+
} else {
|
|
18131
|
+
parsed = JSON.parse(raw);
|
|
18132
|
+
}
|
|
17695
18133
|
const result = storedSessionSchema.safeParse(parsed);
|
|
17696
18134
|
if (!result.success) {
|
|
17697
18135
|
this.logger?.warn("[RedisSessionStore] Invalid session format", {
|
|
@@ -17702,6 +18140,14 @@ var init_redis_session_store = __esm({
|
|
|
17702
18140
|
return null;
|
|
17703
18141
|
}
|
|
17704
18142
|
const session = result.data;
|
|
18143
|
+
if (session.maxLifetimeAt && session.maxLifetimeAt < Date.now()) {
|
|
18144
|
+
this.logger?.info("[RedisSessionStore] Session exceeded max lifetime", {
|
|
18145
|
+
sessionId: sessionId.slice(0, 20),
|
|
18146
|
+
maxLifetimeAt: session.maxLifetimeAt
|
|
18147
|
+
});
|
|
18148
|
+
await this.delete(sessionId);
|
|
18149
|
+
return null;
|
|
18150
|
+
}
|
|
17705
18151
|
if (session.session.expiresAt && session.session.expiresAt < Date.now()) {
|
|
17706
18152
|
await this.delete(sessionId);
|
|
17707
18153
|
return null;
|
|
@@ -17709,7 +18155,12 @@ var init_redis_session_store = __esm({
|
|
|
17709
18155
|
if (session.session.expiresAt) {
|
|
17710
18156
|
const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());
|
|
17711
18157
|
if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {
|
|
17712
|
-
this.redis.pexpire(key, ttlMs).catch(() =>
|
|
18158
|
+
this.redis.pexpire(key, ttlMs).catch((err) => {
|
|
18159
|
+
this.logger?.warn("[RedisSessionStore] TTL extension failed", {
|
|
18160
|
+
sessionId: sessionId.slice(0, 20),
|
|
18161
|
+
error: err.message
|
|
18162
|
+
});
|
|
18163
|
+
});
|
|
17713
18164
|
}
|
|
17714
18165
|
}
|
|
17715
18166
|
const updatedSession = {
|
|
@@ -17731,7 +18182,12 @@ var init_redis_session_store = __esm({
|
|
|
17731
18182
|
*/
|
|
17732
18183
|
async set(sessionId, session, ttlMs) {
|
|
17733
18184
|
const key = this.key(sessionId);
|
|
17734
|
-
|
|
18185
|
+
let value;
|
|
18186
|
+
if (this.security.enableSigning) {
|
|
18187
|
+
value = signSession(session, { secret: this.security.signingSecret });
|
|
18188
|
+
} else {
|
|
18189
|
+
value = JSON.stringify(session);
|
|
18190
|
+
}
|
|
17735
18191
|
if (ttlMs && ttlMs > 0) {
|
|
17736
18192
|
await this.redis.set(key, value, "PX", ttlMs);
|
|
17737
18193
|
} else if (session.session.expiresAt) {
|
|
@@ -17806,6 +18262,8 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17806
18262
|
"libs/sdk/src/auth/session/vercel-kv-session.store.ts"() {
|
|
17807
18263
|
"use strict";
|
|
17808
18264
|
init_transport_session_types();
|
|
18265
|
+
init_session_crypto();
|
|
18266
|
+
init_session_rate_limiter();
|
|
17809
18267
|
VercelKvSessionStore = class {
|
|
17810
18268
|
kv = null;
|
|
17811
18269
|
connectPromise = null;
|
|
@@ -17813,11 +18271,21 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17813
18271
|
defaultTtlMs;
|
|
17814
18272
|
logger;
|
|
17815
18273
|
config;
|
|
18274
|
+
// Security features
|
|
18275
|
+
security;
|
|
18276
|
+
rateLimiter;
|
|
17816
18277
|
constructor(config, logger) {
|
|
17817
18278
|
this.config = config;
|
|
17818
18279
|
this.keyPrefix = config.keyPrefix ?? "mcp:session:";
|
|
17819
18280
|
this.defaultTtlMs = config.defaultTtlMs ?? 36e5;
|
|
17820
18281
|
this.logger = logger;
|
|
18282
|
+
this.security = ("security" in config ? config.security : void 0) ?? {};
|
|
18283
|
+
if (this.security.enableRateLimiting) {
|
|
18284
|
+
this.rateLimiter = new SessionRateLimiter({
|
|
18285
|
+
windowMs: this.security.rateLimiting?.windowMs,
|
|
18286
|
+
maxRequests: this.security.rateLimiting?.maxRequests
|
|
18287
|
+
});
|
|
18288
|
+
}
|
|
17821
18289
|
}
|
|
17822
18290
|
/**
|
|
17823
18291
|
* Connect to Vercel KV
|
|
@@ -17870,15 +18338,51 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17870
18338
|
*
|
|
17871
18339
|
* Note: Vercel KV doesn't support GETEX, so we use GET + PEXPIRE separately.
|
|
17872
18340
|
* This is slightly less atomic than Redis GETEX but sufficient for most use cases.
|
|
17873
|
-
|
|
17874
|
-
|
|
18341
|
+
*
|
|
18342
|
+
* @param sessionId - The session ID to look up
|
|
18343
|
+
* @param options - Optional parameters for rate limiting
|
|
18344
|
+
* @param options.clientIdentifier - Client identifier (e.g., IP address) for rate limiting.
|
|
18345
|
+
* When provided, rate limiting is applied per-client to prevent session enumeration.
|
|
18346
|
+
* If not provided, falls back to sessionId which provides DoS protection per-session.
|
|
18347
|
+
*/
|
|
18348
|
+
async get(sessionId, options) {
|
|
18349
|
+
if (this.rateLimiter) {
|
|
18350
|
+
const rateLimitKey = options?.clientIdentifier || sessionId;
|
|
18351
|
+
const rateLimitResult = this.rateLimiter.check(rateLimitKey);
|
|
18352
|
+
if (!rateLimitResult.allowed) {
|
|
18353
|
+
this.logger?.warn("[VercelKvSessionStore] Rate limit exceeded for session lookup", {
|
|
18354
|
+
sessionId: sessionId.slice(0, 20),
|
|
18355
|
+
clientIdentifier: options?.clientIdentifier ? options.clientIdentifier.slice(0, 20) : void 0,
|
|
18356
|
+
retryAfterMs: rateLimitResult.retryAfterMs
|
|
18357
|
+
});
|
|
18358
|
+
return null;
|
|
18359
|
+
}
|
|
18360
|
+
}
|
|
17875
18361
|
const kv = await this.ensureConnected();
|
|
17876
18362
|
const key = this.key(sessionId);
|
|
17877
18363
|
const raw = await kv.get(key);
|
|
17878
18364
|
if (!raw) return null;
|
|
17879
|
-
kv.pexpire(key, this.defaultTtlMs).catch(() =>
|
|
18365
|
+
kv.pexpire(key, this.defaultTtlMs).catch((err) => {
|
|
18366
|
+
this.logger?.warn("[VercelKvSessionStore] TTL extension failed", {
|
|
18367
|
+
sessionId: sessionId.slice(0, 20),
|
|
18368
|
+
error: err.message
|
|
18369
|
+
});
|
|
18370
|
+
});
|
|
17880
18371
|
try {
|
|
17881
|
-
|
|
18372
|
+
let parsed;
|
|
18373
|
+
const rawStr = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
18374
|
+
if (this.security.enableSigning) {
|
|
18375
|
+
parsed = verifyOrParseSession(rawStr, { secret: this.security.signingSecret });
|
|
18376
|
+
if (!parsed) {
|
|
18377
|
+
this.logger?.warn("[VercelKvSessionStore] Session signature verification failed", {
|
|
18378
|
+
sessionId: sessionId.slice(0, 20)
|
|
18379
|
+
});
|
|
18380
|
+
this.delete(sessionId).catch(() => void 0);
|
|
18381
|
+
return null;
|
|
18382
|
+
}
|
|
18383
|
+
} else {
|
|
18384
|
+
parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
18385
|
+
}
|
|
17882
18386
|
const result = storedSessionSchema.safeParse(parsed);
|
|
17883
18387
|
if (!result.success) {
|
|
17884
18388
|
this.logger?.warn("[VercelKvSessionStore] Invalid session format", {
|
|
@@ -17889,6 +18393,14 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17889
18393
|
return null;
|
|
17890
18394
|
}
|
|
17891
18395
|
const session = result.data;
|
|
18396
|
+
if (session.maxLifetimeAt && session.maxLifetimeAt < Date.now()) {
|
|
18397
|
+
this.logger?.info("[VercelKvSessionStore] Session exceeded max lifetime", {
|
|
18398
|
+
sessionId: sessionId.slice(0, 20),
|
|
18399
|
+
maxLifetimeAt: session.maxLifetimeAt
|
|
18400
|
+
});
|
|
18401
|
+
await this.delete(sessionId);
|
|
18402
|
+
return null;
|
|
18403
|
+
}
|
|
17892
18404
|
if (session.session.expiresAt && session.session.expiresAt < Date.now()) {
|
|
17893
18405
|
await this.delete(sessionId);
|
|
17894
18406
|
return null;
|
|
@@ -17896,7 +18408,12 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17896
18408
|
if (session.session.expiresAt) {
|
|
17897
18409
|
const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());
|
|
17898
18410
|
if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {
|
|
17899
|
-
kv.pexpire(key, ttlMs).catch(() =>
|
|
18411
|
+
kv.pexpire(key, ttlMs).catch((err) => {
|
|
18412
|
+
this.logger?.warn("[VercelKvSessionStore] TTL bound extension failed", {
|
|
18413
|
+
sessionId: sessionId.slice(0, 20),
|
|
18414
|
+
error: err.message
|
|
18415
|
+
});
|
|
18416
|
+
});
|
|
17900
18417
|
}
|
|
17901
18418
|
}
|
|
17902
18419
|
const updatedSession = {
|
|
@@ -17919,7 +18436,12 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17919
18436
|
async set(sessionId, session, ttlMs) {
|
|
17920
18437
|
const kv = await this.ensureConnected();
|
|
17921
18438
|
const key = this.key(sessionId);
|
|
17922
|
-
|
|
18439
|
+
let value;
|
|
18440
|
+
if (this.security.enableSigning) {
|
|
18441
|
+
value = signSession(session, { secret: this.security.signingSecret });
|
|
18442
|
+
} else {
|
|
18443
|
+
value = JSON.stringify(session);
|
|
18444
|
+
}
|
|
17923
18445
|
if (ttlMs && ttlMs > 0) {
|
|
17924
18446
|
await kv.set(key, value, { px: ttlMs });
|
|
17925
18447
|
} else if (session.session.expiresAt) {
|
|
@@ -18204,9 +18726,12 @@ var init_transport_registry = __esm({
|
|
|
18204
18726
|
* @param type - Transport type
|
|
18205
18727
|
* @param token - Authorization token
|
|
18206
18728
|
* @param sessionId - Session ID
|
|
18729
|
+
* @param options - Optional validation options
|
|
18730
|
+
* @param options.clientFingerprint - Client fingerprint for additional validation
|
|
18731
|
+
* @param options.warnOnFingerprintMismatch - If true, log warning on mismatch but still return session
|
|
18207
18732
|
* @returns Stored session data if exists and token matches, undefined otherwise
|
|
18208
18733
|
*/
|
|
18209
|
-
async getStoredSession(type, token, sessionId) {
|
|
18734
|
+
async getStoredSession(type, token, sessionId, options) {
|
|
18210
18735
|
if (!this.sessionStore || type !== "streamable-http") return void 0;
|
|
18211
18736
|
const tokenHash = this.sha256(token);
|
|
18212
18737
|
const stored = await this.sessionStore.get(sessionId);
|
|
@@ -18219,6 +18744,18 @@ var init_transport_registry = __esm({
|
|
|
18219
18744
|
});
|
|
18220
18745
|
return void 0;
|
|
18221
18746
|
}
|
|
18747
|
+
if (options?.clientFingerprint && stored.session.clientFingerprint) {
|
|
18748
|
+
if (stored.session.clientFingerprint !== options.clientFingerprint) {
|
|
18749
|
+
this.scope.logger.warn("[TransportService] Client fingerprint mismatch", {
|
|
18750
|
+
sessionId: sessionId.slice(0, 20),
|
|
18751
|
+
storedFingerprint: stored.session.clientFingerprint.slice(0, 8),
|
|
18752
|
+
requestFingerprint: options.clientFingerprint.slice(0, 8)
|
|
18753
|
+
});
|
|
18754
|
+
if (!options.warnOnFingerprintMismatch) {
|
|
18755
|
+
return void 0;
|
|
18756
|
+
}
|
|
18757
|
+
}
|
|
18758
|
+
}
|
|
18222
18759
|
return stored;
|
|
18223
18760
|
}
|
|
18224
18761
|
/**
|
|
@@ -18281,6 +18818,9 @@ var init_transport_registry = __esm({
|
|
|
18281
18818
|
}
|
|
18282
18819
|
});
|
|
18283
18820
|
await transporter.ready();
|
|
18821
|
+
if (storedSession.initialized !== false) {
|
|
18822
|
+
transporter.markAsInitialized();
|
|
18823
|
+
}
|
|
18284
18824
|
this.insertLocal(key, transporter);
|
|
18285
18825
|
if (sessionStore) {
|
|
18286
18826
|
const updatedSession = {
|
|
@@ -18347,7 +18887,9 @@ var init_transport_registry = __esm({
|
|
|
18347
18887
|
},
|
|
18348
18888
|
authorizationId: key.tokenHash,
|
|
18349
18889
|
createdAt: Date.now(),
|
|
18350
|
-
lastAccessedAt: Date.now()
|
|
18890
|
+
lastAccessedAt: Date.now(),
|
|
18891
|
+
initialized: true
|
|
18892
|
+
// Mark as initialized for session recreation
|
|
18351
18893
|
};
|
|
18352
18894
|
sessionStore.set(sessionId, storedSession, persistenceConfig?.defaultTtlMs).catch((err) => {
|
|
18353
18895
|
this.scope.logger.warn("[TransportService] Failed to persist session to Redis", {
|
|
@@ -19936,6 +20478,54 @@ var init_front_mcp2 = __esm({
|
|
|
19936
20478
|
}
|
|
19937
20479
|
});
|
|
19938
20480
|
|
|
20481
|
+
// libs/sdk/src/front-mcp/serverless-handler.ts
|
|
20482
|
+
var serverless_handler_exports = {};
|
|
20483
|
+
__export(serverless_handler_exports, {
|
|
20484
|
+
getServerlessHandler: () => getServerlessHandler,
|
|
20485
|
+
getServerlessHandlerAsync: () => getServerlessHandlerAsync,
|
|
20486
|
+
setServerlessHandler: () => setServerlessHandler,
|
|
20487
|
+
setServerlessHandlerError: () => setServerlessHandlerError,
|
|
20488
|
+
setServerlessHandlerPromise: () => setServerlessHandlerPromise
|
|
20489
|
+
});
|
|
20490
|
+
function setServerlessHandler(handler) {
|
|
20491
|
+
globalHandler = handler;
|
|
20492
|
+
}
|
|
20493
|
+
function setServerlessHandlerPromise(promise) {
|
|
20494
|
+
globalHandlerPromise = promise;
|
|
20495
|
+
}
|
|
20496
|
+
function setServerlessHandlerError(error) {
|
|
20497
|
+
globalHandlerError = error;
|
|
20498
|
+
}
|
|
20499
|
+
function getServerlessHandler() {
|
|
20500
|
+
if (globalHandlerError) {
|
|
20501
|
+
throw globalHandlerError;
|
|
20502
|
+
}
|
|
20503
|
+
return globalHandler;
|
|
20504
|
+
}
|
|
20505
|
+
async function getServerlessHandlerAsync() {
|
|
20506
|
+
if (globalHandlerError) {
|
|
20507
|
+
throw globalHandlerError;
|
|
20508
|
+
}
|
|
20509
|
+
if (globalHandlerPromise) {
|
|
20510
|
+
return globalHandlerPromise;
|
|
20511
|
+
}
|
|
20512
|
+
if (!globalHandler) {
|
|
20513
|
+
throw new Error(
|
|
20514
|
+
"Serverless handler not initialized. Ensure @FrontMcp decorator ran and FRONTMCP_SERVERLESS=1 is set."
|
|
20515
|
+
);
|
|
20516
|
+
}
|
|
20517
|
+
return globalHandler;
|
|
20518
|
+
}
|
|
20519
|
+
var globalHandler, globalHandlerPromise, globalHandlerError;
|
|
20520
|
+
var init_serverless_handler = __esm({
|
|
20521
|
+
"libs/sdk/src/front-mcp/serverless-handler.ts"() {
|
|
20522
|
+
"use strict";
|
|
20523
|
+
globalHandler = null;
|
|
20524
|
+
globalHandlerPromise = null;
|
|
20525
|
+
globalHandlerError = null;
|
|
20526
|
+
}
|
|
20527
|
+
});
|
|
20528
|
+
|
|
19939
20529
|
// libs/sdk/src/common/decorators/front-mcp.decorator.ts
|
|
19940
20530
|
import "reflect-metadata";
|
|
19941
20531
|
function getFrontMcpInstance() {
|
|
@@ -19947,6 +20537,15 @@ function getFrontMcpInstance() {
|
|
|
19947
20537
|
}
|
|
19948
20538
|
return _FrontMcpInstance;
|
|
19949
20539
|
}
|
|
20540
|
+
function getServerlessHandlerFns() {
|
|
20541
|
+
if (!_serverlessHandlerFns) {
|
|
20542
|
+
_serverlessHandlerFns = (init_serverless_handler(), __toCommonJS(serverless_handler_exports));
|
|
20543
|
+
}
|
|
20544
|
+
if (!_serverlessHandlerFns) {
|
|
20545
|
+
throw new InternalMcpError("Serverless handler functions not found", "MODULE_LOAD_FAILED");
|
|
20546
|
+
}
|
|
20547
|
+
return _serverlessHandlerFns;
|
|
20548
|
+
}
|
|
19950
20549
|
function FrontMcp(providedMetadata) {
|
|
19951
20550
|
return (target) => {
|
|
19952
20551
|
const migratedMetadata = applyMigration(providedMetadata);
|
|
@@ -19987,18 +20586,8 @@ ${JSON.stringify(
|
|
|
19987
20586
|
}
|
|
19988
20587
|
const isServerless = typeof process !== "undefined" && process.env?.["FRONTMCP_SERVERLESS"] === "1";
|
|
19989
20588
|
if (isServerless) {
|
|
19990
|
-
const
|
|
19991
|
-
|
|
19992
|
-
setServerlessHandler: setServerlessHandler2,
|
|
19993
|
-
setServerlessHandlerPromise: setServerlessHandlerPromise2,
|
|
19994
|
-
setServerlessHandlerError: setServerlessHandlerError2
|
|
19995
|
-
} = __require("@frontmcp/sdk");
|
|
19996
|
-
if (!ServerlessInstance) {
|
|
19997
|
-
throw new InternalMcpError(
|
|
19998
|
-
"@frontmcp/sdk version mismatch, make sure you have the same version for all @frontmcp/* packages",
|
|
19999
|
-
"SDK_VERSION_MISMATCH"
|
|
20000
|
-
);
|
|
20001
|
-
}
|
|
20589
|
+
const ServerlessInstance = getFrontMcpInstance();
|
|
20590
|
+
const { setServerlessHandler: setServerlessHandler2, setServerlessHandlerPromise: setServerlessHandlerPromise2, setServerlessHandlerError: setServerlessHandlerError2 } = getServerlessHandlerFns();
|
|
20002
20591
|
const handlerPromise = ServerlessInstance.createHandler(metadata);
|
|
20003
20592
|
setServerlessHandlerPromise2(handlerPromise);
|
|
20004
20593
|
handlerPromise.then(setServerlessHandler2).catch((err) => {
|
|
@@ -20011,7 +20600,7 @@ ${JSON.stringify(
|
|
|
20011
20600
|
}
|
|
20012
20601
|
};
|
|
20013
20602
|
}
|
|
20014
|
-
var _FrontMcpInstance;
|
|
20603
|
+
var _FrontMcpInstance, _serverlessHandlerFns;
|
|
20015
20604
|
var init_front_mcp_decorator = __esm({
|
|
20016
20605
|
"libs/sdk/src/common/decorators/front-mcp.decorator.ts"() {
|
|
20017
20606
|
"use strict";
|
|
@@ -20020,6 +20609,7 @@ var init_front_mcp_decorator = __esm({
|
|
|
20020
20609
|
init_migrate();
|
|
20021
20610
|
init_mcp_error();
|
|
20022
20611
|
_FrontMcpInstance = null;
|
|
20612
|
+
_serverlessHandlerFns = null;
|
|
20023
20613
|
}
|
|
20024
20614
|
});
|
|
20025
20615
|
|
|
@@ -22326,46 +22916,11 @@ var init_common = __esm({
|
|
|
22326
22916
|
// libs/sdk/src/index.ts
|
|
22327
22917
|
init_common();
|
|
22328
22918
|
init_front_mcp2();
|
|
22329
|
-
|
|
22330
|
-
|
|
22331
|
-
// libs/sdk/src/front-mcp/serverless-handler.ts
|
|
22332
|
-
var globalHandler = null;
|
|
22333
|
-
var globalHandlerPromise = null;
|
|
22334
|
-
var globalHandlerError = null;
|
|
22335
|
-
function setServerlessHandler(handler) {
|
|
22336
|
-
globalHandler = handler;
|
|
22337
|
-
}
|
|
22338
|
-
function setServerlessHandlerPromise(promise) {
|
|
22339
|
-
globalHandlerPromise = promise;
|
|
22340
|
-
}
|
|
22341
|
-
function setServerlessHandlerError(error) {
|
|
22342
|
-
globalHandlerError = error;
|
|
22343
|
-
}
|
|
22344
|
-
function getServerlessHandler() {
|
|
22345
|
-
if (globalHandlerError) {
|
|
22346
|
-
throw globalHandlerError;
|
|
22347
|
-
}
|
|
22348
|
-
return globalHandler;
|
|
22349
|
-
}
|
|
22350
|
-
async function getServerlessHandlerAsync() {
|
|
22351
|
-
if (globalHandlerError) {
|
|
22352
|
-
throw globalHandlerError;
|
|
22353
|
-
}
|
|
22354
|
-
if (globalHandlerPromise) {
|
|
22355
|
-
return globalHandlerPromise;
|
|
22356
|
-
}
|
|
22357
|
-
if (!globalHandler) {
|
|
22358
|
-
throw new Error(
|
|
22359
|
-
"Serverless handler not initialized. Ensure @FrontMcp decorator ran and FRONTMCP_SERVERLESS=1 is set."
|
|
22360
|
-
);
|
|
22361
|
-
}
|
|
22362
|
-
return globalHandler;
|
|
22363
|
-
}
|
|
22364
|
-
|
|
22365
|
-
// libs/sdk/src/index.ts
|
|
22919
|
+
init_serverless_handler();
|
|
22366
22920
|
init_common();
|
|
22367
22921
|
init_errors();
|
|
22368
22922
|
init_context();
|
|
22923
|
+
import "reflect-metadata";
|
|
22369
22924
|
var ToolHook = FlowHooksOf("tools:call-tool");
|
|
22370
22925
|
var ListToolsHook = FlowHooksOf("tools:list-tools");
|
|
22371
22926
|
var HttpHook = FlowHooksOf("http:request");
|