@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/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, statelessSessionJwtPayloadSchema, encryptedBlobSchema, storedSessionSchema, redisConfigSchema, statefulStorageSchema, sessionStorageConfigSchema;
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
- init_legacy_sse_tranporter();
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 scopePath = this.scope.fullPath;
16874
- const transport = new SSEServerTransport(`${scopePath}/message`, res, {
16963
+ const transport = new RecreateableSSEServerTransport(`${this.scope.fullPath}/message`, res, {
16875
16964
  sessionId
16876
16965
  });
16877
- transport.onerror = (error) => {
16878
- console.error("SSE error:", error instanceof Error ? error.message : "Unknown error");
16879
- };
16880
- transport.onclose = this.destroy.bind(this);
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/transport.streamable-http.adapter.ts
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 StreamableHTTPServerTransport({
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
- async get(sessionId) {
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
- const parsed = JSON.parse(raw);
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(() => void 0);
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
- const value = JSON.stringify(session);
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
- async get(sessionId) {
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(() => void 0);
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
- const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
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(() => void 0);
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
- const value = JSON.stringify(session);
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
- FrontMcpInstance: ServerlessInstance,
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
- import "reflect-metadata";
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");