portkey-admin-mcp 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/server.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { pathToFileURL } from "node:url";
5
5
 
6
6
  // src/lib/http-app.ts
7
- import { randomUUID as randomUUID2 } from "node:crypto";
7
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
8
8
  import { readFileSync as readFileSync2 } from "node:fs";
9
9
  import {
10
10
  createServer as createHttpServer
@@ -22,6 +22,7 @@ import {
22
22
  } from "@modelcontextprotocol/sdk/types.js";
23
23
  import cors from "cors";
24
24
  import express from "express";
25
+ import { rateLimit as expressRateLimit } from "express-rate-limit";
25
26
  import helmet from "helmet";
26
27
 
27
28
  // src/services/base.service.ts
@@ -136,6 +137,7 @@ var Logger = {
136
137
  // src/services/base.service.ts
137
138
  var DEFAULT_BASE_URL = "https://api.portkey.ai/v1";
138
139
  var PRIVATE_BASE_URL_OVERRIDE_HINT = "Set PORTKEY_ALLOW_PRIVATE_BASE_URL=true to allow self-hosted gateways on loopback or private networks.";
140
+ var INSECURE_HTTP_OVERRIDE_HINT = "Set PORTKEY_ALLOW_INSECURE_HTTP=true only for a trusted self-hosted gateway when TLS is unavailable.";
139
141
  function isPrivateOrLocalHost(hostname) {
140
142
  const host = hostname.trim().toLowerCase().replace(/^\[|\]$/g, "");
141
143
  if (host === "localhost" || host.endsWith(".localhost")) {
@@ -202,6 +204,14 @@ function validateUrl(url) {
202
204
  `Refusing to use a loopback or private-network PORTKEY_BASE_URL host: ${parsed.hostname}. ${PRIVATE_BASE_URL_OVERRIDE_HINT}`
203
205
  );
204
206
  }
207
+ const allowInsecureHttp = /^(1|true|yes)$/i.test(
208
+ process.env.PORTKEY_ALLOW_INSECURE_HTTP?.trim() ?? ""
209
+ );
210
+ if (parsed.protocol === "http:" && !allowInsecureHttp) {
211
+ throw new Error(
212
+ `Refusing insecure HTTP PORTKEY_BASE_URL: ${parsed.origin}. ${INSECURE_HTTP_OVERRIDE_HINT}`
213
+ );
214
+ }
205
215
  }
206
216
  var BaseService = class {
207
217
  apiKey;
@@ -213,13 +223,9 @@ var BaseService = class {
213
223
  throw new Error("PORTKEY_API_KEY environment variable is not set");
214
224
  }
215
225
  this.apiKey = apiKey;
216
- if (baseUrlOverride !== void 0) {
217
- this.baseUrl = baseUrlOverride.replace(/\/+$/, "");
218
- } else {
219
- const baseUrl = process.env.PORTKEY_BASE_URL ?? DEFAULT_BASE_URL;
220
- validateUrl(baseUrl);
221
- this.baseUrl = baseUrl.replace(/\/+$/, "");
222
- }
226
+ const baseUrl = baseUrlOverride ?? process.env.PORTKEY_BASE_URL ?? DEFAULT_BASE_URL;
227
+ validateUrl(baseUrl);
228
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
223
229
  }
224
230
  encodePathSegment(value) {
225
231
  return encodeURIComponent(value);
@@ -255,6 +261,7 @@ var BaseService = class {
255
261
  try {
256
262
  const response = await fetchWithTimeout(url, {
257
263
  method,
264
+ redirect: "manual",
258
265
  headers: this.buildHeaders(method),
259
266
  body: this.serializeBody(options.body),
260
267
  timeout: this.timeout
@@ -8425,6 +8432,11 @@ function registerAllTools(server, service, options = {}) {
8425
8432
  // src/lib/auth.ts
8426
8433
  import crypto2 from "node:crypto";
8427
8434
  import { createRemoteJWKSet, jwtVerify } from "jose";
8435
+ function getPrincipalOwnerKey(principal) {
8436
+ return crypto2.createHash("sha256").update(principal.id, "utf8").digest("hex");
8437
+ }
8438
+ var ClerkAuthorizationError = class extends Error {
8439
+ };
8428
8440
  var AUTH_SCHEMES = {
8429
8441
  bearer: "Bearer"
8430
8442
  };
@@ -8471,6 +8483,12 @@ function getHttpAuthConfigFromEnv() {
8471
8483
  process.env.CLERK_JWKS_URL?.trim(),
8472
8484
  issuer
8473
8485
  );
8486
+ const allowedSubjects = parseCsv(process.env.CLERK_ALLOWED_SUBJECTS);
8487
+ const allowedOrganizationIds = parseCsv(
8488
+ process.env.CLERK_ALLOWED_ORGANIZATION_IDS
8489
+ );
8490
+ const allowedRoles = parseCsv(process.env.CLERK_ALLOWED_ROLES);
8491
+ const requiredPermissions = parseCsv(process.env.CLERK_REQUIRED_PERMISSIONS);
8474
8492
  if (mode === "bearer" && !bearerToken) {
8475
8493
  throw new Error("MCP_AUTH_MODE=bearer requires MCP_AUTH_TOKEN to be set");
8476
8494
  }
@@ -8502,13 +8520,22 @@ function getHttpAuthConfigFromEnv() {
8502
8520
  `MCP_AUTH_MODE=clerk configuration error (${issues.join("; ")})`
8503
8521
  );
8504
8522
  }
8523
+ if (!allowedSubjects && !allowedOrganizationIds && !allowedRoles && !requiredPermissions) {
8524
+ throw new Error(
8525
+ "MCP_AUTH_MODE=clerk requires an explicit authorization policy. Set at least one of CLERK_ALLOWED_SUBJECTS, CLERK_ALLOWED_ORGANIZATION_IDS, CLERK_ALLOWED_ROLES, or CLERK_REQUIRED_PERMISSIONS."
8526
+ );
8527
+ }
8505
8528
  }
8506
8529
  return {
8507
8530
  mode,
8508
8531
  bearerToken,
8509
8532
  jwksUrl,
8510
8533
  issuer,
8511
- audience
8534
+ audience,
8535
+ allowedSubjects,
8536
+ allowedOrganizationIds,
8537
+ allowedRoles,
8538
+ requiredPermissions
8512
8539
  };
8513
8540
  }
8514
8541
  var HTTP_AUTH_CONFIG = getHttpAuthConfigFromEnv();
@@ -8543,6 +8570,65 @@ function timingSafeEqual(a, b) {
8543
8570
  return crypto2.timingSafeEqual(left, right);
8544
8571
  }
8545
8572
  var jwksCache = /* @__PURE__ */ new Map();
8573
+ function asString(value) {
8574
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
8575
+ }
8576
+ function asStringArray(value) {
8577
+ if (!Array.isArray(value)) {
8578
+ return [];
8579
+ }
8580
+ return value.flatMap((item) => {
8581
+ const normalized = asString(item);
8582
+ return normalized ? [normalized] : [];
8583
+ });
8584
+ }
8585
+ function unique(values) {
8586
+ return [...new Set(values)];
8587
+ }
8588
+ function authorizeClerkClaims(payload, config) {
8589
+ const subject = asString(payload.sub);
8590
+ if (!subject) {
8591
+ throw new ClerkAuthorizationError("Clerk token is missing a subject");
8592
+ }
8593
+ const organization = typeof payload.o === "object" && payload.o !== null ? payload.o : void 0;
8594
+ const organizationId = asString(payload.org_id) ?? asString(organization?.id);
8595
+ const roles = unique(
8596
+ [
8597
+ asString(payload.org_role),
8598
+ asString(organization?.rol),
8599
+ ...asStringArray(payload.roles)
8600
+ ].filter((role) => Boolean(role))
8601
+ );
8602
+ const permissions = unique([
8603
+ ...asStringArray(payload.org_permissions),
8604
+ ...asStringArray(organization?.per),
8605
+ ...asStringArray(payload.permissions)
8606
+ ]);
8607
+ if (config.allowedSubjects && !config.allowedSubjects.includes(subject)) {
8608
+ throw new ClerkAuthorizationError("Clerk subject is not authorized");
8609
+ }
8610
+ if (config.allowedOrganizationIds && (!organizationId || !config.allowedOrganizationIds.includes(organizationId))) {
8611
+ throw new ClerkAuthorizationError("Clerk organization is not authorized");
8612
+ }
8613
+ if (config.allowedRoles && !roles.some((role) => config.allowedRoles?.includes(role))) {
8614
+ throw new ClerkAuthorizationError("Clerk role is not authorized");
8615
+ }
8616
+ if (config.requiredPermissions && !config.requiredPermissions.every(
8617
+ (permission) => permissions.includes(permission)
8618
+ )) {
8619
+ throw new ClerkAuthorizationError(
8620
+ "Clerk token is missing a required permission"
8621
+ );
8622
+ }
8623
+ return {
8624
+ id: `clerk:${config.issuer}:${subject}`,
8625
+ mode: "clerk",
8626
+ subject,
8627
+ organizationId,
8628
+ roles,
8629
+ permissions
8630
+ };
8631
+ }
8546
8632
  async function verifyClerkToken(token, config) {
8547
8633
  if (!config.jwksUrl) {
8548
8634
  throw new Error("Missing Clerk JWKS URL configuration");
@@ -8558,11 +8644,12 @@ async function verifyClerkToken(token, config) {
8558
8644
  jwks = createRemoteJWKSet(new URL(config.jwksUrl));
8559
8645
  jwksCache.set(config.jwksUrl, jwks);
8560
8646
  }
8561
- await jwtVerify(token, jwks, {
8647
+ const { payload } = await jwtVerify(token, jwks, {
8562
8648
  issuer: config.issuer,
8563
8649
  audience: config.audience,
8564
8650
  clockTolerance: "5s"
8565
8651
  });
8652
+ return authorizeClerkClaims(payload, config);
8566
8653
  }
8567
8654
  async function mcpAuthMiddleware(req, res, next) {
8568
8655
  if (!req.path.startsWith("/mcp")) {
@@ -8571,6 +8658,12 @@ async function mcpAuthMiddleware(req, res, next) {
8571
8658
  }
8572
8659
  const config = getHttpAuthConfig();
8573
8660
  if (config.mode === "none") {
8661
+ res.locals.authPrincipal = {
8662
+ id: "anonymous",
8663
+ mode: "none",
8664
+ roles: [],
8665
+ permissions: []
8666
+ };
8574
8667
  next();
8575
8668
  return;
8576
8669
  }
@@ -8582,13 +8675,21 @@ async function mcpAuthMiddleware(req, res, next) {
8582
8675
  return;
8583
8676
  }
8584
8677
  try {
8678
+ let principal;
8585
8679
  if (config.mode === "bearer") {
8586
8680
  if (!config.bearerToken || !timingSafeEqual(token, config.bearerToken)) {
8587
8681
  throw new Error("Bearer token mismatch");
8588
8682
  }
8683
+ principal = {
8684
+ id: `bearer:${crypto2.createHash("sha256").update(token, "utf8").digest("hex")}`,
8685
+ mode: "bearer",
8686
+ roles: [],
8687
+ permissions: []
8688
+ };
8589
8689
  } else {
8590
- await verifyClerkToken(token, config);
8690
+ principal = await verifyClerkToken(token, config);
8591
8691
  }
8692
+ res.locals.authPrincipal = principal;
8592
8693
  next();
8593
8694
  } catch (error) {
8594
8695
  Logger.warn("MCP auth failed", {
@@ -8599,6 +8700,10 @@ async function mcpAuthMiddleware(req, res, next) {
8599
8700
  reason: error instanceof Error ? error.message : "Unknown error"
8600
8701
  }
8601
8702
  });
8703
+ if (error instanceof ClerkAuthorizationError) {
8704
+ res.status(403).json({ error: "Forbidden: Principal is not authorized" });
8705
+ return;
8706
+ }
8602
8707
  res.status(401).json({ error: "Unauthorized: Token validation failed" });
8603
8708
  }
8604
8709
  }
@@ -8669,7 +8774,7 @@ function getServerConfig() {
8669
8774
  `Invalid MCP_MAX_SESSIONS value: ${maxSessionsStr}. Must be a positive integer`
8670
8775
  );
8671
8776
  }
8672
- const eventStoreTtlStr = (process.env.MCP_EVENT_TTL_SECONDS || "3600").trim();
8777
+ const eventStoreTtlStr = (process.env.MCP_EVENT_TTL_SECONDS || "300").trim();
8673
8778
  const eventStoreTtlSeconds = parseStrictInteger(eventStoreTtlStr);
8674
8779
  if (Number.isNaN(eventStoreTtlSeconds) || eventStoreTtlSeconds <= 0) {
8675
8780
  throw new Error(
@@ -8682,6 +8787,38 @@ function getServerConfig() {
8682
8787
  "MCP_EVENT_STORE=redis requires MCP_REDIS_URL (or REDIS_URL) to be set"
8683
8788
  );
8684
8789
  }
8790
+ if (eventStoreMode === "redis" && redisUrl) {
8791
+ let parsedRedisUrl;
8792
+ try {
8793
+ parsedRedisUrl = new URL(redisUrl);
8794
+ } catch {
8795
+ throw new Error(
8796
+ "MCP_REDIS_URL must be a valid redis:// or rediss:// URL"
8797
+ );
8798
+ }
8799
+ if (!["redis:", "rediss:"].includes(parsedRedisUrl.protocol)) {
8800
+ throw new Error("MCP_REDIS_URL must use redis:// or rediss://");
8801
+ }
8802
+ if (process.env.NODE_ENV?.trim().toLowerCase() === "production" && parsedRedisUrl.protocol !== "rediss:") {
8803
+ throw new Error("MCP_REDIS_URL must use rediss:// in production");
8804
+ }
8805
+ }
8806
+ const rawEventEncryptionKey = process.env.MCP_EVENT_ENCRYPTION_KEY?.trim();
8807
+ let eventEncryptionKey;
8808
+ if (eventStoreMode === "redis") {
8809
+ if (!rawEventEncryptionKey) {
8810
+ throw new Error(
8811
+ "MCP_EVENT_ENCRYPTION_KEY must be base64-encoded 32-byte key when MCP_EVENT_STORE=redis"
8812
+ );
8813
+ }
8814
+ const decodedKey = Buffer.from(rawEventEncryptionKey, "base64");
8815
+ if (decodedKey.length !== 32 || decodedKey.toString("base64") !== rawEventEncryptionKey) {
8816
+ throw new Error(
8817
+ "MCP_EVENT_ENCRYPTION_KEY must be base64-encoded 32-byte key when MCP_EVENT_STORE=redis"
8818
+ );
8819
+ }
8820
+ eventEncryptionKey = decodedKey;
8821
+ }
8685
8822
  const redisKeyPrefix = process.env.MCP_REDIS_KEY_PREFIX?.trim() || "mcp:event-store";
8686
8823
  const keyPath = process.env.MCP_TLS_KEY_PATH?.trim() || void 0;
8687
8824
  const certPath = process.env.MCP_TLS_CERT_PATH?.trim() || void 0;
@@ -8702,7 +8839,8 @@ function getServerConfig() {
8702
8839
  mode: eventStoreMode,
8703
8840
  ttlSeconds: eventStoreTtlSeconds,
8704
8841
  redisUrl,
8705
- redisKeyPrefix
8842
+ redisKeyPrefix,
8843
+ encryptionKey: eventEncryptionKey
8706
8844
  },
8707
8845
  protocol,
8708
8846
  port,
@@ -8720,8 +8858,69 @@ function getServerConfig() {
8720
8858
  }
8721
8859
 
8722
8860
  // src/lib/event-store.ts
8723
- import { randomUUID } from "node:crypto";
8861
+ import {
8862
+ createCipheriv,
8863
+ createDecipheriv,
8864
+ randomBytes as randomBytes2,
8865
+ randomUUID
8866
+ } from "node:crypto";
8724
8867
  var EVENT_STORE_CLEANUP_INTERVAL_MS = 3e4;
8868
+ var EVENT_PAYLOAD_ENCRYPTION_VERSION = "v1";
8869
+ var EVENT_PAYLOAD_AAD = Buffer.from(
8870
+ "portkey-admin-mcp:event-payload:v1",
8871
+ "utf8"
8872
+ );
8873
+ function assertEncryptionKey(key) {
8874
+ if (key.length !== 32) {
8875
+ throw new Error("Event payload encryption requires a 32-byte key");
8876
+ }
8877
+ }
8878
+ function encryptEventPayload(message, key) {
8879
+ assertEncryptionKey(key);
8880
+ const iv = randomBytes2(12);
8881
+ const cipher = createCipheriv("aes-256-gcm", key, iv, {
8882
+ authTagLength: 16
8883
+ });
8884
+ cipher.setAAD(EVENT_PAYLOAD_AAD);
8885
+ const ciphertext = Buffer.concat([
8886
+ cipher.update(JSON.stringify(message), "utf8"),
8887
+ cipher.final()
8888
+ ]);
8889
+ const authenticationTag = cipher.getAuthTag();
8890
+ return [
8891
+ EVENT_PAYLOAD_ENCRYPTION_VERSION,
8892
+ iv.toString("base64url"),
8893
+ authenticationTag.toString("base64url"),
8894
+ ciphertext.toString("base64url")
8895
+ ].join(".");
8896
+ }
8897
+ function decryptEventPayload(encoded, key) {
8898
+ assertEncryptionKey(key);
8899
+ const [version, encodedIv, encodedTag, encodedCiphertext, ...rest] = encoded.split(".");
8900
+ if (version !== EVENT_PAYLOAD_ENCRYPTION_VERSION || !encodedIv || !encodedTag || !encodedCiphertext || rest.length > 0) {
8901
+ throw new Error("Invalid encrypted event payload envelope");
8902
+ }
8903
+ const iv = Buffer.from(encodedIv, "base64url");
8904
+ const authenticationTag = Buffer.from(encodedTag, "base64url");
8905
+ if (iv.length !== 12) {
8906
+ throw new Error("Encrypted event payload must use a 12-byte nonce");
8907
+ }
8908
+ if (authenticationTag.length !== 16) {
8909
+ throw new Error(
8910
+ "Encrypted event payload must use a 16-byte authentication tag"
8911
+ );
8912
+ }
8913
+ const decipher = createDecipheriv("aes-256-gcm", key, iv, {
8914
+ authTagLength: 16
8915
+ });
8916
+ decipher.setAAD(EVENT_PAYLOAD_AAD);
8917
+ decipher.setAuthTag(authenticationTag);
8918
+ const plaintext = Buffer.concat([
8919
+ decipher.update(Buffer.from(encodedCiphertext, "base64url")),
8920
+ decipher.final()
8921
+ ]);
8922
+ return JSON.parse(plaintext.toString("utf8"));
8923
+ }
8725
8924
  var SAFE_EVENT_ID_PATTERN = /^[\w-]{1,128}$/;
8726
8925
  function assertSafeRedisId(id, kind) {
8727
8926
  if (!SAFE_EVENT_ID_PATTERN.test(id)) {
@@ -8731,36 +8930,47 @@ function assertSafeRedisId(id, kind) {
8731
8930
  }
8732
8931
  return id;
8733
8932
  }
8734
- var InMemoryEventStore = class {
8933
+ var InMemoryEventStore = class _InMemoryEventStore {
8735
8934
  ttlMs;
8736
- events = /* @__PURE__ */ new Map();
8737
- streamEvents = /* @__PURE__ */ new Map();
8738
- activeReplayStreams = /* @__PURE__ */ new Set();
8739
- lastCleanupAt = 0;
8740
- constructor(ttlSeconds) {
8935
+ ownerKey;
8936
+ state;
8937
+ constructor(ttlSeconds, ownerKey, state = {
8938
+ events: /* @__PURE__ */ new Map(),
8939
+ streamEvents: /* @__PURE__ */ new Map(),
8940
+ activeReplayStreams: /* @__PURE__ */ new Set(),
8941
+ lastCleanupAt: 0
8942
+ }) {
8741
8943
  this.ttlMs = ttlSeconds * 1e3;
8944
+ this.ownerKey = ownerKey;
8945
+ this.state = state;
8946
+ }
8947
+ forOwner(ownerKey) {
8948
+ return new _InMemoryEventStore(this.ttlMs / 1e3, ownerKey, this.state);
8742
8949
  }
8743
- removeEvent(eventId, event = this.events.get(eventId)) {
8950
+ removeEvent(eventId, event = this.state.events.get(eventId)) {
8744
8951
  if (!event) {
8745
8952
  return;
8746
8953
  }
8747
- this.events.delete(eventId);
8748
- const eventIds = this.streamEvents.get(event.streamId);
8954
+ this.state.events.delete(eventId);
8955
+ const eventIds = this.state.streamEvents.get(event.streamId);
8749
8956
  if (!eventIds) {
8750
8957
  return;
8751
8958
  }
8752
8959
  const filtered = eventIds.filter((candidate) => candidate !== eventId);
8753
8960
  if (filtered.length === 0) {
8754
- this.streamEvents.delete(event.streamId);
8961
+ this.state.streamEvents.delete(event.streamId);
8755
8962
  } else if (filtered.length !== eventIds.length) {
8756
- this.streamEvents.set(event.streamId, filtered);
8963
+ this.state.streamEvents.set(event.streamId, filtered);
8757
8964
  }
8758
8965
  }
8759
8966
  getEventIfUnexpired(eventId, now = Date.now()) {
8760
- const event = this.events.get(eventId);
8967
+ const event = this.state.events.get(eventId);
8761
8968
  if (!event) {
8762
8969
  return void 0;
8763
8970
  }
8971
+ if (event.ownerKey !== this.ownerKey) {
8972
+ return void 0;
8973
+ }
8764
8974
  if (event.expiresAt > now) {
8765
8975
  return event;
8766
8976
  }
@@ -8768,38 +8978,41 @@ var InMemoryEventStore = class {
8768
8978
  return void 0;
8769
8979
  }
8770
8980
  cleanupExpired(now = Date.now()) {
8771
- for (const [eventId, event] of this.events.entries()) {
8981
+ for (const [eventId, event] of this.state.events.entries()) {
8772
8982
  if (event.expiresAt <= now) {
8773
- this.events.delete(eventId);
8983
+ this.state.events.delete(eventId);
8774
8984
  }
8775
8985
  }
8776
- for (const [streamId, eventIds] of this.streamEvents.entries()) {
8777
- const filtered = eventIds.filter((eventId) => this.events.has(eventId));
8986
+ for (const [streamId, eventIds] of this.state.streamEvents.entries()) {
8987
+ const filtered = eventIds.filter(
8988
+ (eventId) => this.state.events.has(eventId)
8989
+ );
8778
8990
  if (filtered.length === 0) {
8779
- this.streamEvents.delete(streamId);
8991
+ this.state.streamEvents.delete(streamId);
8780
8992
  } else if (filtered.length !== eventIds.length) {
8781
- this.streamEvents.set(streamId, filtered);
8993
+ this.state.streamEvents.set(streamId, filtered);
8782
8994
  }
8783
8995
  }
8784
8996
  }
8785
8997
  maybeCleanupExpired(now = Date.now()) {
8786
- if (now - this.lastCleanupAt < EVENT_STORE_CLEANUP_INTERVAL_MS) {
8998
+ if (now - this.state.lastCleanupAt < EVENT_STORE_CLEANUP_INTERVAL_MS) {
8787
8999
  return;
8788
9000
  }
8789
9001
  this.cleanupExpired(now);
8790
- this.lastCleanupAt = now;
9002
+ this.state.lastCleanupAt = now;
8791
9003
  }
8792
9004
  async storeEvent(streamId, message) {
8793
9005
  this.maybeCleanupExpired();
8794
9006
  const eventId = randomUUID();
8795
- this.events.set(eventId, {
9007
+ this.state.events.set(eventId, {
8796
9008
  streamId,
9009
+ ownerKey: this.ownerKey,
8797
9010
  message,
8798
9011
  expiresAt: Date.now() + this.ttlMs
8799
9012
  });
8800
- const streamIds = this.streamEvents.get(streamId) || [];
9013
+ const streamIds = this.state.streamEvents.get(streamId) || [];
8801
9014
  streamIds.push(eventId);
8802
- this.streamEvents.set(streamId, streamIds);
9015
+ this.state.streamEvents.set(streamId, streamIds);
8803
9016
  return eventId;
8804
9017
  }
8805
9018
  async getStreamIdForEventId(eventId) {
@@ -8812,17 +9025,17 @@ var InMemoryEventStore = class {
8812
9025
  if (!streamId) {
8813
9026
  return { status: "missing" };
8814
9027
  }
8815
- if (this.activeReplayStreams.has(streamId)) {
9028
+ if (this.state.activeReplayStreams.has(streamId)) {
8816
9029
  return { status: "conflict" };
8817
9030
  }
8818
- this.activeReplayStreams.add(streamId);
9031
+ this.state.activeReplayStreams.add(streamId);
8819
9032
  let released = false;
8820
9033
  return {
8821
9034
  status: "acquired",
8822
9035
  release: async () => {
8823
9036
  if (!released) {
8824
9037
  released = true;
8825
- this.activeReplayStreams.delete(streamId);
9038
+ this.state.activeReplayStreams.delete(streamId);
8826
9039
  }
8827
9040
  }
8828
9041
  };
@@ -8836,7 +9049,7 @@ var InMemoryEventStore = class {
8836
9049
  if (!lastEvent) {
8837
9050
  throw new Error(`Event not found for replay: ${lastEventId}`);
8838
9051
  }
8839
- const eventIds = this.streamEvents.get(lastEvent.streamId) || [];
9052
+ const eventIds = this.state.streamEvents.get(lastEvent.streamId) || [];
8840
9053
  const index = eventIds.indexOf(lastEventId);
8841
9054
  if (index < 0) {
8842
9055
  throw new Error(
@@ -8853,16 +9066,36 @@ var InMemoryEventStore = class {
8853
9066
  return lastEvent.streamId;
8854
9067
  }
8855
9068
  };
8856
- var RedisEventStore = class {
8857
- client;
9069
+ var RedisEventStore = class _RedisEventStore {
8858
9070
  redisUrl;
8859
9071
  ttlSeconds;
8860
9072
  keyPrefix;
8861
- connectPromise;
8862
- constructor(redisUrl, keyPrefix, ttlSeconds) {
9073
+ ownerKey;
9074
+ encryptionKey;
9075
+ connection;
9076
+ constructor(redisUrl, keyPrefix, ttlSeconds, ownerKey, encryptionKey, connection = {}) {
8863
9077
  this.redisUrl = redisUrl;
8864
9078
  this.ttlSeconds = ttlSeconds;
8865
9079
  this.keyPrefix = keyPrefix;
9080
+ this.ownerKey = ownerKey;
9081
+ this.encryptionKey = encryptionKey;
9082
+ this.connection = connection;
9083
+ }
9084
+ get client() {
9085
+ return this.connection.client;
9086
+ }
9087
+ set client(client) {
9088
+ this.connection.client = client;
9089
+ }
9090
+ forOwner(ownerKey) {
9091
+ return new _RedisEventStore(
9092
+ this.redisUrl,
9093
+ this.keyPrefix,
9094
+ this.ttlSeconds,
9095
+ ownerKey,
9096
+ this.encryptionKey,
9097
+ this.connection
9098
+ );
8866
9099
  }
8867
9100
  counterKey() {
8868
9101
  return `${this.keyPrefix}:counter`;
@@ -8877,9 +9110,9 @@ var RedisEventStore = class {
8877
9110
  return `${this.keyPrefix}:stream:${assertSafeRedisId(streamId, "streamId")}:replay-lease`;
8878
9111
  }
8879
9112
  async ensureConnected() {
8880
- if (!this.client) {
9113
+ if (!this.connection.client) {
8881
9114
  const { createClient } = await import("redis");
8882
- this.client = createClient({
9115
+ this.connection.client = createClient({
8883
9116
  url: this.redisUrl,
8884
9117
  // node-redis v6 defaults to RESP3, a 5-second command timeout, and a
8885
9118
  // 30-second keepalive delay. Keep the event store's v5 behavior stable
@@ -8888,7 +9121,7 @@ var RedisEventStore = class {
8888
9121
  socket: { keepAliveInitialDelay: 5e3 },
8889
9122
  commandOptions: { timeout: void 0 }
8890
9123
  });
8891
- this.client.on("error", (error) => {
9124
+ this.connection.client.on("error", (error) => {
8892
9125
  Logger.error("Redis event store error", {
8893
9126
  metadata: {
8894
9127
  error: error instanceof Error ? error.message : String(error)
@@ -8896,22 +9129,22 @@ var RedisEventStore = class {
8896
9129
  });
8897
9130
  });
8898
9131
  }
8899
- if (this.client.isOpen) {
9132
+ if (this.connection.client.isOpen) {
8900
9133
  return;
8901
9134
  }
8902
- if (!this.connectPromise) {
8903
- this.connectPromise = this.client.connect().catch((error) => {
8904
- this.connectPromise = void 0;
9135
+ if (!this.connection.connectPromise) {
9136
+ this.connection.connectPromise = this.connection.client.connect().catch((error) => {
9137
+ this.connection.connectPromise = void 0;
8905
9138
  throw error;
8906
9139
  });
8907
9140
  }
8908
- await this.connectPromise;
9141
+ await this.connection.connectPromise;
8909
9142
  }
8910
9143
  getConnectedClient() {
8911
- if (!this.client) {
9144
+ if (!this.connection.client) {
8912
9145
  throw new Error("Redis client not initialized");
8913
9146
  }
8914
- return this.client;
9147
+ return this.connection.client;
8915
9148
  }
8916
9149
  async storeEvent(streamId, message) {
8917
9150
  await this.ensureConnected();
@@ -8923,8 +9156,9 @@ var RedisEventStore = class {
8923
9156
  const tx = client.multi();
8924
9157
  tx.hSet(eventKey, {
8925
9158
  streamId,
9159
+ ownerKey: this.ownerKey,
8926
9160
  sequence: String(sequence),
8927
- message: JSON.stringify(message)
9161
+ message: encryptEventPayload(message, this.encryptionKey)
8928
9162
  });
8929
9163
  tx.expire(eventKey, this.ttlSeconds);
8930
9164
  tx.zAdd(streamKey, {
@@ -8940,11 +9174,10 @@ var RedisEventStore = class {
8940
9174
  return void 0;
8941
9175
  }
8942
9176
  await this.ensureConnected();
8943
- const streamId = await this.getConnectedClient().hGet(
8944
- this.eventKey(eventId),
8945
- "streamId"
9177
+ const event = await this.getConnectedClient().hGetAll(
9178
+ this.eventKey(eventId)
8946
9179
  );
8947
- return streamId || void 0;
9180
+ return event.ownerKey === this.ownerKey ? event.streamId || void 0 : void 0;
8948
9181
  }
8949
9182
  async acquireReplayLease(eventId) {
8950
9183
  const streamId = await this.getStreamIdForEventId(eventId);
@@ -8983,7 +9216,7 @@ var RedisEventStore = class {
8983
9216
  const client = this.getConnectedClient();
8984
9217
  const baseEvent = await client.hGetAll(this.eventKey(lastEventId));
8985
9218
  const streamId = baseEvent.streamId;
8986
- if (!streamId) {
9219
+ if (!streamId || baseEvent.ownerKey !== this.ownerKey) {
8987
9220
  throw new Error(`Event not found for replay: ${lastEventId}`);
8988
9221
  }
8989
9222
  const baseScore = Number(baseEvent.sequence);
@@ -9000,17 +9233,17 @@ var RedisEventStore = class {
9000
9233
  }
9001
9234
  const tx = client.multi();
9002
9235
  for (const eventId of eventIds) {
9003
- tx.hGet(this.eventKey(eventId), "message");
9236
+ tx.hGetAll(this.eventKey(eventId));
9004
9237
  }
9005
- const encodedMessages = await tx.exec();
9238
+ const encodedEvents = await tx.exec();
9006
9239
  for (const [index, eventId] of eventIds.entries()) {
9007
- const encoded = encodedMessages[index];
9008
- if (typeof encoded !== "string") {
9240
+ const event = encodedEvents[index];
9241
+ if (typeof event !== "object" || event === null || !("message" in event) || !("ownerKey" in event) || event.ownerKey !== this.ownerKey || typeof event.message !== "string") {
9009
9242
  continue;
9010
9243
  }
9011
9244
  let message;
9012
9245
  try {
9013
- message = JSON.parse(encoded);
9246
+ message = decryptEventPayload(event.message, this.encryptionKey);
9014
9247
  } catch {
9015
9248
  continue;
9016
9249
  }
@@ -9019,17 +9252,17 @@ var RedisEventStore = class {
9019
9252
  return streamId;
9020
9253
  }
9021
9254
  async close() {
9022
- if (!this.client) {
9255
+ if (!this.connection.client) {
9023
9256
  return;
9024
9257
  }
9025
- if (!this.client.isOpen && this.connectPromise) {
9258
+ if (!this.connection.client.isOpen && this.connection.connectPromise) {
9026
9259
  try {
9027
- await this.connectPromise;
9260
+ await this.connection.connectPromise;
9028
9261
  } catch {
9029
9262
  }
9030
9263
  }
9031
- if (this.client.isOpen) {
9032
- await this.client.close();
9264
+ if (this.connection.client.isOpen) {
9265
+ await this.connection.client.close();
9033
9266
  }
9034
9267
  }
9035
9268
  };
@@ -9037,29 +9270,41 @@ function createManagedEventStore(config) {
9037
9270
  if (config.eventStore.mode === "off") {
9038
9271
  return {
9039
9272
  mode: "off",
9273
+ eventStoreForOwner: () => void 0,
9040
9274
  close: async () => {
9041
9275
  }
9042
9276
  };
9043
9277
  }
9044
9278
  if (config.eventStore.mode === "memory") {
9045
- const memoryStore = new InMemoryEventStore(config.eventStore.ttlSeconds);
9279
+ const memoryStore = new InMemoryEventStore(
9280
+ config.eventStore.ttlSeconds,
9281
+ "internal"
9282
+ );
9046
9283
  return {
9047
9284
  mode: "memory",
9048
- eventStore: memoryStore,
9049
- acquireReplayLease: (eventId) => memoryStore.acquireReplayLease(eventId),
9285
+ eventStoreForOwner: (ownerKey) => memoryStore.forOwner(ownerKey),
9286
+ acquireReplayLease: (eventId, ownerKey) => memoryStore.forOwner(ownerKey).acquireReplayLease(eventId),
9050
9287
  close: async () => {
9051
9288
  }
9052
9289
  };
9053
9290
  }
9291
+ const encryptionKey = config.eventStore.encryptionKey;
9292
+ if (!encryptionKey) {
9293
+ throw new Error(
9294
+ "Redis event store requires MCP_EVENT_ENCRYPTION_KEY configuration"
9295
+ );
9296
+ }
9054
9297
  const redisStore = new RedisEventStore(
9055
9298
  config.eventStore.redisUrl,
9056
9299
  config.eventStore.redisKeyPrefix,
9057
- config.eventStore.ttlSeconds
9300
+ config.eventStore.ttlSeconds,
9301
+ "internal",
9302
+ encryptionKey
9058
9303
  );
9059
9304
  return {
9060
9305
  mode: "redis",
9061
- eventStore: redisStore,
9062
- acquireReplayLease: (eventId) => redisStore.acquireReplayLease(eventId),
9306
+ eventStoreForOwner: (ownerKey) => redisStore.forOwner(ownerKey),
9307
+ acquireReplayLease: (eventId, ownerKey) => redisStore.forOwner(ownerKey).acquireReplayLease(eventId),
9063
9308
  close: async () => {
9064
9309
  await redisStore.close();
9065
9310
  }
@@ -9195,6 +9440,25 @@ function parseConfiguredToolDomains(rawValue = process.env.PORTKEY_TOOL_DOMAINS?
9195
9440
  }
9196
9441
  return normalizeToolDomains(requestedDomains);
9197
9442
  }
9443
+ function resolveToolDomains(requestedDomains) {
9444
+ const configuredDomains = parseConfiguredToolDomains();
9445
+ if (!configuredDomains) {
9446
+ return requestedDomains ? normalizeToolDomains(requestedDomains) : void 0;
9447
+ }
9448
+ if (!requestedDomains) {
9449
+ return configuredDomains;
9450
+ }
9451
+ const configuredDomainSet = new Set(configuredDomains);
9452
+ const disallowedDomains = normalizeToolDomains(requestedDomains).filter(
9453
+ (domain) => !configuredDomainSet.has(domain)
9454
+ );
9455
+ if (disallowedDomains.length > 0) {
9456
+ throw new Error(
9457
+ `Requested tool domains are not allowed by PORTKEY_TOOL_DOMAINS: ${disallowedDomains.join(", ")}`
9458
+ );
9459
+ }
9460
+ return normalizeToolDomains(requestedDomains);
9461
+ }
9198
9462
  function createMcpServer(options = {}) {
9199
9463
  const service = getSharedPortkeyService();
9200
9464
  const server = new McpServer(
@@ -9213,7 +9477,7 @@ function createMcpServer(options = {}) {
9213
9477
  );
9214
9478
  registerServerPromptsAndResources(server);
9215
9479
  registerAllTools(server, service, {
9216
- domains: options.toolDomains ?? parseConfiguredToolDomains()
9480
+ domains: resolveToolDomains(options.toolDomains)
9217
9481
  });
9218
9482
  return {
9219
9483
  server,
@@ -9222,6 +9486,8 @@ function createMcpServer(options = {}) {
9222
9486
  }
9223
9487
 
9224
9488
  // src/lib/security.ts
9489
+ import { createHash } from "node:crypto";
9490
+ import { ipKeyGenerator } from "express-rate-limit";
9225
9491
  function parseOrigins(raw) {
9226
9492
  return raw.split(",").map((origin) => origin.trim()).filter(Boolean);
9227
9493
  }
@@ -9342,6 +9608,65 @@ function hostValidationMiddleware(req, res, next) {
9342
9608
  }
9343
9609
  next();
9344
9610
  }
9611
+ var RATE_LIMIT_REDIS_COMMAND_TIMEOUT_MS = 5e3;
9612
+ var REDIS_TOKEN_BUCKET_SCRIPT = `
9613
+ local values = redis.call('HMGET', KEYS[1], 'tokens', 'lastRefill')
9614
+ local tokens = tonumber(values[1])
9615
+ local lastRefill = tonumber(values[2])
9616
+ local maxTokens = tonumber(ARGV[1])
9617
+ local windowMs = tonumber(ARGV[2])
9618
+ local refillRate = tonumber(ARGV[3])
9619
+ local now = tonumber(ARGV[4])
9620
+
9621
+ if not tokens or not lastRefill then
9622
+ tokens = maxTokens
9623
+ lastRefill = now
9624
+ end
9625
+
9626
+ local elapsedMs = math.max(0, now - lastRefill)
9627
+ local tokensToAdd = math.floor((elapsedMs / windowMs) * refillRate)
9628
+ if tokensToAdd > 0 then
9629
+ tokens = math.min(maxTokens, tokens + tokensToAdd)
9630
+ lastRefill = now
9631
+ end
9632
+
9633
+ local allowed = 0
9634
+ if tokens > 0 then
9635
+ tokens = tokens - 1
9636
+ allowed = 1
9637
+ end
9638
+
9639
+ redis.call('HSET', KEYS[1], 'tokens', tokens, 'lastRefill', lastRefill)
9640
+ redis.call('PEXPIRE', KEYS[1], math.max(windowMs * 2, 1000))
9641
+ return { allowed, tokens }
9642
+ `;
9643
+ async function consumeRedisRateLimitToken(client, options) {
9644
+ let timeout;
9645
+ const result = await Promise.race([
9646
+ client.eval(REDIS_TOKEN_BUCKET_SCRIPT, {
9647
+ keys: [options.key],
9648
+ arguments: [
9649
+ String(options.maxTokens),
9650
+ String(options.windowMs),
9651
+ String(options.refillRate),
9652
+ String(options.now)
9653
+ ]
9654
+ }),
9655
+ new Promise((_resolve, reject) => {
9656
+ timeout = setTimeout(() => {
9657
+ reject(new Error("Redis rate-limit command timed out"));
9658
+ }, options.timeoutMs ?? RATE_LIMIT_REDIS_COMMAND_TIMEOUT_MS);
9659
+ })
9660
+ ]).finally(() => {
9661
+ if (timeout) {
9662
+ clearTimeout(timeout);
9663
+ }
9664
+ });
9665
+ if (!Array.isArray(result) || result.length < 1) {
9666
+ throw new Error("Invalid response from Redis rate-limit script");
9667
+ }
9668
+ return { allowed: Number(result[0]) === 1 };
9669
+ }
9345
9670
  function parsePositiveIntegerEnv(name, fallback) {
9346
9671
  const raw = process.env[name];
9347
9672
  if (raw === void 0) {
@@ -9361,20 +9686,71 @@ function parsePositiveIntegerEnv(name, fallback) {
9361
9686
  });
9362
9687
  return fallback;
9363
9688
  }
9689
+ function isExplicitlyEnabled2(value) {
9690
+ return /^(1|true|yes)$/i.test(value?.trim() ?? "");
9691
+ }
9692
+ function resolveRateLimitStore() {
9693
+ const store = process.env.RATE_LIMIT_STORE?.trim().toLowerCase() || "memory";
9694
+ if (store !== "memory" && store !== "redis") {
9695
+ throw new Error(
9696
+ `Invalid RATE_LIMIT_STORE value: ${store}. Must be 'memory' or 'redis'`
9697
+ );
9698
+ }
9699
+ return store;
9700
+ }
9701
+ function resolveRateLimitRedisUrl(store) {
9702
+ if (store !== "redis") {
9703
+ return void 0;
9704
+ }
9705
+ const redisUrl = process.env.RATE_LIMIT_REDIS_URL?.trim() || process.env.MCP_REDIS_URL?.trim() || process.env.REDIS_URL?.trim();
9706
+ if (!redisUrl) {
9707
+ throw new Error(
9708
+ "RATE_LIMIT_STORE=redis requires RATE_LIMIT_REDIS_URL, MCP_REDIS_URL, or REDIS_URL"
9709
+ );
9710
+ }
9711
+ let parsedUrl;
9712
+ try {
9713
+ parsedUrl = new URL(redisUrl);
9714
+ } catch {
9715
+ throw new Error(
9716
+ "Rate-limit Redis URL must be a valid redis:// or rediss:// URL"
9717
+ );
9718
+ }
9719
+ if (!["redis:", "rediss:"].includes(parsedUrl.protocol)) {
9720
+ throw new Error("Rate-limit Redis URL must use redis:// or rediss://");
9721
+ }
9722
+ if (process.env.NODE_ENV?.trim().toLowerCase() === "production" && parsedUrl.protocol !== "rediss:") {
9723
+ throw new Error("Rate-limit Redis URL must use rediss:// in production");
9724
+ }
9725
+ return redisUrl;
9726
+ }
9727
+ var rateLimitEnabled = process.env.RATE_LIMIT_ENABLED?.trim().toLowerCase() !== "false";
9728
+ var rateLimitStore = resolveRateLimitStore();
9729
+ if (rateLimitEnabled && rateLimitStore === "memory" && process.env.NODE_ENV?.trim().toLowerCase() === "production" && !isExplicitlyEnabled2(process.env.RATE_LIMIT_SINGLE_PROCESS)) {
9730
+ throw new Error(
9731
+ "Production in-memory rate limiting requires RATE_LIMIT_SINGLE_PROCESS=true. Use RATE_LIMIT_STORE=redis for multi-instance deployments."
9732
+ );
9733
+ }
9364
9734
  var RATE_LIMIT_CONFIG = {
9365
- enabled: process.env.RATE_LIMIT_ENABLED?.trim().toLowerCase() !== "false",
9735
+ enabled: rateLimitEnabled,
9736
+ store: rateLimitStore,
9366
9737
  maxTokens: parsePositiveIntegerEnv("RATE_LIMIT_MAX", 60),
9367
9738
  windowMs: parsePositiveIntegerEnv("RATE_LIMIT_WINDOW_MS", 6e4),
9368
9739
  refillRate: parsePositiveIntegerEnv("RATE_LIMIT_REFILL", 60),
9369
- maxBuckets: parsePositiveIntegerEnv("RATE_LIMIT_MAX_BUCKETS", 1e4)
9740
+ maxBuckets: parsePositiveIntegerEnv("RATE_LIMIT_MAX_BUCKETS", 1e4),
9741
+ redisUrl: resolveRateLimitRedisUrl(rateLimitStore),
9742
+ redisKeyPrefix: process.env.RATE_LIMIT_REDIS_KEY_PREFIX?.trim() || "mcp:rate-limit"
9370
9743
  };
9371
9744
  function getRateLimitConfig() {
9372
9745
  return RATE_LIMIT_CONFIG;
9373
9746
  }
9374
9747
  var buckets = /* @__PURE__ */ new Map();
9375
9748
  var overflowBucket;
9376
- function getClientIdentifier(req) {
9377
- return req.ip || "unknown";
9749
+ function getClientIdentifier(req, res, scope) {
9750
+ const principal = res.locals?.authPrincipal;
9751
+ const principalKey = scope === "principal" && principal ? getPrincipalOwnerKey(principal) : scope;
9752
+ const trustedIp = ipKeyGenerator(req.ip || "unknown");
9753
+ return createHash("sha256").update(scope, "utf8").update("\0", "utf8").update(principalKey, "utf8").update("\0", "utf8").update(trustedIp, "utf8").digest("hex");
9378
9754
  }
9379
9755
  function refillBucket(bucket, config) {
9380
9756
  const now = Date.now();
@@ -9435,7 +9811,63 @@ function consumeToken(clientId, config) {
9435
9811
  }
9436
9812
  return { allowed: false };
9437
9813
  }
9438
- function rateLimitMiddleware(req, res, next) {
9814
+ var rateLimitRedisClient;
9815
+ var rateLimitRedisCreatePromise;
9816
+ var rateLimitRedisConnectPromise;
9817
+ async function createRateLimitRedisClient(config) {
9818
+ const { createClient } = await import("redis");
9819
+ const client = createClient({
9820
+ url: config.redisUrl,
9821
+ RESP: 2,
9822
+ socket: { keepAliveInitialDelay: 5e3 },
9823
+ commandOptions: { timeout: RATE_LIMIT_REDIS_COMMAND_TIMEOUT_MS }
9824
+ });
9825
+ client.on("error", (error) => {
9826
+ Logger.error("Redis rate-limit store error", {
9827
+ metadata: {
9828
+ error: error instanceof Error ? error.message : String(error)
9829
+ }
9830
+ });
9831
+ });
9832
+ return client;
9833
+ }
9834
+ async function getRateLimitRedisClient(config) {
9835
+ if (!config.redisUrl) {
9836
+ throw new Error("Rate-limit Redis URL is not configured");
9837
+ }
9838
+ if (!rateLimitRedisClient) {
9839
+ rateLimitRedisCreatePromise ??= createRateLimitRedisClient(config).catch(
9840
+ (error) => {
9841
+ rateLimitRedisCreatePromise = void 0;
9842
+ throw error;
9843
+ }
9844
+ );
9845
+ rateLimitRedisClient = await rateLimitRedisCreatePromise;
9846
+ }
9847
+ if (!rateLimitRedisClient.isOpen) {
9848
+ rateLimitRedisConnectPromise ??= rateLimitRedisClient.connect().catch((error) => {
9849
+ rateLimitRedisConnectPromise = void 0;
9850
+ throw error;
9851
+ });
9852
+ await rateLimitRedisConnectPromise;
9853
+ }
9854
+ return rateLimitRedisClient;
9855
+ }
9856
+ function applyRateLimitDecision(allowed, config, req, res, next, clientId) {
9857
+ if (allowed) {
9858
+ next();
9859
+ return;
9860
+ }
9861
+ const retryAfterMs = Math.ceil(config.windowMs / config.refillRate);
9862
+ Logger.warn("Rate limit exceeded", {
9863
+ path: req.path,
9864
+ method: req.method,
9865
+ metadata: { clientFingerprint: clientId.slice(0, 16), retryAfterMs }
9866
+ });
9867
+ res.setHeader("Retry-After", Math.ceil(retryAfterMs / 1e3).toString());
9868
+ res.status(429).json({ error: "Too Many Requests" });
9869
+ }
9870
+ function applyRateLimit(scope, req, res, next) {
9439
9871
  const config = getRateLimitConfig();
9440
9872
  if (!config.enabled) {
9441
9873
  next();
@@ -9445,20 +9877,69 @@ function rateLimitMiddleware(req, res, next) {
9445
9877
  next();
9446
9878
  return;
9447
9879
  }
9448
- const clientId = getClientIdentifier(req);
9449
- const { allowed } = consumeToken(clientId, config);
9450
- if (!allowed) {
9451
- const retryAfterMs = Math.ceil(config.windowMs / config.refillRate);
9452
- Logger.warn("Rate limit exceeded", {
9453
- path: req.path,
9454
- method: req.method,
9455
- metadata: { clientId, retryAfterMs }
9456
- });
9457
- res.setHeader("Retry-After", Math.ceil(retryAfterMs / 1e3).toString());
9458
- res.status(429).json({ error: "Too Many Requests" });
9880
+ if (scope === "authentication" && req.path !== "/mcp") {
9881
+ next();
9459
9882
  return;
9460
9883
  }
9461
- next();
9884
+ const clientId = getClientIdentifier(req, res, scope);
9885
+ if (config.store === "memory") {
9886
+ const { allowed } = consumeToken(clientId, config);
9887
+ applyRateLimitDecision(allowed, config, req, res, next, clientId);
9888
+ return;
9889
+ }
9890
+ return (async () => {
9891
+ try {
9892
+ const client = await getRateLimitRedisClient(config);
9893
+ const { allowed } = await consumeRedisRateLimitToken(client, {
9894
+ key: `${config.redisKeyPrefix}:${clientId}`,
9895
+ maxTokens: config.maxTokens,
9896
+ windowMs: config.windowMs,
9897
+ refillRate: config.refillRate,
9898
+ now: Date.now()
9899
+ });
9900
+ applyRateLimitDecision(allowed, config, req, res, next, clientId);
9901
+ } catch (error) {
9902
+ Logger.error("Rate-limit store unavailable", {
9903
+ path: req.path,
9904
+ method: req.method,
9905
+ metadata: {
9906
+ error: error instanceof Error ? error.message : String(error)
9907
+ }
9908
+ });
9909
+ res.status(503).json({ error: "Rate limit service unavailable" });
9910
+ }
9911
+ })();
9912
+ }
9913
+ function rateLimitMiddleware(req, res, next) {
9914
+ return applyRateLimit("authentication", req, res, next);
9915
+ }
9916
+ function principalRateLimitMiddleware(req, res, next) {
9917
+ return applyRateLimit("principal", req, res, next);
9918
+ }
9919
+ async function closeRateLimitStore() {
9920
+ if (!rateLimitRedisClient && rateLimitRedisCreatePromise) {
9921
+ try {
9922
+ rateLimitRedisClient = await rateLimitRedisCreatePromise;
9923
+ } catch {
9924
+ rateLimitRedisCreatePromise = void 0;
9925
+ return;
9926
+ }
9927
+ }
9928
+ if (!rateLimitRedisClient) {
9929
+ return;
9930
+ }
9931
+ if (!rateLimitRedisClient.isOpen && rateLimitRedisConnectPromise) {
9932
+ try {
9933
+ await rateLimitRedisConnectPromise;
9934
+ } catch {
9935
+ }
9936
+ }
9937
+ if (rateLimitRedisClient.isOpen) {
9938
+ await rateLimitRedisClient.close();
9939
+ }
9940
+ rateLimitRedisClient = void 0;
9941
+ rateLimitRedisCreatePromise = void 0;
9942
+ rateLimitRedisConnectPromise = void 0;
9462
9943
  }
9463
9944
  var cleanupTimer = setInterval(
9464
9945
  () => {
@@ -9495,6 +9976,11 @@ var SessionStore = class {
9495
9976
  get(sessionId) {
9496
9977
  return this.sessions.get(sessionId);
9497
9978
  }
9979
+ /** Return a session only when it belongs to the supplied principal. */
9980
+ getOwned(sessionId, ownerKey) {
9981
+ const entry = this.sessions.get(sessionId);
9982
+ return entry?.ownerKey === ownerKey ? entry : void 0;
9983
+ }
9498
9984
  /**
9499
9985
  * Get a transport by session ID
9500
9986
  * @param sessionId - The session identifier
@@ -9746,6 +10232,16 @@ function respondJsonRpcClientError(res, statusCode, message, code = -32602) {
9746
10232
  id: null
9747
10233
  });
9748
10234
  }
10235
+ function getRequestOwnerKey(res) {
10236
+ const principal = res.locals.authPrincipal;
10237
+ if (!principal) {
10238
+ throw new Error("Authenticated MCP request is missing its principal");
10239
+ }
10240
+ return getPrincipalOwnerKey(principal);
10241
+ }
10242
+ function fingerprintCapabilityId(id) {
10243
+ return createHash2("sha256").update(id, "utf8").digest("hex").slice(0, 16);
10244
+ }
9749
10245
  function parseRequestedToolDomains(rawQuery) {
9750
10246
  if (rawQuery === void 0) {
9751
10247
  return void 0;
@@ -9773,7 +10269,7 @@ function parseRequestedToolDomains(rawQuery) {
9773
10269
  `Unknown tool domains: ${invalidDomains.join(", ")}. Valid domains: ${TOOL_DOMAIN_NAMES.join(", ")}`
9774
10270
  );
9775
10271
  }
9776
- return normalizeToolDomains(normalizedDomains);
10272
+ return resolveToolDomains(normalizedDomains);
9777
10273
  }
9778
10274
  function areToolDomainsEqual(left, right) {
9779
10275
  const normalizedLeft = left ? normalizeToolDomains(left) : [];
@@ -9845,11 +10341,11 @@ function createHttpAppRuntime() {
9845
10341
  let closeRuntimeResourcesPromise;
9846
10342
  let sigintHandler;
9847
10343
  let sigtermHandler;
9848
- async function handleStatelessRequest(req, res, context, parsedBody, toolDomains) {
10344
+ async function handleStatelessRequest(req, res, context, ownerKey, parsedBody, toolDomains) {
9849
10345
  let replayStreamId;
9850
10346
  let responseDisconnected = false;
9851
10347
  const storedMessages = /* @__PURE__ */ new WeakSet();
9852
- const sharedEventStore = managedEventStore.eventStore;
10348
+ const sharedEventStore = managedEventStore.eventStoreForOwner(ownerKey);
9853
10349
  const requestEventStore = sharedEventStore ? {
9854
10350
  storeEvent: async (streamId, message) => {
9855
10351
  replayStreamId ??= streamId;
@@ -9960,8 +10456,21 @@ function createHttpAppRuntime() {
9960
10456
  app.use(hostValidationMiddleware);
9961
10457
  }
9962
10458
  app.use(originValidationMiddleware);
10459
+ const rateLimitConfig = getRateLimitConfig();
10460
+ app.use(
10461
+ expressRateLimit({
10462
+ windowMs: rateLimitConfig.windowMs,
10463
+ limit: rateLimitConfig.maxTokens,
10464
+ standardHeaders: false,
10465
+ legacyHeaders: false,
10466
+ validate: false,
10467
+ skip: (req) => !rateLimitConfig.enabled || req.path !== "/mcp",
10468
+ message: { error: "Too Many Requests" }
10469
+ })
10470
+ );
9963
10471
  app.use(rateLimitMiddleware);
9964
10472
  app.use(mcpAuthMiddleware);
10473
+ app.use(principalRateLimitMiddleware);
9965
10474
  app.use(
9966
10475
  (err, _req, res, next) => {
9967
10476
  const errorType = err && typeof err === "object" && "type" in err ? err.type : void 0;
@@ -10118,6 +10627,7 @@ function createHttpAppRuntime() {
10118
10627
  });
10119
10628
  });
10120
10629
  app.post("/mcp", async (req, res) => {
10630
+ const ownerKey = getRequestOwnerKey(res);
10121
10631
  let requestedToolDomains;
10122
10632
  const protocolVersionHeader = getMcpProtocolVersion(req);
10123
10633
  try {
@@ -10154,6 +10664,7 @@ function createHttpAppRuntime() {
10154
10664
  req,
10155
10665
  res,
10156
10666
  "POST /mcp stateless",
10667
+ ownerKey,
10157
10668
  req.body,
10158
10669
  requestedToolDomains
10159
10670
  );
@@ -10165,7 +10676,7 @@ function createHttpAppRuntime() {
10165
10676
  const sessionId = req.headers["mcp-session-id"];
10166
10677
  let transport;
10167
10678
  let hasReservedSessionSlot = false;
10168
- const sessionEntry = sessionId ? sessionStore.get(sessionId) : void 0;
10679
+ const sessionEntry = sessionId ? sessionStore.getOwned(sessionId, ownerKey) : void 0;
10169
10680
  if (sessionId && sessionEntry) {
10170
10681
  transport = sessionEntry.transport;
10171
10682
  if (transport) {
@@ -10209,10 +10720,11 @@ function createHttpAppRuntime() {
10209
10720
  hasReservedSessionSlot = true;
10210
10721
  const newTransport = new StreamableHTTPServerTransport({
10211
10722
  sessionIdGenerator: () => randomUUID2(),
10212
- eventStore: managedEventStore.eventStore,
10723
+ eventStore: managedEventStore.eventStoreForOwner(ownerKey),
10213
10724
  onsessioninitialized: (id) => {
10214
10725
  sessionStore.set(id, {
10215
10726
  transport: newTransport,
10727
+ ownerKey,
10216
10728
  toolDomains: requestedToolDomains,
10217
10729
  protocolVersion: getNegotiatedProtocolVersion(req.body),
10218
10730
  createdAt: Date.now(),
@@ -10220,13 +10732,13 @@ function createHttpAppRuntime() {
10220
10732
  });
10221
10733
  hasReservedSessionSlot = false;
10222
10734
  Logger.info("MCP session initialized", {
10223
- metadata: { sessionId: id }
10735
+ metadata: { sessionFingerprint: fingerprintCapabilityId(id) }
10224
10736
  });
10225
10737
  },
10226
10738
  onsessionclosed: (id) => {
10227
10739
  sessionStore.delete(id);
10228
10740
  Logger.info("MCP session closed", {
10229
- metadata: { sessionId: id }
10741
+ metadata: { sessionFingerprint: fingerprintCapabilityId(id) }
10230
10742
  });
10231
10743
  }
10232
10744
  });
@@ -10278,6 +10790,7 @@ function createHttpAppRuntime() {
10278
10790
  }
10279
10791
  });
10280
10792
  app.get("/mcp", async (req, res) => {
10793
+ const ownerKey = getRequestOwnerKey(res);
10281
10794
  let requestedToolDomains;
10282
10795
  try {
10283
10796
  requestedToolDomains = parseRequestedToolDomains(req.query.tools);
@@ -10299,7 +10812,8 @@ function createHttpAppRuntime() {
10299
10812
  );
10300
10813
  return;
10301
10814
  }
10302
- if (!managedEventStore.eventStore || !managedEventStore.acquireReplayLease) {
10815
+ const replayEventStore = managedEventStore.eventStoreForOwner(ownerKey);
10816
+ if (!replayEventStore || !managedEventStore.acquireReplayLease) {
10303
10817
  res.setHeader("Allow", "POST");
10304
10818
  respondJsonRpcClientError(
10305
10819
  res,
@@ -10336,12 +10850,11 @@ function createHttpAppRuntime() {
10336
10850
  respondJsonRpcClientError(res, 400, protocolError, -32e3);
10337
10851
  return;
10338
10852
  }
10339
- const replayEventStore = managedEventStore.eventStore;
10340
- if (!replayEventStore) {
10341
- return;
10342
- }
10343
10853
  try {
10344
- const lease = await managedEventStore.acquireReplayLease(lastEventId);
10854
+ const lease = await managedEventStore.acquireReplayLease(
10855
+ lastEventId,
10856
+ ownerKey
10857
+ );
10345
10858
  if (lease.status !== "acquired") {
10346
10859
  if (lease.status === "missing") {
10347
10860
  respondJsonRpcClientError(
@@ -10452,7 +10965,7 @@ data:
10452
10965
  });
10453
10966
  return;
10454
10967
  }
10455
- const sessionEntry = sessionStore.get(sessionId);
10968
+ const sessionEntry = sessionStore.getOwned(sessionId, ownerKey);
10456
10969
  const transport = sessionEntry?.transport;
10457
10970
  if (transport) {
10458
10971
  const protocolError = validateRequiredProtocolVersion(
@@ -10485,6 +10998,7 @@ data:
10485
10998
  }
10486
10999
  });
10487
11000
  app.delete("/mcp", async (req, res) => {
11001
+ const ownerKey = getRequestOwnerKey(res);
10488
11002
  if (!isStatefulSessionMode) {
10489
11003
  res.status(405).json({
10490
11004
  jsonrpc: "2.0",
@@ -10509,7 +11023,7 @@ data:
10509
11023
  });
10510
11024
  return;
10511
11025
  }
10512
- const sessionEntry = sessionStore.get(sessionId);
11026
+ const sessionEntry = sessionStore.getOwned(sessionId, ownerKey);
10513
11027
  const transport = sessionEntry?.transport;
10514
11028
  if (transport) {
10515
11029
  const protocolError = validateRequiredProtocolVersion(
@@ -10537,7 +11051,9 @@ data:
10537
11051
  const expiredIds = await sessionStore.cleanup(config.sessionTimeout);
10538
11052
  for (const id of expiredIds) {
10539
11053
  Logger.info("MCP session expired and cleaned up", {
10540
- metadata: { sessionId: id }
11054
+ metadata: {
11055
+ sessionFingerprint: fingerprintCapabilityId(id)
11056
+ }
10541
11057
  });
10542
11058
  }
10543
11059
  } catch (error) {
@@ -10584,7 +11100,7 @@ data:
10584
11100
  );
10585
11101
  await Promise.allSettled(activeReplayOperations);
10586
11102
  } finally {
10587
- await managedEventStore.close();
11103
+ await Promise.all([managedEventStore.close(), closeRateLimitStore()]);
10588
11104
  }
10589
11105
  })();
10590
11106
  }
@@ -10631,7 +11147,7 @@ data:
10631
11147
  console.log(" GET /ready - Readiness check");
10632
11148
  console.log(" POST /mcp - MCP requests");
10633
11149
  console.log(
10634
- ` GET /mcp - ${isStatefulSessionMode ? "SSE notifications and replay" : managedEventStore.eventStore ? "SSE replay (requires Last-Event-ID)" : "Unavailable (event store off)"}`
11150
+ ` GET /mcp - ${isStatefulSessionMode ? "SSE notifications and replay" : managedEventStore.mode !== "off" ? "SSE replay (requires Last-Event-ID)" : "Unavailable (event store off)"}`
10635
11151
  );
10636
11152
  console.log(
10637
11153
  ` DELETE /mcp - ${isStatefulSessionMode ? "Close session" : "Not used in stateless mode"}`