@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/index.js
CHANGED
|
@@ -516,7 +516,7 @@ var init_http_options = __esm({
|
|
|
516
516
|
});
|
|
517
517
|
|
|
518
518
|
// libs/sdk/src/auth/session/transport-session.types.ts
|
|
519
|
-
var import_zod6, transportProtocolSchema, sseTransportStateSchema, streamableHttpTransportStateSchema, statefulHttpTransportStateSchema, statelessHttpTransportStateSchema, legacySseTransportStateSchema, transportStateSchema, transportSessionSchema, sessionJwtPayloadSchema,
|
|
519
|
+
var import_zod6, transportProtocolSchema, sseTransportStateSchema, streamableHttpTransportStateSchema, statefulHttpTransportStateSchema, statelessHttpTransportStateSchema, legacySseTransportStateSchema, transportStateSchema, transportSessionSchema, sessionJwtPayloadSchema, encryptedBlobSchema, storedSessionSchema, redisConfigSchema;
|
|
520
520
|
var init_transport_session_types = __esm({
|
|
521
521
|
"libs/sdk/src/auth/session/transport-session.types.ts"() {
|
|
522
522
|
"use strict";
|
|
@@ -582,10 +582,6 @@ var init_transport_session_types = __esm({
|
|
|
582
582
|
iat: import_zod6.z.number(),
|
|
583
583
|
exp: import_zod6.z.number().optional()
|
|
584
584
|
});
|
|
585
|
-
statelessSessionJwtPayloadSchema = sessionJwtPayloadSchema.extend({
|
|
586
|
-
state: import_zod6.z.string().optional(),
|
|
587
|
-
tokens: import_zod6.z.string().optional()
|
|
588
|
-
});
|
|
589
585
|
encryptedBlobSchema = import_zod6.z.object({
|
|
590
586
|
alg: import_zod6.z.literal("A256GCM"),
|
|
591
587
|
kid: import_zod6.z.string().optional(),
|
|
@@ -600,7 +596,9 @@ var init_transport_session_types = __esm({
|
|
|
600
596
|
authorizationId: import_zod6.z.string(),
|
|
601
597
|
tokens: import_zod6.z.record(import_zod6.z.string(), encryptedBlobSchema).optional(),
|
|
602
598
|
createdAt: import_zod6.z.number(),
|
|
603
|
-
lastAccessedAt: import_zod6.z.number()
|
|
599
|
+
lastAccessedAt: import_zod6.z.number(),
|
|
600
|
+
initialized: import_zod6.z.boolean().optional(),
|
|
601
|
+
maxLifetimeAt: import_zod6.z.number().optional()
|
|
604
602
|
});
|
|
605
603
|
redisConfigSchema = import_zod6.z.object({
|
|
606
604
|
host: import_zod6.z.string().min(1),
|
|
@@ -612,15 +610,6 @@ var init_transport_session_types = __esm({
|
|
|
612
610
|
defaultTtlMs: import_zod6.z.number().int().positive().optional().default(36e5)
|
|
613
611
|
// 1 hour default
|
|
614
612
|
});
|
|
615
|
-
statefulStorageSchema = import_zod6.z.discriminatedUnion("store", [
|
|
616
|
-
import_zod6.z.object({ store: import_zod6.z.literal("memory") }),
|
|
617
|
-
import_zod6.z.object({ store: import_zod6.z.literal("redis"), config: redisConfigSchema })
|
|
618
|
-
]);
|
|
619
|
-
sessionStorageConfigSchema = import_zod6.z.union([
|
|
620
|
-
import_zod6.z.object({ mode: import_zod6.z.literal("stateless") }),
|
|
621
|
-
import_zod6.z.object({ mode: import_zod6.z.literal("stateful") }).merge(statefulStorageSchema.options[0]),
|
|
622
|
-
import_zod6.z.object({ mode: import_zod6.z.literal("stateful") }).merge(statefulStorageSchema.options[1])
|
|
623
|
-
]);
|
|
624
613
|
}
|
|
625
614
|
});
|
|
626
615
|
|
|
@@ -15726,8 +15715,19 @@ var init_http_request_flow = __esm({
|
|
|
15726
15715
|
try {
|
|
15727
15716
|
const { request } = this.rawInput;
|
|
15728
15717
|
this.logger.verbose(`[${this.requestId}] router: check request decision`);
|
|
15729
|
-
const transport = this.scope.auth.transport;
|
|
15718
|
+
const transport = this.scope.metadata.transport ?? this.scope.auth.transport;
|
|
15719
|
+
this.logger.debug(`[${this.requestId}] transport config`, {
|
|
15720
|
+
enableLegacySSE: transport.enableLegacySSE,
|
|
15721
|
+
enableStreamableHttp: transport.enableStreamableHttp,
|
|
15722
|
+
path: request.path,
|
|
15723
|
+
accept: request.headers?.["accept"]
|
|
15724
|
+
});
|
|
15730
15725
|
const decision = decideIntent(request, { ...transport, tolerateMissingAccept: true });
|
|
15726
|
+
this.logger.debug(`[${this.requestId}] decision result`, {
|
|
15727
|
+
intent: decision.intent,
|
|
15728
|
+
reasons: decision.reasons,
|
|
15729
|
+
debug: decision.debug
|
|
15730
|
+
});
|
|
15731
15731
|
if (request.method.toUpperCase() === "DELETE") {
|
|
15732
15732
|
this.logger.verbose(`[${this.requestId}] DELETE request, using decision intent: ${decision.intent}`);
|
|
15733
15733
|
if (decision.intent === "unknown") {
|
|
@@ -16041,6 +16041,8 @@ var init_transport_remote = __esm({
|
|
|
16041
16041
|
async destroy(_reason) {
|
|
16042
16042
|
throw new Error("RemoteTransporter: destroy() not implemented.");
|
|
16043
16043
|
}
|
|
16044
|
+
markAsInitialized() {
|
|
16045
|
+
}
|
|
16044
16046
|
};
|
|
16045
16047
|
}
|
|
16046
16048
|
});
|
|
@@ -16220,6 +16222,79 @@ data: ${JSON.stringify(message)}
|
|
|
16220
16222
|
}
|
|
16221
16223
|
});
|
|
16222
16224
|
|
|
16225
|
+
// libs/sdk/src/transport/adapters/sse-transport.ts
|
|
16226
|
+
var RecreateableSSEServerTransport;
|
|
16227
|
+
var init_sse_transport = __esm({
|
|
16228
|
+
"libs/sdk/src/transport/adapters/sse-transport.ts"() {
|
|
16229
|
+
"use strict";
|
|
16230
|
+
init_legacy_sse_tranporter();
|
|
16231
|
+
RecreateableSSEServerTransport = class extends SSEServerTransport {
|
|
16232
|
+
_isRecreatedSession = false;
|
|
16233
|
+
constructor(endpoint, res, options) {
|
|
16234
|
+
super(endpoint, res, options);
|
|
16235
|
+
if (options?.initialEventId !== void 0 && this.isValidEventId(options.initialEventId)) {
|
|
16236
|
+
this.setEventIdCounter(options.initialEventId);
|
|
16237
|
+
this._isRecreatedSession = true;
|
|
16238
|
+
}
|
|
16239
|
+
}
|
|
16240
|
+
/**
|
|
16241
|
+
* Validates that an event ID is a valid non-negative integer.
|
|
16242
|
+
* Protects against negative values, NaN, Infinity, and non-integer values.
|
|
16243
|
+
*/
|
|
16244
|
+
isValidEventId(eventId) {
|
|
16245
|
+
return Number.isInteger(eventId) && eventId >= 0 && eventId <= Number.MAX_SAFE_INTEGER;
|
|
16246
|
+
}
|
|
16247
|
+
/**
|
|
16248
|
+
* Returns whether this is a recreated session.
|
|
16249
|
+
*/
|
|
16250
|
+
get isRecreatedSession() {
|
|
16251
|
+
return this._isRecreatedSession;
|
|
16252
|
+
}
|
|
16253
|
+
/**
|
|
16254
|
+
* Returns the current event ID counter value.
|
|
16255
|
+
* Alias for lastEventId for consistency with the recreation API.
|
|
16256
|
+
*/
|
|
16257
|
+
get eventIdCounter() {
|
|
16258
|
+
return this.lastEventId;
|
|
16259
|
+
}
|
|
16260
|
+
/**
|
|
16261
|
+
* Sets the event ID counter for session recreation.
|
|
16262
|
+
* Use this when recreating a session from stored state to maintain
|
|
16263
|
+
* event ID continuity for SSE reconnection support.
|
|
16264
|
+
*
|
|
16265
|
+
* @param eventId - The event ID to restore (must be a non-negative integer)
|
|
16266
|
+
*/
|
|
16267
|
+
setEventIdCounter(eventId) {
|
|
16268
|
+
if (!this.isValidEventId(eventId)) {
|
|
16269
|
+
console.warn(
|
|
16270
|
+
`[RecreateableSSEServerTransport] Invalid eventId: ${eventId}. Must be a non-negative integer. Ignoring.`
|
|
16271
|
+
);
|
|
16272
|
+
return;
|
|
16273
|
+
}
|
|
16274
|
+
this._eventIdCounter = eventId;
|
|
16275
|
+
}
|
|
16276
|
+
/**
|
|
16277
|
+
* Sets the transport to a recreated session state.
|
|
16278
|
+
* Use this when recreating a transport from a stored session.
|
|
16279
|
+
*
|
|
16280
|
+
* @param sessionId - The session ID (for verification, should match constructor)
|
|
16281
|
+
* @param lastEventId - The last event ID that was sent to the client
|
|
16282
|
+
*/
|
|
16283
|
+
setSessionState(sessionId, lastEventId) {
|
|
16284
|
+
if (this.sessionId !== sessionId) {
|
|
16285
|
+
console.warn(
|
|
16286
|
+
`RecreateableSSEServerTransport: session ID mismatch. Expected ${sessionId}, got ${this.sessionId}. Using constructor value.`
|
|
16287
|
+
);
|
|
16288
|
+
}
|
|
16289
|
+
if (lastEventId !== void 0) {
|
|
16290
|
+
this.setEventIdCounter(lastEventId);
|
|
16291
|
+
}
|
|
16292
|
+
this._isRecreatedSession = true;
|
|
16293
|
+
}
|
|
16294
|
+
};
|
|
16295
|
+
}
|
|
16296
|
+
});
|
|
16297
|
+
|
|
16223
16298
|
// libs/sdk/src/transport/transport.event-store.ts
|
|
16224
16299
|
var InMemoryEventStore;
|
|
16225
16300
|
var init_transport_event_store = __esm({
|
|
@@ -16719,6 +16794,12 @@ var init_transport_local_adapter = __esm({
|
|
|
16719
16794
|
#requestId = 1;
|
|
16720
16795
|
ready;
|
|
16721
16796
|
server;
|
|
16797
|
+
/**
|
|
16798
|
+
* Marks this transport as pre-initialized for session recreation.
|
|
16799
|
+
* Override in subclasses that need to set the MCP SDK's _initialized flag.
|
|
16800
|
+
*/
|
|
16801
|
+
markAsInitialized() {
|
|
16802
|
+
}
|
|
16722
16803
|
connectServer() {
|
|
16723
16804
|
const { info } = this.scope.metadata;
|
|
16724
16805
|
const hasPrompts = this.scope.prompts.hasAny();
|
|
@@ -16851,23 +16932,46 @@ var import_v42, TransportSSEAdapter;
|
|
|
16851
16932
|
var init_transport_sse_adapter = __esm({
|
|
16852
16933
|
"libs/sdk/src/transport/adapters/transport.sse.adapter.ts"() {
|
|
16853
16934
|
"use strict";
|
|
16854
|
-
|
|
16935
|
+
init_sse_transport();
|
|
16855
16936
|
init_transport_local_adapter();
|
|
16856
16937
|
import_v42 = require("zod/v4");
|
|
16857
16938
|
init_transport_error();
|
|
16858
16939
|
TransportSSEAdapter = class extends LocalTransportAdapter {
|
|
16859
16940
|
sessionId;
|
|
16941
|
+
/**
|
|
16942
|
+
* Configures common error and close handlers for SSE transports.
|
|
16943
|
+
*/
|
|
16944
|
+
configureTransportHandlers(transport) {
|
|
16945
|
+
transport.onerror = (error) => {
|
|
16946
|
+
console.error("SSE error:", error instanceof Error ? error.message : "Unknown error");
|
|
16947
|
+
};
|
|
16948
|
+
transport.onclose = this.destroy.bind(this);
|
|
16949
|
+
}
|
|
16860
16950
|
createTransport(sessionId, res) {
|
|
16861
16951
|
this.sessionId = sessionId;
|
|
16862
16952
|
this.logger.info(`new transport session: ${sessionId.slice(0, 40)}`);
|
|
16863
|
-
const
|
|
16864
|
-
const transport = new SSEServerTransport(`${scopePath}/message`, res, {
|
|
16953
|
+
const transport = new RecreateableSSEServerTransport(`${this.scope.fullPath}/message`, res, {
|
|
16865
16954
|
sessionId
|
|
16866
16955
|
});
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16956
|
+
this.configureTransportHandlers(transport);
|
|
16957
|
+
return transport;
|
|
16958
|
+
}
|
|
16959
|
+
/**
|
|
16960
|
+
* Recreates a transport with preserved session state.
|
|
16961
|
+
* Use this when restoring a session from Redis or other storage.
|
|
16962
|
+
*
|
|
16963
|
+
* @param sessionId - The session ID to restore
|
|
16964
|
+
* @param res - The new response stream for SSE
|
|
16965
|
+
* @param lastEventId - The last event ID that was sent (for reconnection support)
|
|
16966
|
+
*/
|
|
16967
|
+
createTransportFromSession(sessionId, res, lastEventId) {
|
|
16968
|
+
this.sessionId = sessionId;
|
|
16969
|
+
this.logger.info(`recreating transport session: ${sessionId.slice(0, 40)}, lastEventId: ${lastEventId ?? "none"}`);
|
|
16970
|
+
const transport = new RecreateableSSEServerTransport(`${this.scope.fullPath}/message`, res, {
|
|
16971
|
+
sessionId,
|
|
16972
|
+
initialEventId: lastEventId
|
|
16973
|
+
});
|
|
16974
|
+
this.configureTransportHandlers(transport);
|
|
16871
16975
|
return transport;
|
|
16872
16976
|
}
|
|
16873
16977
|
initialize(req, res) {
|
|
@@ -16914,22 +17018,73 @@ var init_transport_sse_adapter = __esm({
|
|
|
16914
17018
|
}
|
|
16915
17019
|
});
|
|
16916
17020
|
|
|
17021
|
+
// libs/sdk/src/transport/adapters/streamable-http-transport.ts
|
|
17022
|
+
var import_streamableHttp, RecreateableStreamableHTTPServerTransport;
|
|
17023
|
+
var init_streamable_http_transport = __esm({
|
|
17024
|
+
"libs/sdk/src/transport/adapters/streamable-http-transport.ts"() {
|
|
17025
|
+
"use strict";
|
|
17026
|
+
import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
17027
|
+
RecreateableStreamableHTTPServerTransport = class extends import_streamableHttp.StreamableHTTPServerTransport {
|
|
17028
|
+
constructor(options = {}) {
|
|
17029
|
+
super(options);
|
|
17030
|
+
}
|
|
17031
|
+
/**
|
|
17032
|
+
* Returns whether the transport has been initialized.
|
|
17033
|
+
*/
|
|
17034
|
+
get isInitialized() {
|
|
17035
|
+
return this._webStandardTransport?._initialized ?? false;
|
|
17036
|
+
}
|
|
17037
|
+
/**
|
|
17038
|
+
* Sets the transport to an initialized state with the given session ID.
|
|
17039
|
+
* Use this when recreating a transport from a stored session.
|
|
17040
|
+
*
|
|
17041
|
+
* This method allows you to "restore" a session without replaying the
|
|
17042
|
+
* initialization handshake. After calling this method, the transport
|
|
17043
|
+
* will accept requests with the given session ID.
|
|
17044
|
+
*
|
|
17045
|
+
* @param sessionId - The session ID that was previously assigned to this session
|
|
17046
|
+
* @throws Error if sessionId is empty or invalid
|
|
17047
|
+
*/
|
|
17048
|
+
setInitializationState(sessionId) {
|
|
17049
|
+
if (!sessionId || typeof sessionId !== "string" || sessionId.trim() === "") {
|
|
17050
|
+
throw new Error("[RecreateableStreamableHTTPServerTransport] sessionId cannot be empty");
|
|
17051
|
+
}
|
|
17052
|
+
const webTransport = this._webStandardTransport;
|
|
17053
|
+
if (!webTransport) {
|
|
17054
|
+
console.warn(
|
|
17055
|
+
"[RecreateableStreamableHTTPServerTransport] Internal transport not found. This may indicate an incompatible MCP SDK version."
|
|
17056
|
+
);
|
|
17057
|
+
return;
|
|
17058
|
+
}
|
|
17059
|
+
if (!("_initialized" in webTransport) || !("sessionId" in webTransport)) {
|
|
17060
|
+
console.warn(
|
|
17061
|
+
"[RecreateableStreamableHTTPServerTransport] Expected fields not found on internal transport. This may indicate an incompatible MCP SDK version."
|
|
17062
|
+
);
|
|
17063
|
+
return;
|
|
17064
|
+
}
|
|
17065
|
+
webTransport._initialized = true;
|
|
17066
|
+
webTransport.sessionId = sessionId;
|
|
17067
|
+
}
|
|
17068
|
+
};
|
|
17069
|
+
}
|
|
17070
|
+
});
|
|
17071
|
+
|
|
16917
17072
|
// libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts
|
|
16918
|
-
var
|
|
17073
|
+
var import_v43, resolveSessionIdGenerator, TransportStreamableHttpAdapter;
|
|
16919
17074
|
var init_transport_streamable_http_adapter = __esm({
|
|
16920
17075
|
"libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts"() {
|
|
16921
17076
|
"use strict";
|
|
16922
|
-
import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
16923
17077
|
init_transport_local_adapter();
|
|
16924
17078
|
import_v43 = require("zod/v4");
|
|
16925
17079
|
init_transport_error();
|
|
17080
|
+
init_streamable_http_transport();
|
|
16926
17081
|
resolveSessionIdGenerator = (transportType, sessionId) => {
|
|
16927
17082
|
return transportType === "stateless-http" ? void 0 : () => sessionId;
|
|
16928
17083
|
};
|
|
16929
17084
|
TransportStreamableHttpAdapter = class extends LocalTransportAdapter {
|
|
16930
17085
|
createTransport(sessionId, response) {
|
|
16931
17086
|
const sessionIdGenerator = resolveSessionIdGenerator(this.key.type, sessionId);
|
|
16932
|
-
return new
|
|
17087
|
+
return new RecreateableStreamableHTTPServerTransport({
|
|
16933
17088
|
sessionIdGenerator,
|
|
16934
17089
|
onsessionclosed: () => {
|
|
16935
17090
|
},
|
|
@@ -17008,6 +17163,21 @@ var init_transport_streamable_http_adapter = __esm({
|
|
|
17008
17163
|
};
|
|
17009
17164
|
});
|
|
17010
17165
|
}
|
|
17166
|
+
/**
|
|
17167
|
+
* Marks this transport as pre-initialized for session recreation.
|
|
17168
|
+
* This is needed when recreating a transport from Redis because the
|
|
17169
|
+
* original initialize request was processed by a different transport instance.
|
|
17170
|
+
*
|
|
17171
|
+
* Uses the RecreateableStreamableHTTPServerTransport's public API to set
|
|
17172
|
+
* initialization state, avoiding access to private properties.
|
|
17173
|
+
*/
|
|
17174
|
+
markAsInitialized() {
|
|
17175
|
+
this.transport.setInitializationState(this.key.sessionId);
|
|
17176
|
+
this.logger.info("[StreamableHttpAdapter] Marked transport as pre-initialized for session recreation", {
|
|
17177
|
+
sessionId: this.key.sessionId?.slice(0, 20),
|
|
17178
|
+
isInitialized: this.transport.isInitialized
|
|
17179
|
+
});
|
|
17180
|
+
}
|
|
17011
17181
|
};
|
|
17012
17182
|
}
|
|
17013
17183
|
});
|
|
@@ -17066,6 +17236,14 @@ var init_transport_local = __esm({
|
|
|
17066
17236
|
res.status(500).json(rpcError("Internal error"));
|
|
17067
17237
|
}
|
|
17068
17238
|
}
|
|
17239
|
+
/**
|
|
17240
|
+
* Marks this transport as pre-initialized for session recreation.
|
|
17241
|
+
* This is needed when recreating a transport from Redis because the
|
|
17242
|
+
* original initialize request was processed by a different transport instance.
|
|
17243
|
+
*/
|
|
17244
|
+
markAsInitialized() {
|
|
17245
|
+
this.adapter.markAsInitialized();
|
|
17246
|
+
}
|
|
17069
17247
|
async destroy(reason) {
|
|
17070
17248
|
try {
|
|
17071
17249
|
await this.adapter.destroy(reason);
|
|
@@ -17203,14 +17381,27 @@ var init_handle_streamable_http_flow = __esm({
|
|
|
17203
17381
|
const logger = this.scopeLogger.child("handle:streamable-http:onMessage");
|
|
17204
17382
|
const { request, response } = this.rawInput;
|
|
17205
17383
|
const { token, session } = this.state.required;
|
|
17384
|
+
logger.info("onMessage: starting", {
|
|
17385
|
+
sessionId: session.id?.slice(0, 20),
|
|
17386
|
+
hasToken: !!token
|
|
17387
|
+
});
|
|
17206
17388
|
let transport = await transportService.getTransporter("streamable-http", token, session.id);
|
|
17389
|
+
logger.info("onMessage: getTransporter result", { found: !!transport });
|
|
17207
17390
|
if (!transport) {
|
|
17208
17391
|
try {
|
|
17392
|
+
logger.info("onMessage: transport not in memory, checking Redis", {
|
|
17393
|
+
sessionId: session.id?.slice(0, 20)
|
|
17394
|
+
});
|
|
17209
17395
|
const storedSession = await transportService.getStoredSession("streamable-http", token, session.id);
|
|
17396
|
+
logger.info("onMessage: getStoredSession result", {
|
|
17397
|
+
found: !!storedSession,
|
|
17398
|
+
initialized: storedSession?.initialized
|
|
17399
|
+
});
|
|
17210
17400
|
if (storedSession) {
|
|
17211
17401
|
logger.info("Recreating transport from Redis session", {
|
|
17212
17402
|
sessionId: session.id?.slice(0, 20),
|
|
17213
|
-
createdAt: storedSession.createdAt
|
|
17403
|
+
createdAt: storedSession.createdAt,
|
|
17404
|
+
initialized: storedSession.initialized
|
|
17214
17405
|
});
|
|
17215
17406
|
transport = await transportService.recreateTransporter(
|
|
17216
17407
|
"streamable-http",
|
|
@@ -17219,6 +17410,7 @@ var init_handle_streamable_http_flow = __esm({
|
|
|
17219
17410
|
storedSession,
|
|
17220
17411
|
response
|
|
17221
17412
|
);
|
|
17413
|
+
logger.info("onMessage: transport recreated successfully");
|
|
17222
17414
|
}
|
|
17223
17415
|
} catch (error) {
|
|
17224
17416
|
logger.warn("Failed to recreate transport from stored session", {
|
|
@@ -17615,27 +17807,244 @@ var init_store_helpers = __esm({
|
|
|
17615
17807
|
}
|
|
17616
17808
|
});
|
|
17617
17809
|
|
|
17810
|
+
// libs/sdk/src/auth/session/session-crypto.ts
|
|
17811
|
+
function getSigningSecret(config) {
|
|
17812
|
+
const secret = config?.secret || process.env["MCP_SESSION_SECRET"];
|
|
17813
|
+
if (!secret) {
|
|
17814
|
+
if (process.env["NODE_ENV"] === "production") {
|
|
17815
|
+
throw new Error(
|
|
17816
|
+
"[SessionCrypto] MCP_SESSION_SECRET is required in production for session signing. Set this environment variable to a secure random string."
|
|
17817
|
+
);
|
|
17818
|
+
}
|
|
17819
|
+
console.warn("[SessionCrypto] MCP_SESSION_SECRET not set. Using insecure default for development only.");
|
|
17820
|
+
return "insecure-dev-secret-do-not-use-in-production";
|
|
17821
|
+
}
|
|
17822
|
+
return secret;
|
|
17823
|
+
}
|
|
17824
|
+
function computeSignature(data, secret) {
|
|
17825
|
+
return (0, import_crypto13.createHmac)("sha256", secret).update(data, "utf8").digest("base64url");
|
|
17826
|
+
}
|
|
17827
|
+
function signSession(session, config) {
|
|
17828
|
+
const secret = getSigningSecret(config);
|
|
17829
|
+
const data = JSON.stringify(session);
|
|
17830
|
+
const sig = computeSignature(data, secret);
|
|
17831
|
+
const signed = {
|
|
17832
|
+
data: session,
|
|
17833
|
+
sig,
|
|
17834
|
+
v: 1
|
|
17835
|
+
};
|
|
17836
|
+
return JSON.stringify(signed);
|
|
17837
|
+
}
|
|
17838
|
+
function verifySession(signedData, config) {
|
|
17839
|
+
try {
|
|
17840
|
+
const secret = getSigningSecret(config);
|
|
17841
|
+
const parsed = JSON.parse(signedData);
|
|
17842
|
+
if (!parsed || typeof parsed !== "object" || !("sig" in parsed)) {
|
|
17843
|
+
return null;
|
|
17844
|
+
}
|
|
17845
|
+
const signed = parsed;
|
|
17846
|
+
if (signed.v !== 1) {
|
|
17847
|
+
console.warn("[SessionCrypto] Unknown signature version:", signed.v);
|
|
17848
|
+
return null;
|
|
17849
|
+
}
|
|
17850
|
+
const data = JSON.stringify(signed.data);
|
|
17851
|
+
const expectedSig = computeSignature(data, secret);
|
|
17852
|
+
const sigBuffer = Buffer.from(signed.sig, "base64url");
|
|
17853
|
+
const expectedBuffer = Buffer.from(expectedSig, "base64url");
|
|
17854
|
+
if (sigBuffer.length !== expectedBuffer.length) {
|
|
17855
|
+
console.warn("[SessionCrypto] Signature length mismatch - possible tampering");
|
|
17856
|
+
return null;
|
|
17857
|
+
}
|
|
17858
|
+
if (!(0, import_crypto13.timingSafeEqual)(sigBuffer, expectedBuffer)) {
|
|
17859
|
+
console.warn("[SessionCrypto] HMAC verification failed - session data may be tampered");
|
|
17860
|
+
return null;
|
|
17861
|
+
}
|
|
17862
|
+
return signed.data;
|
|
17863
|
+
} catch (error) {
|
|
17864
|
+
console.warn("[SessionCrypto] Failed to verify session:", error.message);
|
|
17865
|
+
return null;
|
|
17866
|
+
}
|
|
17867
|
+
}
|
|
17868
|
+
function isSignedSession(data) {
|
|
17869
|
+
try {
|
|
17870
|
+
const parsed = JSON.parse(data);
|
|
17871
|
+
return parsed && typeof parsed === "object" && "sig" in parsed && "v" in parsed;
|
|
17872
|
+
} catch {
|
|
17873
|
+
return false;
|
|
17874
|
+
}
|
|
17875
|
+
}
|
|
17876
|
+
function verifyOrParseSession(data, config) {
|
|
17877
|
+
if (isSignedSession(data)) {
|
|
17878
|
+
return verifySession(data, config);
|
|
17879
|
+
}
|
|
17880
|
+
try {
|
|
17881
|
+
return JSON.parse(data);
|
|
17882
|
+
} catch {
|
|
17883
|
+
return null;
|
|
17884
|
+
}
|
|
17885
|
+
}
|
|
17886
|
+
var import_crypto13;
|
|
17887
|
+
var init_session_crypto = __esm({
|
|
17888
|
+
"libs/sdk/src/auth/session/session-crypto.ts"() {
|
|
17889
|
+
"use strict";
|
|
17890
|
+
import_crypto13 = require("crypto");
|
|
17891
|
+
}
|
|
17892
|
+
});
|
|
17893
|
+
|
|
17894
|
+
// libs/sdk/src/auth/session/session-rate-limiter.ts
|
|
17895
|
+
var SessionRateLimiter, defaultSessionRateLimiter;
|
|
17896
|
+
var init_session_rate_limiter = __esm({
|
|
17897
|
+
"libs/sdk/src/auth/session/session-rate-limiter.ts"() {
|
|
17898
|
+
"use strict";
|
|
17899
|
+
SessionRateLimiter = class {
|
|
17900
|
+
windowMs;
|
|
17901
|
+
maxRequests;
|
|
17902
|
+
requests = /* @__PURE__ */ new Map();
|
|
17903
|
+
cleanupTimer = null;
|
|
17904
|
+
constructor(config = {}) {
|
|
17905
|
+
this.windowMs = config.windowMs ?? 6e4;
|
|
17906
|
+
this.maxRequests = config.maxRequests ?? 100;
|
|
17907
|
+
const cleanupIntervalMs = config.cleanupIntervalMs ?? 6e4;
|
|
17908
|
+
if (cleanupIntervalMs > 0) {
|
|
17909
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), cleanupIntervalMs);
|
|
17910
|
+
this.cleanupTimer.unref();
|
|
17911
|
+
}
|
|
17912
|
+
}
|
|
17913
|
+
/**
|
|
17914
|
+
* Check if a request is allowed for the given key.
|
|
17915
|
+
*
|
|
17916
|
+
* @param key - Identifier for rate limiting (e.g., client IP, session ID prefix)
|
|
17917
|
+
* @returns Rate limit result with allowed status and metadata
|
|
17918
|
+
*/
|
|
17919
|
+
check(key) {
|
|
17920
|
+
const now = Date.now();
|
|
17921
|
+
const windowStart = now - this.windowMs;
|
|
17922
|
+
let timestamps = this.requests.get(key);
|
|
17923
|
+
if (!timestamps) {
|
|
17924
|
+
timestamps = [];
|
|
17925
|
+
this.requests.set(key, timestamps);
|
|
17926
|
+
}
|
|
17927
|
+
const validTimestamps = timestamps.filter((t) => t > windowStart);
|
|
17928
|
+
const oldestInWindow = validTimestamps[0] ?? now;
|
|
17929
|
+
const resetAt = oldestInWindow + this.windowMs;
|
|
17930
|
+
if (validTimestamps.length >= this.maxRequests) {
|
|
17931
|
+
return {
|
|
17932
|
+
allowed: false,
|
|
17933
|
+
remaining: 0,
|
|
17934
|
+
resetAt,
|
|
17935
|
+
retryAfterMs: resetAt - now
|
|
17936
|
+
};
|
|
17937
|
+
}
|
|
17938
|
+
validTimestamps.push(now);
|
|
17939
|
+
this.requests.set(key, validTimestamps);
|
|
17940
|
+
return {
|
|
17941
|
+
allowed: true,
|
|
17942
|
+
remaining: this.maxRequests - validTimestamps.length,
|
|
17943
|
+
resetAt
|
|
17944
|
+
};
|
|
17945
|
+
}
|
|
17946
|
+
/**
|
|
17947
|
+
* Check if a request would be allowed without recording it.
|
|
17948
|
+
* Useful for pre-checking without consuming quota.
|
|
17949
|
+
*
|
|
17950
|
+
* @param key - Identifier for rate limiting
|
|
17951
|
+
* @returns true if request would be allowed
|
|
17952
|
+
*/
|
|
17953
|
+
wouldAllow(key) {
|
|
17954
|
+
const now = Date.now();
|
|
17955
|
+
const windowStart = now - this.windowMs;
|
|
17956
|
+
const timestamps = this.requests.get(key);
|
|
17957
|
+
if (!timestamps) return true;
|
|
17958
|
+
const validCount = timestamps.filter((t) => t > windowStart).length;
|
|
17959
|
+
return validCount < this.maxRequests;
|
|
17960
|
+
}
|
|
17961
|
+
/**
|
|
17962
|
+
* Reset rate limit for a specific key.
|
|
17963
|
+
* Useful for testing or after successful authentication.
|
|
17964
|
+
*
|
|
17965
|
+
* @param key - Identifier to reset
|
|
17966
|
+
*/
|
|
17967
|
+
reset(key) {
|
|
17968
|
+
this.requests.delete(key);
|
|
17969
|
+
}
|
|
17970
|
+
/**
|
|
17971
|
+
* Clean up expired entries from all keys.
|
|
17972
|
+
* Called automatically on configured interval, but can be called manually.
|
|
17973
|
+
*/
|
|
17974
|
+
cleanup() {
|
|
17975
|
+
const now = Date.now();
|
|
17976
|
+
const windowStart = now - this.windowMs;
|
|
17977
|
+
for (const [key, timestamps] of this.requests.entries()) {
|
|
17978
|
+
const valid = timestamps.filter((t) => t > windowStart);
|
|
17979
|
+
if (valid.length === 0) {
|
|
17980
|
+
this.requests.delete(key);
|
|
17981
|
+
} else if (valid.length < timestamps.length) {
|
|
17982
|
+
this.requests.set(key, valid);
|
|
17983
|
+
}
|
|
17984
|
+
}
|
|
17985
|
+
}
|
|
17986
|
+
/**
|
|
17987
|
+
* Get current statistics for monitoring.
|
|
17988
|
+
*/
|
|
17989
|
+
getStats() {
|
|
17990
|
+
let totalRequests = 0;
|
|
17991
|
+
for (const timestamps of this.requests.values()) {
|
|
17992
|
+
totalRequests += timestamps.length;
|
|
17993
|
+
}
|
|
17994
|
+
return {
|
|
17995
|
+
totalKeys: this.requests.size,
|
|
17996
|
+
totalRequests
|
|
17997
|
+
};
|
|
17998
|
+
}
|
|
17999
|
+
/**
|
|
18000
|
+
* Stop the automatic cleanup timer.
|
|
18001
|
+
* Call this when disposing of the rate limiter.
|
|
18002
|
+
*/
|
|
18003
|
+
dispose() {
|
|
18004
|
+
if (this.cleanupTimer) {
|
|
18005
|
+
clearInterval(this.cleanupTimer);
|
|
18006
|
+
this.cleanupTimer = null;
|
|
18007
|
+
}
|
|
18008
|
+
this.requests.clear();
|
|
18009
|
+
}
|
|
18010
|
+
};
|
|
18011
|
+
defaultSessionRateLimiter = new SessionRateLimiter();
|
|
18012
|
+
}
|
|
18013
|
+
});
|
|
18014
|
+
|
|
17618
18015
|
// libs/sdk/src/auth/session/redis-session.store.ts
|
|
17619
18016
|
var redis_session_store_exports = {};
|
|
17620
18017
|
__export(redis_session_store_exports, {
|
|
17621
18018
|
RedisSessionStore: () => RedisSessionStore
|
|
17622
18019
|
});
|
|
17623
|
-
var import_ioredis,
|
|
18020
|
+
var import_ioredis, import_crypto14, RedisSessionStore;
|
|
17624
18021
|
var init_redis_session_store = __esm({
|
|
17625
18022
|
"libs/sdk/src/auth/session/redis-session.store.ts"() {
|
|
17626
18023
|
"use strict";
|
|
17627
18024
|
import_ioredis = __toESM(require("ioredis"));
|
|
17628
|
-
|
|
18025
|
+
import_crypto14 = require("crypto");
|
|
17629
18026
|
init_transport_session_types();
|
|
18027
|
+
init_session_crypto();
|
|
18028
|
+
init_session_rate_limiter();
|
|
17630
18029
|
RedisSessionStore = class {
|
|
17631
18030
|
redis;
|
|
17632
18031
|
keyPrefix;
|
|
17633
18032
|
defaultTtlMs;
|
|
17634
18033
|
logger;
|
|
17635
18034
|
externalInstance = false;
|
|
18035
|
+
// Security features
|
|
18036
|
+
security;
|
|
18037
|
+
rateLimiter;
|
|
17636
18038
|
constructor(config, logger) {
|
|
17637
18039
|
this.defaultTtlMs = ("defaultTtlMs" in config ? config.defaultTtlMs : void 0) ?? 36e5;
|
|
17638
18040
|
this.logger = logger;
|
|
18041
|
+
this.security = ("security" in config ? config.security : void 0) ?? {};
|
|
18042
|
+
if (this.security.enableRateLimiting) {
|
|
18043
|
+
this.rateLimiter = new SessionRateLimiter({
|
|
18044
|
+
windowMs: this.security.rateLimiting?.windowMs,
|
|
18045
|
+
maxRequests: this.security.rateLimiting?.maxRequests
|
|
18046
|
+
});
|
|
18047
|
+
}
|
|
17639
18048
|
if ("redis" in config && config.redis) {
|
|
17640
18049
|
this.redis = config.redis;
|
|
17641
18050
|
this.keyPrefix = config.keyPrefix ?? "mcp:session:";
|
|
@@ -17670,8 +18079,26 @@ var init_redis_session_store = __esm({
|
|
|
17670
18079
|
*
|
|
17671
18080
|
* Note: Uses atomic GETEX to extend TTL while reading, preventing race conditions
|
|
17672
18081
|
* where concurrent readers might resurrect expired sessions.
|
|
17673
|
-
|
|
17674
|
-
|
|
18082
|
+
*
|
|
18083
|
+
* @param sessionId - The session ID to look up
|
|
18084
|
+
* @param options - Optional parameters for rate limiting
|
|
18085
|
+
* @param options.clientIdentifier - Client identifier (e.g., IP address) for rate limiting.
|
|
18086
|
+
* When provided, rate limiting is applied per-client to prevent session enumeration.
|
|
18087
|
+
* If not provided, falls back to sessionId which provides DoS protection per-session.
|
|
18088
|
+
*/
|
|
18089
|
+
async get(sessionId, options) {
|
|
18090
|
+
if (this.rateLimiter) {
|
|
18091
|
+
const rateLimitKey = options?.clientIdentifier || sessionId;
|
|
18092
|
+
const rateLimitResult = this.rateLimiter.check(rateLimitKey);
|
|
18093
|
+
if (!rateLimitResult.allowed) {
|
|
18094
|
+
this.logger?.warn("[RedisSessionStore] Rate limit exceeded for session lookup", {
|
|
18095
|
+
sessionId: sessionId.slice(0, 20),
|
|
18096
|
+
clientIdentifier: options?.clientIdentifier ? options.clientIdentifier.slice(0, 20) : void 0,
|
|
18097
|
+
retryAfterMs: rateLimitResult.retryAfterMs
|
|
18098
|
+
});
|
|
18099
|
+
return null;
|
|
18100
|
+
}
|
|
18101
|
+
}
|
|
17675
18102
|
const key = this.key(sessionId);
|
|
17676
18103
|
let raw;
|
|
17677
18104
|
try {
|
|
@@ -17681,7 +18108,19 @@ var init_redis_session_store = __esm({
|
|
|
17681
18108
|
}
|
|
17682
18109
|
if (!raw) return null;
|
|
17683
18110
|
try {
|
|
17684
|
-
|
|
18111
|
+
let parsed;
|
|
18112
|
+
if (this.security.enableSigning) {
|
|
18113
|
+
parsed = verifyOrParseSession(raw, { secret: this.security.signingSecret });
|
|
18114
|
+
if (!parsed) {
|
|
18115
|
+
this.logger?.warn("[RedisSessionStore] Session signature verification failed", {
|
|
18116
|
+
sessionId: sessionId.slice(0, 20)
|
|
18117
|
+
});
|
|
18118
|
+
this.delete(sessionId).catch(() => void 0);
|
|
18119
|
+
return null;
|
|
18120
|
+
}
|
|
18121
|
+
} else {
|
|
18122
|
+
parsed = JSON.parse(raw);
|
|
18123
|
+
}
|
|
17685
18124
|
const result = storedSessionSchema.safeParse(parsed);
|
|
17686
18125
|
if (!result.success) {
|
|
17687
18126
|
this.logger?.warn("[RedisSessionStore] Invalid session format", {
|
|
@@ -17692,6 +18131,14 @@ var init_redis_session_store = __esm({
|
|
|
17692
18131
|
return null;
|
|
17693
18132
|
}
|
|
17694
18133
|
const session = result.data;
|
|
18134
|
+
if (session.maxLifetimeAt && session.maxLifetimeAt < Date.now()) {
|
|
18135
|
+
this.logger?.info("[RedisSessionStore] Session exceeded max lifetime", {
|
|
18136
|
+
sessionId: sessionId.slice(0, 20),
|
|
18137
|
+
maxLifetimeAt: session.maxLifetimeAt
|
|
18138
|
+
});
|
|
18139
|
+
await this.delete(sessionId);
|
|
18140
|
+
return null;
|
|
18141
|
+
}
|
|
17695
18142
|
if (session.session.expiresAt && session.session.expiresAt < Date.now()) {
|
|
17696
18143
|
await this.delete(sessionId);
|
|
17697
18144
|
return null;
|
|
@@ -17699,7 +18146,12 @@ var init_redis_session_store = __esm({
|
|
|
17699
18146
|
if (session.session.expiresAt) {
|
|
17700
18147
|
const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());
|
|
17701
18148
|
if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {
|
|
17702
|
-
this.redis.pexpire(key, ttlMs).catch(() =>
|
|
18149
|
+
this.redis.pexpire(key, ttlMs).catch((err) => {
|
|
18150
|
+
this.logger?.warn("[RedisSessionStore] TTL extension failed", {
|
|
18151
|
+
sessionId: sessionId.slice(0, 20),
|
|
18152
|
+
error: err.message
|
|
18153
|
+
});
|
|
18154
|
+
});
|
|
17703
18155
|
}
|
|
17704
18156
|
}
|
|
17705
18157
|
const updatedSession = {
|
|
@@ -17721,7 +18173,12 @@ var init_redis_session_store = __esm({
|
|
|
17721
18173
|
*/
|
|
17722
18174
|
async set(sessionId, session, ttlMs) {
|
|
17723
18175
|
const key = this.key(sessionId);
|
|
17724
|
-
|
|
18176
|
+
let value;
|
|
18177
|
+
if (this.security.enableSigning) {
|
|
18178
|
+
value = signSession(session, { secret: this.security.signingSecret });
|
|
18179
|
+
} else {
|
|
18180
|
+
value = JSON.stringify(session);
|
|
18181
|
+
}
|
|
17725
18182
|
if (ttlMs && ttlMs > 0) {
|
|
17726
18183
|
await this.redis.set(key, value, "PX", ttlMs);
|
|
17727
18184
|
} else if (session.session.expiresAt) {
|
|
@@ -17751,7 +18208,7 @@ var init_redis_session_store = __esm({
|
|
|
17751
18208
|
* Allocate a new session ID
|
|
17752
18209
|
*/
|
|
17753
18210
|
allocId() {
|
|
17754
|
-
return (0,
|
|
18211
|
+
return (0, import_crypto14.randomUUID)();
|
|
17755
18212
|
}
|
|
17756
18213
|
/**
|
|
17757
18214
|
* Disconnect from Redis (only if we created the connection)
|
|
@@ -17790,12 +18247,14 @@ var vercel_kv_session_store_exports = {};
|
|
|
17790
18247
|
__export(vercel_kv_session_store_exports, {
|
|
17791
18248
|
VercelKvSessionStore: () => VercelKvSessionStore
|
|
17792
18249
|
});
|
|
17793
|
-
var
|
|
18250
|
+
var import_crypto15, VercelKvSessionStore;
|
|
17794
18251
|
var init_vercel_kv_session_store = __esm({
|
|
17795
18252
|
"libs/sdk/src/auth/session/vercel-kv-session.store.ts"() {
|
|
17796
18253
|
"use strict";
|
|
17797
|
-
|
|
18254
|
+
import_crypto15 = require("crypto");
|
|
17798
18255
|
init_transport_session_types();
|
|
18256
|
+
init_session_crypto();
|
|
18257
|
+
init_session_rate_limiter();
|
|
17799
18258
|
VercelKvSessionStore = class {
|
|
17800
18259
|
kv = null;
|
|
17801
18260
|
connectPromise = null;
|
|
@@ -17803,11 +18262,21 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17803
18262
|
defaultTtlMs;
|
|
17804
18263
|
logger;
|
|
17805
18264
|
config;
|
|
18265
|
+
// Security features
|
|
18266
|
+
security;
|
|
18267
|
+
rateLimiter;
|
|
17806
18268
|
constructor(config, logger) {
|
|
17807
18269
|
this.config = config;
|
|
17808
18270
|
this.keyPrefix = config.keyPrefix ?? "mcp:session:";
|
|
17809
18271
|
this.defaultTtlMs = config.defaultTtlMs ?? 36e5;
|
|
17810
18272
|
this.logger = logger;
|
|
18273
|
+
this.security = ("security" in config ? config.security : void 0) ?? {};
|
|
18274
|
+
if (this.security.enableRateLimiting) {
|
|
18275
|
+
this.rateLimiter = new SessionRateLimiter({
|
|
18276
|
+
windowMs: this.security.rateLimiting?.windowMs,
|
|
18277
|
+
maxRequests: this.security.rateLimiting?.maxRequests
|
|
18278
|
+
});
|
|
18279
|
+
}
|
|
17811
18280
|
}
|
|
17812
18281
|
/**
|
|
17813
18282
|
* Connect to Vercel KV
|
|
@@ -17860,15 +18329,51 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17860
18329
|
*
|
|
17861
18330
|
* Note: Vercel KV doesn't support GETEX, so we use GET + PEXPIRE separately.
|
|
17862
18331
|
* This is slightly less atomic than Redis GETEX but sufficient for most use cases.
|
|
17863
|
-
|
|
17864
|
-
|
|
18332
|
+
*
|
|
18333
|
+
* @param sessionId - The session ID to look up
|
|
18334
|
+
* @param options - Optional parameters for rate limiting
|
|
18335
|
+
* @param options.clientIdentifier - Client identifier (e.g., IP address) for rate limiting.
|
|
18336
|
+
* When provided, rate limiting is applied per-client to prevent session enumeration.
|
|
18337
|
+
* If not provided, falls back to sessionId which provides DoS protection per-session.
|
|
18338
|
+
*/
|
|
18339
|
+
async get(sessionId, options) {
|
|
18340
|
+
if (this.rateLimiter) {
|
|
18341
|
+
const rateLimitKey = options?.clientIdentifier || sessionId;
|
|
18342
|
+
const rateLimitResult = this.rateLimiter.check(rateLimitKey);
|
|
18343
|
+
if (!rateLimitResult.allowed) {
|
|
18344
|
+
this.logger?.warn("[VercelKvSessionStore] Rate limit exceeded for session lookup", {
|
|
18345
|
+
sessionId: sessionId.slice(0, 20),
|
|
18346
|
+
clientIdentifier: options?.clientIdentifier ? options.clientIdentifier.slice(0, 20) : void 0,
|
|
18347
|
+
retryAfterMs: rateLimitResult.retryAfterMs
|
|
18348
|
+
});
|
|
18349
|
+
return null;
|
|
18350
|
+
}
|
|
18351
|
+
}
|
|
17865
18352
|
const kv = await this.ensureConnected();
|
|
17866
18353
|
const key = this.key(sessionId);
|
|
17867
18354
|
const raw = await kv.get(key);
|
|
17868
18355
|
if (!raw) return null;
|
|
17869
|
-
kv.pexpire(key, this.defaultTtlMs).catch(() =>
|
|
18356
|
+
kv.pexpire(key, this.defaultTtlMs).catch((err) => {
|
|
18357
|
+
this.logger?.warn("[VercelKvSessionStore] TTL extension failed", {
|
|
18358
|
+
sessionId: sessionId.slice(0, 20),
|
|
18359
|
+
error: err.message
|
|
18360
|
+
});
|
|
18361
|
+
});
|
|
17870
18362
|
try {
|
|
17871
|
-
|
|
18363
|
+
let parsed;
|
|
18364
|
+
const rawStr = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
18365
|
+
if (this.security.enableSigning) {
|
|
18366
|
+
parsed = verifyOrParseSession(rawStr, { secret: this.security.signingSecret });
|
|
18367
|
+
if (!parsed) {
|
|
18368
|
+
this.logger?.warn("[VercelKvSessionStore] Session signature verification failed", {
|
|
18369
|
+
sessionId: sessionId.slice(0, 20)
|
|
18370
|
+
});
|
|
18371
|
+
this.delete(sessionId).catch(() => void 0);
|
|
18372
|
+
return null;
|
|
18373
|
+
}
|
|
18374
|
+
} else {
|
|
18375
|
+
parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
18376
|
+
}
|
|
17872
18377
|
const result = storedSessionSchema.safeParse(parsed);
|
|
17873
18378
|
if (!result.success) {
|
|
17874
18379
|
this.logger?.warn("[VercelKvSessionStore] Invalid session format", {
|
|
@@ -17879,6 +18384,14 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17879
18384
|
return null;
|
|
17880
18385
|
}
|
|
17881
18386
|
const session = result.data;
|
|
18387
|
+
if (session.maxLifetimeAt && session.maxLifetimeAt < Date.now()) {
|
|
18388
|
+
this.logger?.info("[VercelKvSessionStore] Session exceeded max lifetime", {
|
|
18389
|
+
sessionId: sessionId.slice(0, 20),
|
|
18390
|
+
maxLifetimeAt: session.maxLifetimeAt
|
|
18391
|
+
});
|
|
18392
|
+
await this.delete(sessionId);
|
|
18393
|
+
return null;
|
|
18394
|
+
}
|
|
17882
18395
|
if (session.session.expiresAt && session.session.expiresAt < Date.now()) {
|
|
17883
18396
|
await this.delete(sessionId);
|
|
17884
18397
|
return null;
|
|
@@ -17886,7 +18399,12 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17886
18399
|
if (session.session.expiresAt) {
|
|
17887
18400
|
const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());
|
|
17888
18401
|
if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {
|
|
17889
|
-
kv.pexpire(key, ttlMs).catch(() =>
|
|
18402
|
+
kv.pexpire(key, ttlMs).catch((err) => {
|
|
18403
|
+
this.logger?.warn("[VercelKvSessionStore] TTL bound extension failed", {
|
|
18404
|
+
sessionId: sessionId.slice(0, 20),
|
|
18405
|
+
error: err.message
|
|
18406
|
+
});
|
|
18407
|
+
});
|
|
17890
18408
|
}
|
|
17891
18409
|
}
|
|
17892
18410
|
const updatedSession = {
|
|
@@ -17909,7 +18427,12 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17909
18427
|
async set(sessionId, session, ttlMs) {
|
|
17910
18428
|
const kv = await this.ensureConnected();
|
|
17911
18429
|
const key = this.key(sessionId);
|
|
17912
|
-
|
|
18430
|
+
let value;
|
|
18431
|
+
if (this.security.enableSigning) {
|
|
18432
|
+
value = signSession(session, { secret: this.security.signingSecret });
|
|
18433
|
+
} else {
|
|
18434
|
+
value = JSON.stringify(session);
|
|
18435
|
+
}
|
|
17913
18436
|
if (ttlMs && ttlMs > 0) {
|
|
17914
18437
|
await kv.set(key, value, { px: ttlMs });
|
|
17915
18438
|
} else if (session.session.expiresAt) {
|
|
@@ -17941,7 +18464,7 @@ var init_vercel_kv_session_store = __esm({
|
|
|
17941
18464
|
* Allocate a new session ID
|
|
17942
18465
|
*/
|
|
17943
18466
|
allocId() {
|
|
17944
|
-
return (0,
|
|
18467
|
+
return (0, import_crypto15.randomUUID)();
|
|
17945
18468
|
}
|
|
17946
18469
|
/**
|
|
17947
18470
|
* Disconnect from Vercel KV
|
|
@@ -18066,11 +18589,11 @@ var init_store = __esm({
|
|
|
18066
18589
|
});
|
|
18067
18590
|
|
|
18068
18591
|
// libs/sdk/src/transport/transport.registry.ts
|
|
18069
|
-
var
|
|
18592
|
+
var import_crypto16, TransportService;
|
|
18070
18593
|
var init_transport_registry = __esm({
|
|
18071
18594
|
"libs/sdk/src/transport/transport.registry.ts"() {
|
|
18072
18595
|
"use strict";
|
|
18073
|
-
|
|
18596
|
+
import_crypto16 = require("crypto");
|
|
18074
18597
|
init_transport_remote();
|
|
18075
18598
|
init_transport_local();
|
|
18076
18599
|
init_handle_streamable_http_flow();
|
|
@@ -18195,9 +18718,12 @@ var init_transport_registry = __esm({
|
|
|
18195
18718
|
* @param type - Transport type
|
|
18196
18719
|
* @param token - Authorization token
|
|
18197
18720
|
* @param sessionId - Session ID
|
|
18721
|
+
* @param options - Optional validation options
|
|
18722
|
+
* @param options.clientFingerprint - Client fingerprint for additional validation
|
|
18723
|
+
* @param options.warnOnFingerprintMismatch - If true, log warning on mismatch but still return session
|
|
18198
18724
|
* @returns Stored session data if exists and token matches, undefined otherwise
|
|
18199
18725
|
*/
|
|
18200
|
-
async getStoredSession(type, token, sessionId) {
|
|
18726
|
+
async getStoredSession(type, token, sessionId, options) {
|
|
18201
18727
|
if (!this.sessionStore || type !== "streamable-http") return void 0;
|
|
18202
18728
|
const tokenHash = this.sha256(token);
|
|
18203
18729
|
const stored = await this.sessionStore.get(sessionId);
|
|
@@ -18210,6 +18736,18 @@ var init_transport_registry = __esm({
|
|
|
18210
18736
|
});
|
|
18211
18737
|
return void 0;
|
|
18212
18738
|
}
|
|
18739
|
+
if (options?.clientFingerprint && stored.session.clientFingerprint) {
|
|
18740
|
+
if (stored.session.clientFingerprint !== options.clientFingerprint) {
|
|
18741
|
+
this.scope.logger.warn("[TransportService] Client fingerprint mismatch", {
|
|
18742
|
+
sessionId: sessionId.slice(0, 20),
|
|
18743
|
+
storedFingerprint: stored.session.clientFingerprint.slice(0, 8),
|
|
18744
|
+
requestFingerprint: options.clientFingerprint.slice(0, 8)
|
|
18745
|
+
});
|
|
18746
|
+
if (!options.warnOnFingerprintMismatch) {
|
|
18747
|
+
return void 0;
|
|
18748
|
+
}
|
|
18749
|
+
}
|
|
18750
|
+
}
|
|
18213
18751
|
return stored;
|
|
18214
18752
|
}
|
|
18215
18753
|
/**
|
|
@@ -18272,6 +18810,9 @@ var init_transport_registry = __esm({
|
|
|
18272
18810
|
}
|
|
18273
18811
|
});
|
|
18274
18812
|
await transporter.ready();
|
|
18813
|
+
if (storedSession.initialized !== false) {
|
|
18814
|
+
transporter.markAsInitialized();
|
|
18815
|
+
}
|
|
18275
18816
|
this.insertLocal(key, transporter);
|
|
18276
18817
|
if (sessionStore) {
|
|
18277
18818
|
const updatedSession = {
|
|
@@ -18338,7 +18879,9 @@ var init_transport_registry = __esm({
|
|
|
18338
18879
|
},
|
|
18339
18880
|
authorizationId: key.tokenHash,
|
|
18340
18881
|
createdAt: Date.now(),
|
|
18341
|
-
lastAccessedAt: Date.now()
|
|
18882
|
+
lastAccessedAt: Date.now(),
|
|
18883
|
+
initialized: true
|
|
18884
|
+
// Mark as initialized for session recreation
|
|
18342
18885
|
};
|
|
18343
18886
|
sessionStore.set(sessionId, storedSession, persistenceConfig?.defaultTtlMs).catch((err) => {
|
|
18344
18887
|
this.scope.logger.warn("[TransportService] Failed to persist session to Redis", {
|
|
@@ -18444,7 +18987,7 @@ var init_transport_registry = __esm({
|
|
|
18444
18987
|
}
|
|
18445
18988
|
/* --------------------------------- internals -------------------------------- */
|
|
18446
18989
|
sha256(value) {
|
|
18447
|
-
return (0,
|
|
18990
|
+
return (0, import_crypto16.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
18448
18991
|
}
|
|
18449
18992
|
/**
|
|
18450
18993
|
* Create a history key from components.
|
|
@@ -19927,6 +20470,54 @@ var init_front_mcp2 = __esm({
|
|
|
19927
20470
|
}
|
|
19928
20471
|
});
|
|
19929
20472
|
|
|
20473
|
+
// libs/sdk/src/front-mcp/serverless-handler.ts
|
|
20474
|
+
var serverless_handler_exports = {};
|
|
20475
|
+
__export(serverless_handler_exports, {
|
|
20476
|
+
getServerlessHandler: () => getServerlessHandler,
|
|
20477
|
+
getServerlessHandlerAsync: () => getServerlessHandlerAsync,
|
|
20478
|
+
setServerlessHandler: () => setServerlessHandler,
|
|
20479
|
+
setServerlessHandlerError: () => setServerlessHandlerError,
|
|
20480
|
+
setServerlessHandlerPromise: () => setServerlessHandlerPromise
|
|
20481
|
+
});
|
|
20482
|
+
function setServerlessHandler(handler) {
|
|
20483
|
+
globalHandler = handler;
|
|
20484
|
+
}
|
|
20485
|
+
function setServerlessHandlerPromise(promise) {
|
|
20486
|
+
globalHandlerPromise = promise;
|
|
20487
|
+
}
|
|
20488
|
+
function setServerlessHandlerError(error) {
|
|
20489
|
+
globalHandlerError = error;
|
|
20490
|
+
}
|
|
20491
|
+
function getServerlessHandler() {
|
|
20492
|
+
if (globalHandlerError) {
|
|
20493
|
+
throw globalHandlerError;
|
|
20494
|
+
}
|
|
20495
|
+
return globalHandler;
|
|
20496
|
+
}
|
|
20497
|
+
async function getServerlessHandlerAsync() {
|
|
20498
|
+
if (globalHandlerError) {
|
|
20499
|
+
throw globalHandlerError;
|
|
20500
|
+
}
|
|
20501
|
+
if (globalHandlerPromise) {
|
|
20502
|
+
return globalHandlerPromise;
|
|
20503
|
+
}
|
|
20504
|
+
if (!globalHandler) {
|
|
20505
|
+
throw new Error(
|
|
20506
|
+
"Serverless handler not initialized. Ensure @FrontMcp decorator ran and FRONTMCP_SERVERLESS=1 is set."
|
|
20507
|
+
);
|
|
20508
|
+
}
|
|
20509
|
+
return globalHandler;
|
|
20510
|
+
}
|
|
20511
|
+
var globalHandler, globalHandlerPromise, globalHandlerError;
|
|
20512
|
+
var init_serverless_handler = __esm({
|
|
20513
|
+
"libs/sdk/src/front-mcp/serverless-handler.ts"() {
|
|
20514
|
+
"use strict";
|
|
20515
|
+
globalHandler = null;
|
|
20516
|
+
globalHandlerPromise = null;
|
|
20517
|
+
globalHandlerError = null;
|
|
20518
|
+
}
|
|
20519
|
+
});
|
|
20520
|
+
|
|
19930
20521
|
// libs/sdk/src/common/decorators/front-mcp.decorator.ts
|
|
19931
20522
|
function getFrontMcpInstance() {
|
|
19932
20523
|
if (!_FrontMcpInstance) {
|
|
@@ -19937,6 +20528,15 @@ function getFrontMcpInstance() {
|
|
|
19937
20528
|
}
|
|
19938
20529
|
return _FrontMcpInstance;
|
|
19939
20530
|
}
|
|
20531
|
+
function getServerlessHandlerFns() {
|
|
20532
|
+
if (!_serverlessHandlerFns) {
|
|
20533
|
+
_serverlessHandlerFns = (init_serverless_handler(), __toCommonJS(serverless_handler_exports));
|
|
20534
|
+
}
|
|
20535
|
+
if (!_serverlessHandlerFns) {
|
|
20536
|
+
throw new InternalMcpError("Serverless handler functions not found", "MODULE_LOAD_FAILED");
|
|
20537
|
+
}
|
|
20538
|
+
return _serverlessHandlerFns;
|
|
20539
|
+
}
|
|
19940
20540
|
function FrontMcp(providedMetadata) {
|
|
19941
20541
|
return (target) => {
|
|
19942
20542
|
const migratedMetadata = applyMigration(providedMetadata);
|
|
@@ -19977,18 +20577,8 @@ ${JSON.stringify(
|
|
|
19977
20577
|
}
|
|
19978
20578
|
const isServerless = typeof process !== "undefined" && process.env?.["FRONTMCP_SERVERLESS"] === "1";
|
|
19979
20579
|
if (isServerless) {
|
|
19980
|
-
const
|
|
19981
|
-
|
|
19982
|
-
setServerlessHandler: setServerlessHandler2,
|
|
19983
|
-
setServerlessHandlerPromise: setServerlessHandlerPromise2,
|
|
19984
|
-
setServerlessHandlerError: setServerlessHandlerError2
|
|
19985
|
-
} = require("@frontmcp/sdk");
|
|
19986
|
-
if (!ServerlessInstance) {
|
|
19987
|
-
throw new InternalMcpError(
|
|
19988
|
-
"@frontmcp/sdk version mismatch, make sure you have the same version for all @frontmcp/* packages",
|
|
19989
|
-
"SDK_VERSION_MISMATCH"
|
|
19990
|
-
);
|
|
19991
|
-
}
|
|
20580
|
+
const ServerlessInstance = getFrontMcpInstance();
|
|
20581
|
+
const { setServerlessHandler: setServerlessHandler2, setServerlessHandlerPromise: setServerlessHandlerPromise2, setServerlessHandlerError: setServerlessHandlerError2 } = getServerlessHandlerFns();
|
|
19992
20582
|
const handlerPromise = ServerlessInstance.createHandler(metadata);
|
|
19993
20583
|
setServerlessHandlerPromise2(handlerPromise);
|
|
19994
20584
|
handlerPromise.then(setServerlessHandler2).catch((err) => {
|
|
@@ -20001,7 +20591,7 @@ ${JSON.stringify(
|
|
|
20001
20591
|
}
|
|
20002
20592
|
};
|
|
20003
20593
|
}
|
|
20004
|
-
var import_reflect_metadata20, _FrontMcpInstance;
|
|
20594
|
+
var import_reflect_metadata20, _FrontMcpInstance, _serverlessHandlerFns;
|
|
20005
20595
|
var init_front_mcp_decorator = __esm({
|
|
20006
20596
|
"libs/sdk/src/common/decorators/front-mcp.decorator.ts"() {
|
|
20007
20597
|
"use strict";
|
|
@@ -20011,6 +20601,7 @@ var init_front_mcp_decorator = __esm({
|
|
|
20011
20601
|
init_migrate();
|
|
20012
20602
|
init_mcp_error();
|
|
20013
20603
|
_FrontMcpInstance = null;
|
|
20604
|
+
_serverlessHandlerFns = null;
|
|
20014
20605
|
}
|
|
20015
20606
|
});
|
|
20016
20607
|
|
|
@@ -20493,11 +21084,11 @@ var init_flow_interface = __esm({
|
|
|
20493
21084
|
});
|
|
20494
21085
|
|
|
20495
21086
|
// libs/sdk/src/common/interfaces/execution-context.interface.ts
|
|
20496
|
-
var
|
|
21087
|
+
var import_crypto17, ExecutionContextBase;
|
|
20497
21088
|
var init_execution_context_interface = __esm({
|
|
20498
21089
|
"libs/sdk/src/common/interfaces/execution-context.interface.ts"() {
|
|
20499
21090
|
"use strict";
|
|
20500
|
-
|
|
21091
|
+
import_crypto17 = require("crypto");
|
|
20501
21092
|
init_flow_interface();
|
|
20502
21093
|
init_context();
|
|
20503
21094
|
init_mcp_error();
|
|
@@ -20516,7 +21107,7 @@ var init_execution_context_interface = __esm({
|
|
|
20516
21107
|
_error;
|
|
20517
21108
|
constructor(args) {
|
|
20518
21109
|
const { providers, logger, authInfo } = args;
|
|
20519
|
-
this.runId = (0,
|
|
21110
|
+
this.runId = (0, import_crypto17.randomUUID)();
|
|
20520
21111
|
this.providers = providers;
|
|
20521
21112
|
this.logger = logger;
|
|
20522
21113
|
this._authInfo = authInfo;
|
|
@@ -20889,11 +21480,11 @@ var init_resource_interface = __esm({
|
|
|
20889
21480
|
});
|
|
20890
21481
|
|
|
20891
21482
|
// libs/sdk/src/common/interfaces/prompt.interface.ts
|
|
20892
|
-
var
|
|
21483
|
+
var import_crypto18, PromptContext;
|
|
20893
21484
|
var init_prompt_interface = __esm({
|
|
20894
21485
|
"libs/sdk/src/common/interfaces/prompt.interface.ts"() {
|
|
20895
21486
|
"use strict";
|
|
20896
|
-
|
|
21487
|
+
import_crypto18 = require("crypto");
|
|
20897
21488
|
init_flow_interface();
|
|
20898
21489
|
PromptContext = class {
|
|
20899
21490
|
providers;
|
|
@@ -20913,7 +21504,7 @@ var init_prompt_interface = __esm({
|
|
|
20913
21504
|
_outputHistory = [];
|
|
20914
21505
|
constructor(ctorArgs) {
|
|
20915
21506
|
const { metadata, args, providers, logger, authInfo } = ctorArgs;
|
|
20916
|
-
this.runId = (0,
|
|
21507
|
+
this.runId = (0, import_crypto18.randomUUID)();
|
|
20917
21508
|
this.promptName = metadata.name;
|
|
20918
21509
|
this.promptId = metadata.name;
|
|
20919
21510
|
this.metadata = metadata;
|
|
@@ -22619,42 +23210,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
22619
23210
|
var import_reflect_metadata31 = require("reflect-metadata");
|
|
22620
23211
|
init_common();
|
|
22621
23212
|
init_front_mcp2();
|
|
22622
|
-
|
|
22623
|
-
// libs/sdk/src/front-mcp/serverless-handler.ts
|
|
22624
|
-
var globalHandler = null;
|
|
22625
|
-
var globalHandlerPromise = null;
|
|
22626
|
-
var globalHandlerError = null;
|
|
22627
|
-
function setServerlessHandler(handler) {
|
|
22628
|
-
globalHandler = handler;
|
|
22629
|
-
}
|
|
22630
|
-
function setServerlessHandlerPromise(promise) {
|
|
22631
|
-
globalHandlerPromise = promise;
|
|
22632
|
-
}
|
|
22633
|
-
function setServerlessHandlerError(error) {
|
|
22634
|
-
globalHandlerError = error;
|
|
22635
|
-
}
|
|
22636
|
-
function getServerlessHandler() {
|
|
22637
|
-
if (globalHandlerError) {
|
|
22638
|
-
throw globalHandlerError;
|
|
22639
|
-
}
|
|
22640
|
-
return globalHandler;
|
|
22641
|
-
}
|
|
22642
|
-
async function getServerlessHandlerAsync() {
|
|
22643
|
-
if (globalHandlerError) {
|
|
22644
|
-
throw globalHandlerError;
|
|
22645
|
-
}
|
|
22646
|
-
if (globalHandlerPromise) {
|
|
22647
|
-
return globalHandlerPromise;
|
|
22648
|
-
}
|
|
22649
|
-
if (!globalHandler) {
|
|
22650
|
-
throw new Error(
|
|
22651
|
-
"Serverless handler not initialized. Ensure @FrontMcp decorator ran and FRONTMCP_SERVERLESS=1 is set."
|
|
22652
|
-
);
|
|
22653
|
-
}
|
|
22654
|
-
return globalHandler;
|
|
22655
|
-
}
|
|
22656
|
-
|
|
22657
|
-
// libs/sdk/src/index.ts
|
|
23213
|
+
init_serverless_handler();
|
|
22658
23214
|
init_common();
|
|
22659
23215
|
init_errors();
|
|
22660
23216
|
init_context();
|