mcp-warden 0.1.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,6 +73,37 @@ export type GuardianMiddleware = (context: GuardianContext, next: () => Promise<
73
73
  * Logging function for policy violations.
74
74
  */
75
75
  export type GuardianLogger = (violation: GuardianViolation) => void;
76
+ /**
77
+ * Metrics snapshot emitted after each validated request.
78
+ */
79
+ export interface GuardianMetrics {
80
+ /** Unix timestamp (ms) when the request was processed. */
81
+ timestamp: number;
82
+ /** JSON-RPC method name. */
83
+ method: string;
84
+ /** Tool name if this was a tools/call request. Undefined for non-tool-call methods. */
85
+ toolName: string | undefined;
86
+ /** Whether the request was allowed. */
87
+ allowed: boolean;
88
+ /** Violation code when blocked. Undefined when allowed. */
89
+ violationCode: GuardianViolationCode | undefined;
90
+ /** Processing time in milliseconds. */
91
+ durationMs: number;
92
+ }
93
+ /**
94
+ * Callback invoked after every request validation with observability data.
95
+ */
96
+ export type GuardianMetricsHook = (metrics: GuardianMetrics) => void;
97
+ /**
98
+ * Per-tool rate-limit override. Takes precedence over the global
99
+ * `maxCallsPerMinute` when the tool name matches.
100
+ */
101
+ export interface ToolRateLimit {
102
+ /** Exact tool name or regex pattern this override applies to. */
103
+ tool: string | RegExp;
104
+ /** Max calls per minute for this tool. */
105
+ maxCallsPerMinute: number;
106
+ }
76
107
  /**
77
108
  * Configuration object for McpGuardian construction.
78
109
  */
@@ -83,6 +114,20 @@ export interface McpGuardianOptions {
83
114
  circuitBreaker?: CircuitBreakerOptions;
84
115
  redactToolOutputs?: boolean;
85
116
  nowProvider?: () => number;
117
+ /** Called after every request with observability data. */
118
+ metricsHook?: GuardianMetricsHook;
119
+ /** Per-tool rate limit overrides. First match wins. */
120
+ toolRateLimits?: ToolRateLimit[];
121
+ /**
122
+ * Maximum allowed depth of a tool arguments object.
123
+ * Requests exceeding this are rejected. Default: 20.
124
+ */
125
+ maxArgDepth?: number;
126
+ /**
127
+ * Maximum allowed byte size of the serialized tool arguments.
128
+ * Requests exceeding this are rejected. Default: 512 KB.
129
+ */
130
+ maxArgBytes?: number;
86
131
  }
87
132
  /**
88
133
  * Core policy guard for intercepting JSON-RPC tool calls.
@@ -99,13 +144,15 @@ export declare class McpGuardian {
99
144
  private readonly injectionKeywords;
100
145
  private readonly redactToolOutputs;
101
146
  private readonly rateLimiter;
147
+ private readonly toolLimiters;
102
148
  private readonly nowProvider;
149
+ private readonly metricsHook;
103
150
  /**
104
151
  * Creates a guardian instance with policy and optional runtime controls.
105
152
  */
106
153
  constructor(policy: GuardianPolicy, options?: McpGuardianOptions);
107
154
  /**
108
- * Registers additional middleware checks that run after the built-in policy check.
155
+ * Registers additional middleware checks that run after the built-in policy checks.
109
156
  */
110
157
  use(middleware: GuardianMiddleware): this;
111
158
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../../src/core/interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AAQxC;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,CAAC,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,SAAS,CAAC;IACd,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,qBAAqB,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAC/B,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,KACpC,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAEjC;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AA8TD;;;;;GAKG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IAExC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IAEnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IAExC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuB;IAEnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAEhD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAoB;IAEtD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAE5C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAE1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAe;IAE3C;;OAEG;gBACgB,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,kBAAuB;IAuB3E;;OAEG;IACI,GAAG,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAKhD;;OAEG;IACU,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA2ChF;;OAEG;IACI,WAAW,CAAC,SAAS,EAC1B,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,SAAS,GAAG,oBAAoB,CAAC,GAC9E,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,SAAS,GAAG,oBAAoB,CAAC;IAoCzE;;OAEG;YACW,cAAc;CAkB7B"}
1
+ {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../../src/core/interceptor.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,8 @@ import { RateLimiter } from "../security/rate-limiter.js";
8
8
  */
9
9
  const PERMISSION_DENIED_NUMERIC_CODE = -32001;
10
10
  const REQUIRES_APPROVAL_NUMERIC_CODE = -32002;
11
+ const DEFAULT_MAX_ARG_DEPTH = 20;
12
+ const DEFAULT_MAX_ARG_BYTES = 512 * 1024; // 512 KB
11
13
  const PATH_ARG_KEYS = new Set([
12
14
  "path",
13
15
  "paths",
@@ -20,6 +22,26 @@ const PATH_ARG_KEYS = new Set([
20
22
  "filepath",
21
23
  "file_path"
22
24
  ]);
25
+ /** Write-intent verbs used to detect mutating operations for read-only enforcement. */
26
+ const WRITE_INTENT_VERBS = new Set([
27
+ "write",
28
+ "create",
29
+ "delete",
30
+ "remove",
31
+ "rm",
32
+ "mv",
33
+ "move",
34
+ "rename",
35
+ "mkdir",
36
+ "touch",
37
+ "truncate",
38
+ "append",
39
+ "overwrite",
40
+ "put",
41
+ "patch",
42
+ "post",
43
+ "upload"
44
+ ]);
23
45
  /**
24
46
  * Detects whether a request looks like an MCP tool call.
25
47
  */
@@ -116,6 +138,44 @@ function collectCandidatePaths(payload, parentKey) {
116
138
  }
117
139
  return result;
118
140
  }
141
+ /**
142
+ * Returns the depth of a nested object/array structure.
143
+ */
144
+ function measureDepth(value, current = 0) {
145
+ if (current > DEFAULT_MAX_ARG_DEPTH) {
146
+ return current;
147
+ }
148
+ if (Array.isArray(value)) {
149
+ let max = current;
150
+ for (const entry of value) {
151
+ max = Math.max(max, measureDepth(entry, current + 1));
152
+ }
153
+ return max;
154
+ }
155
+ if (value && typeof value === "object") {
156
+ let max = current;
157
+ for (const entry of Object.values(value)) {
158
+ max = Math.max(max, measureDepth(entry, current + 1));
159
+ }
160
+ return max;
161
+ }
162
+ return current;
163
+ }
164
+ /**
165
+ * Returns true if the tool name suggests a write/mutating operation.
166
+ */
167
+ function toolImpliesWrite(toolName) {
168
+ const lower = toolName.toLowerCase();
169
+ for (const verb of WRITE_INTENT_VERBS) {
170
+ if (lower.includes(verb)) {
171
+ return true;
172
+ }
173
+ }
174
+ return false;
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Built-in middleware
178
+ // ---------------------------------------------------------------------------
119
179
  /**
120
180
  * Middleware that enforces `allowedTools` policy for tool calls.
121
181
  */
@@ -162,30 +222,47 @@ async function enforceInjectionPolicy(context, next, keywords) {
162
222
  return next();
163
223
  }
164
224
  /**
165
- * Middleware that denies tool calls targeting blocked filesystem paths.
225
+ * Middleware that denies tool calls targeting blocked filesystem paths, and
226
+ * denies write-intent tool calls targeting read-only filesystem paths.
166
227
  */
167
228
  async function enforceRestrictedPaths(context, next) {
168
229
  if (!isToolCallRequest(context.request)) {
169
230
  return next();
170
231
  }
171
- const blockedPaths = context.policy.restrictedPaths
172
- .filter((entry) => entry.mode === "blocked")
173
- .map((entry) => normalizePolicyPath(entry.path));
174
- if (blockedPaths.length === 0) {
232
+ const { restrictedPaths } = context.policy;
233
+ if (restrictedPaths.length === 0) {
175
234
  return next();
176
235
  }
236
+ const blockedPaths = restrictedPaths
237
+ .filter((entry) => entry.mode === "blocked")
238
+ .map((entry) => normalizePolicyPath(entry.path));
239
+ const readOnlyPaths = restrictedPaths
240
+ .filter((entry) => entry.mode === "read-only")
241
+ .map((entry) => normalizePolicyPath(entry.path));
177
242
  const candidatePaths = collectCandidatePaths(context.toolArgs)
178
243
  .map((value) => normalizePolicyPath(value))
179
244
  .filter((value) => value.length > 0);
180
245
  for (const candidatePath of candidatePaths) {
181
- const matchedBlockedPath = blockedPaths.find((blockedPath) => matchesBlockedPath(candidatePath, blockedPath));
182
- if (matchedBlockedPath) {
246
+ // Blocked paths: all access denied
247
+ const matchedBlocked = blockedPaths.find((blocked) => matchesBlockedPath(candidatePath, blocked));
248
+ if (matchedBlocked) {
183
249
  return {
184
250
  allowed: false,
185
251
  code: "PERMISSION_DENIED",
186
252
  reason: `Tool arguments include restricted path '${candidatePath}' blocked by policy.`
187
253
  };
188
254
  }
255
+ // Read-only paths: write-intent operations denied
256
+ if (context.toolName && toolImpliesWrite(context.toolName)) {
257
+ const matchedReadOnly = readOnlyPaths.find((ro) => matchesBlockedPath(candidatePath, ro));
258
+ if (matchedReadOnly) {
259
+ return {
260
+ allowed: false,
261
+ code: "PERMISSION_DENIED",
262
+ reason: `Tool '${context.toolName}' attempts a write operation on read-only path '${candidatePath}'.`
263
+ };
264
+ }
265
+ }
189
266
  }
190
267
  return next();
191
268
  }
@@ -208,11 +285,11 @@ async function enforceApprovalRequired(context, next) {
208
285
  /**
209
286
  * Middleware that blocks tools with an open circuit.
210
287
  */
211
- async function enforceCircuitState(context, next, circuitBreaker) {
288
+ async function enforceCircuitState(context, next, circuitBreaker, nowProvider) {
212
289
  if (!isToolCallRequest(context.request) || !context.toolName) {
213
290
  return next();
214
291
  }
215
- const decision = circuitBreaker.canExecute(context.toolName);
292
+ const decision = circuitBreaker.canExecute(context.toolName, nowProvider());
216
293
  if (!decision.allowed) {
217
294
  const retryMs = Math.max(0, decision.retryAfterMs ?? 0);
218
295
  const retrySeconds = Math.ceil(retryMs / 1000);
@@ -224,16 +301,43 @@ async function enforceCircuitState(context, next, circuitBreaker) {
224
301
  return next();
225
302
  }
226
303
  /**
227
- * Middleware that enforces global max-calls-per-minute policy.
304
+ * Middleware that enforces global and per-tool max-calls-per-minute policy.
228
305
  */
229
- async function enforceRateLimit(context, next, rateLimiter, nowProvider) {
306
+ async function enforceRateLimit(context, next, globalLimiter, toolLimiters, toolRateLimits, nowProvider) {
230
307
  if (!isToolCallRequest(context.request)) {
231
308
  return next();
232
309
  }
233
- const decision = rateLimiter.consume(nowProvider());
310
+ const now = nowProvider();
311
+ // Per-tool rate limit (first matching rule wins)
312
+ if (context.toolName) {
313
+ const toolName = context.toolName;
314
+ const matchingRule = toolRateLimits.find((rule) => {
315
+ if (typeof rule.tool === "string") {
316
+ return rule.tool === toolName;
317
+ }
318
+ return rule.tool.test(toolName);
319
+ });
320
+ if (matchingRule) {
321
+ let limiter = toolLimiters.get(toolName);
322
+ if (!limiter) {
323
+ limiter = new RateLimiter({ maxCallsPerMinute: matchingRule.maxCallsPerMinute });
324
+ toolLimiters.set(toolName, limiter);
325
+ }
326
+ const decision = limiter.consume(now);
327
+ if (!decision.allowed) {
328
+ const retrySeconds = Math.ceil(Math.max(0, decision.retryAfterMs ?? 0) / 1000);
329
+ return {
330
+ allowed: false,
331
+ reason: `Per-tool rate limit exceeded for '${toolName}'. Retry in ${retrySeconds}s.`
332
+ };
333
+ }
334
+ return next();
335
+ }
336
+ }
337
+ // Global rate limit
338
+ const decision = globalLimiter.consume(now);
234
339
  if (!decision.allowed) {
235
- const retryMs = Math.max(0, decision.retryAfterMs ?? 0);
236
- const retrySeconds = Math.ceil(retryMs / 1000);
340
+ const retrySeconds = Math.ceil(Math.max(0, decision.retryAfterMs ?? 0) / 1000);
237
341
  return {
238
342
  allowed: false,
239
343
  reason: `Rate limit exceeded. Retry in ${retrySeconds}s.`
@@ -241,6 +345,40 @@ async function enforceRateLimit(context, next, rateLimiter, nowProvider) {
241
345
  }
242
346
  return next();
243
347
  }
348
+ /**
349
+ * Middleware that rejects payloads exceeding depth or byte-size limits.
350
+ */
351
+ async function enforceInputLimits(context, next, maxDepth, maxBytes) {
352
+ if (!isToolCallRequest(context.request) || context.toolArgs === undefined) {
353
+ return next();
354
+ }
355
+ const depth = measureDepth(context.toolArgs);
356
+ if (depth > maxDepth) {
357
+ return {
358
+ allowed: false,
359
+ reason: `Tool arguments exceed maximum nesting depth of ${maxDepth}.`
360
+ };
361
+ }
362
+ try {
363
+ const serialized = JSON.stringify(context.toolArgs);
364
+ if (serialized.length > maxBytes) {
365
+ return {
366
+ allowed: false,
367
+ reason: `Tool arguments exceed maximum size of ${maxBytes} bytes.`
368
+ };
369
+ }
370
+ }
371
+ catch {
372
+ return {
373
+ allowed: false,
374
+ reason: "Tool arguments could not be serialized for size validation."
375
+ };
376
+ }
377
+ return next();
378
+ }
379
+ // ---------------------------------------------------------------------------
380
+ // McpGuardian
381
+ // ---------------------------------------------------------------------------
244
382
  /**
245
383
  * Core policy guard for intercepting JSON-RPC tool calls.
246
384
  *
@@ -256,7 +394,9 @@ export class McpGuardian {
256
394
  injectionKeywords;
257
395
  redactToolOutputs;
258
396
  rateLimiter;
397
+ toolLimiters;
259
398
  nowProvider;
399
+ metricsHook;
260
400
  /**
261
401
  * Creates a guardian instance with policy and optional runtime controls.
262
402
  */
@@ -267,21 +407,25 @@ export class McpGuardian {
267
407
  this.circuitBreaker = new CircuitBreaker(options.circuitBreaker);
268
408
  this.injectionKeywords = options.injectionKeywords ?? DEFAULT_INJECTION_KEYWORDS;
269
409
  this.redactToolOutputs = options.redactToolOutputs ?? true;
270
- this.rateLimiter = new RateLimiter({
271
- maxCallsPerMinute: this.policy.maxCallsPerMinute
272
- });
410
+ this.rateLimiter = new RateLimiter({ maxCallsPerMinute: this.policy.maxCallsPerMinute });
411
+ this.toolLimiters = new Map();
273
412
  this.nowProvider = options.nowProvider ?? (() => Date.now());
413
+ this.metricsHook = options.metricsHook;
414
+ const toolRateLimits = options.toolRateLimits ?? [];
415
+ const maxDepth = options.maxArgDepth ?? DEFAULT_MAX_ARG_DEPTH;
416
+ const maxBytes = options.maxArgBytes ?? DEFAULT_MAX_ARG_BYTES;
274
417
  this.middlewares = [
275
418
  enforceAllowedTools,
419
+ (context, next) => enforceInputLimits(context, next, maxDepth, maxBytes),
276
420
  enforceRestrictedPaths,
277
421
  enforceApprovalRequired,
278
- async (context, next) => enforceRateLimit(context, next, this.rateLimiter, this.nowProvider),
279
- async (context, next) => enforceInjectionPolicy(context, next, this.injectionKeywords),
280
- async (context, next) => enforceCircuitState(context, next, this.circuitBreaker)
422
+ (context, next) => enforceRateLimit(context, next, this.rateLimiter, this.toolLimiters, toolRateLimits, this.nowProvider),
423
+ (context, next) => enforceInjectionPolicy(context, next, this.injectionKeywords),
424
+ (context, next) => enforceCircuitState(context, next, this.circuitBreaker, this.nowProvider)
281
425
  ];
282
426
  }
283
427
  /**
284
- * Registers additional middleware checks that run after the built-in policy check.
428
+ * Registers additional middleware checks that run after the built-in policy checks.
285
429
  */
286
430
  use(middleware) {
287
431
  this.middlewares.push(middleware);
@@ -291,6 +435,7 @@ export class McpGuardian {
291
435
  * Validates an incoming JSON-RPC request against the configured guardrail chain.
292
436
  */
293
437
  async validateRequest(request) {
438
+ const start = this.nowProvider();
294
439
  const toolName = extractToolName(request.params);
295
440
  const toolArgs = extractToolArgs(request.params);
296
441
  const context = {
@@ -301,7 +446,16 @@ export class McpGuardian {
301
446
  ...(toolArgs !== undefined ? { toolArgs } : {})
302
447
  };
303
448
  const decision = await this.runMiddlewares(context);
449
+ const durationMs = this.nowProvider() - start;
304
450
  if (decision.allowed) {
451
+ this.metricsHook?.({
452
+ timestamp: start,
453
+ method: request.method,
454
+ toolName,
455
+ allowed: true,
456
+ violationCode: undefined,
457
+ durationMs
458
+ });
305
459
  return { isAllowed: true };
306
460
  }
307
461
  const violationCode = decision.code ?? "PERMISSION_DENIED";
@@ -315,11 +469,16 @@ export class McpGuardian {
315
469
  ...(toolName ? { toolName } : {})
316
470
  };
317
471
  this.logger(violation);
472
+ this.metricsHook?.({
473
+ timestamp: start,
474
+ method: request.method,
475
+ toolName,
476
+ allowed: false,
477
+ violationCode: violationCode,
478
+ durationMs
479
+ });
318
480
  if (this.isDryRun) {
319
- return {
320
- isAllowed: true,
321
- violation
322
- };
481
+ return { isAllowed: true, violation };
323
482
  }
324
483
  return {
325
484
  isAllowed: false,
@@ -334,6 +493,7 @@ export class McpGuardian {
334
493
  return async (request) => {
335
494
  const toolName = extractToolName(request.params);
336
495
  const shouldTrackCircuit = isToolCallRequest(request) && !!toolName;
496
+ const now = this.nowProvider();
337
497
  const result = await this.validateRequest(request);
338
498
  if (!result.isAllowed && result.error) {
339
499
  return result.error;
@@ -342,10 +502,10 @@ export class McpGuardian {
342
502
  const response = await handler(request);
343
503
  if (shouldTrackCircuit && toolName) {
344
504
  if (isJsonRpcErrorResponse(response)) {
345
- this.circuitBreaker.recordFailure(toolName);
505
+ this.circuitBreaker.recordFailure(toolName, now);
346
506
  }
347
507
  else {
348
- this.circuitBreaker.recordSuccess(toolName);
508
+ this.circuitBreaker.recordSuccess(toolName, now);
349
509
  }
350
510
  }
351
511
  if (!this.redactToolOutputs || !isToolCallRequest(request)) {
@@ -355,7 +515,7 @@ export class McpGuardian {
355
515
  }
356
516
  catch (error) {
357
517
  if (shouldTrackCircuit && toolName) {
358
- this.circuitBreaker.recordFailure(toolName);
518
+ this.circuitBreaker.recordFailure(toolName, now);
359
519
  }
360
520
  throw error;
361
521
  }
@@ -1 +1 @@
1
- {"version":3,"file":"interceptor.js","sourceRoot":"","sources":["../../src/core/interceptor.ts"],"names":[],"mappings":"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","sourcesContent":["import { GuardianPolicySchema, type GuardianPolicy } from \"../types/policy.js\";\nimport {\n CircuitBreaker,\n type CircuitBreakerOptions\n} from \"../security/circuit-breaker.js\";\nimport {\n DEFAULT_INJECTION_KEYWORDS,\n scanForPromptInjection\n} from \"../security/injection-scanner.js\";\nimport { redactSensitiveData } from \"../security/pii-redactor.js\";\nimport { RateLimiter } from \"../security/rate-limiter.js\";\n\n/**\n * JSON-RPC request identifier shape.\n */\nexport type JsonRpcId = string | number | null;\n\n/**\n * Minimal JSON-RPC 2.0 request payload used by MCP transports.\n */\nexport interface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id?: JsonRpcId;\n method: string;\n params?: unknown;\n}\n\n/**\n * Standardized JSON-RPC 2.0 error object.\n */\nexport interface JsonRpcError {\n code: number;\n message: string;\n data?: Record<string, unknown>;\n}\n\n/**\n * Standardized JSON-RPC 2.0 error response payload.\n */\nexport interface JsonRpcErrorResponse {\n jsonrpc: \"2.0\";\n id: JsonRpcId;\n error: JsonRpcError;\n}\n\nexport type GuardianViolationCode = \"PERMISSION_DENIED\" | \"REQUIRES_APPROVAL\";\n\n/**\n * Security violation metadata emitted by the guardian engine.\n */\nexport interface GuardianViolation {\n code: GuardianViolationCode;\n reason: string;\n method: string;\n toolName?: string;\n}\n\n/**\n * Validation result produced by the guardian engine.\n */\nexport interface ValidationResult {\n isAllowed: boolean;\n error?: JsonRpcErrorResponse;\n violation?: GuardianViolation;\n}\n\n/**\n * Decision object returned by guardian middleware.\n */\nexport interface MiddlewareDecision {\n allowed: boolean;\n code?: GuardianViolationCode;\n reason?: string;\n}\n\n/**\n * Execution context provided to each middleware stage.\n */\nexport interface GuardianContext {\n readonly request: JsonRpcRequest;\n readonly policy: GuardianPolicy;\n readonly isDryRun: boolean;\n readonly toolName?: string;\n readonly toolArgs?: unknown;\n}\n\n/**\n * Async middleware function signature used by the guardian engine.\n */\nexport type GuardianMiddleware = (\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>\n) => Promise<MiddlewareDecision>;\n\n/**\n * Logging function for policy violations.\n */\nexport type GuardianLogger = (violation: GuardianViolation) => void;\n\n/**\n * Configuration object for McpGuardian construction.\n */\nexport interface McpGuardianOptions {\n dryRun?: boolean;\n logger?: GuardianLogger;\n injectionKeywords?: readonly string[];\n circuitBreaker?: CircuitBreakerOptions;\n redactToolOutputs?: boolean;\n nowProvider?: () => number;\n}\n\n/**\n * Constant JSON-RPC code used for permission-denied failures.\n */\nconst PERMISSION_DENIED_NUMERIC_CODE = -32001;\nconst REQUIRES_APPROVAL_NUMERIC_CODE = -32002;\n\nconst PATH_ARG_KEYS = new Set([\n \"path\",\n \"paths\",\n \"root\",\n \"roots\",\n \"directory\",\n \"directories\",\n \"dir\",\n \"cwd\",\n \"filepath\",\n \"file_path\"\n]);\n\n/**\n * Detects whether a request looks like an MCP tool call.\n */\nfunction isToolCallRequest(request: JsonRpcRequest): boolean {\n return request.method === \"tools/call\";\n}\n\n/**\n * Best-effort extraction of tool name from standard MCP params.\n */\nfunction extractToolName(params: unknown): string | undefined {\n if (!params || typeof params !== \"object\") {\n return undefined;\n }\n\n const payload = params as Record<string, unknown>;\n const candidate = payload.name ?? payload.toolName;\n return typeof candidate === \"string\" && candidate.length > 0 ? candidate : undefined;\n}\n\n/**\n * Best-effort extraction of tool arguments from standard MCP params.\n */\nfunction extractToolArgs(params: unknown): unknown {\n if (!params || typeof params !== \"object\") {\n return undefined;\n }\n\n const payload = params as Record<string, unknown>;\n if (\"arguments\" in payload) {\n return payload.arguments;\n }\n\n if (\"args\" in payload) {\n return payload.args;\n }\n\n if (\"input\" in payload) {\n return payload.input;\n }\n\n return undefined;\n}\n\n/**\n * Type guard for JSON-RPC error responses.\n */\nfunction isJsonRpcErrorResponse(response: unknown): response is JsonRpcErrorResponse {\n if (!response || typeof response !== \"object\") {\n return false;\n }\n\n const payload = response as Record<string, unknown>;\n return payload.jsonrpc === \"2.0\" && typeof payload.error === \"object\";\n}\n\n/**\n * Creates a standardized JSON-RPC permission error response.\n */\nfunction createPolicyError(\n request: JsonRpcRequest,\n code: GuardianViolationCode,\n reason: string,\n toolName?: string\n): JsonRpcErrorResponse {\n return {\n jsonrpc: \"2.0\",\n id: request.id ?? null,\n error: {\n code: code === \"REQUIRES_APPROVAL\" ? REQUIRES_APPROVAL_NUMERIC_CODE : PERMISSION_DENIED_NUMERIC_CODE,\n message: code,\n data: {\n reason,\n method: request.method,\n toolName\n }\n }\n };\n}\n\nfunction normalizePolicyPath(value: string): string {\n let normalized = value.trim().replaceAll(\"\\\\\", \"/\").toLowerCase();\n if (normalized.length > 1 && normalized.endsWith(\"/\")) {\n normalized = normalized.slice(0, -1);\n }\n\n return normalized;\n}\n\nfunction matchesBlockedPath(candidatePath: string, blockedPath: string): boolean {\n if (blockedPath === \"/\") {\n return candidatePath.startsWith(\"/\");\n }\n\n return candidatePath === blockedPath || candidatePath.startsWith(`${blockedPath}/`);\n}\n\nfunction collectCandidatePaths(payload: unknown, parentKey?: string): string[] {\n if (typeof payload === \"string\") {\n if (parentKey && PATH_ARG_KEYS.has(parentKey)) {\n return [payload];\n }\n\n return [];\n }\n\n if (Array.isArray(payload)) {\n return payload.flatMap((entry) => collectCandidatePaths(entry, parentKey));\n }\n\n if (!payload || typeof payload !== \"object\") {\n return [];\n }\n\n const result: string[] = [];\n for (const [key, value] of Object.entries(payload as Record<string, unknown>)) {\n result.push(...collectCandidatePaths(value, key.toLowerCase()));\n }\n\n return result;\n}\n\n/**\n * Middleware that enforces `allowedTools` policy for tool calls.\n */\nasync function enforceAllowedTools(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n if (!context.toolName) {\n return {\n allowed: false,\n reason: \"Tool call did not include a valid tool name.\"\n };\n }\n\n const toolName = context.toolName;\n\n const isAllowed = context.policy.allowedTools.some((rule) => {\n if (typeof rule === \"string\") {\n return rule === toolName;\n }\n\n return rule.test(toolName);\n });\n\n if (!isAllowed) {\n return {\n allowed: false,\n reason: `Tool '${context.toolName}' is not allowed by policy.`\n };\n }\n\n return next();\n}\n\n/**\n * Middleware that rejects requests when prompt-injection signatures are present.\n */\nasync function enforceInjectionPolicy(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>,\n keywords: readonly string[]\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n const scanResult = scanForPromptInjection(context.toolArgs, keywords);\n if (scanResult.detected) {\n const signatures = scanResult.matchedKeywords.join(\", \");\n return {\n allowed: false,\n reason: `Prompt injection signature detected: ${signatures}.`\n };\n }\n\n return next();\n}\n\n/**\n * Middleware that denies tool calls targeting blocked filesystem paths.\n */\nasync function enforceRestrictedPaths(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n const blockedPaths = context.policy.restrictedPaths\n .filter((entry) => entry.mode === \"blocked\")\n .map((entry) => normalizePolicyPath(entry.path));\n\n if (blockedPaths.length === 0) {\n return next();\n }\n\n const candidatePaths = collectCandidatePaths(context.toolArgs)\n .map((value) => normalizePolicyPath(value))\n .filter((value) => value.length > 0);\n\n for (const candidatePath of candidatePaths) {\n const matchedBlockedPath = blockedPaths.find((blockedPath) =>\n matchesBlockedPath(candidatePath, blockedPath)\n );\n\n if (matchedBlockedPath) {\n return {\n allowed: false,\n code: \"PERMISSION_DENIED\",\n reason: `Tool arguments include restricted path '${candidatePath}' blocked by policy.`\n };\n }\n }\n\n return next();\n}\n\n/**\n * Middleware that marks tool calls as requiring human approval.\n */\nasync function enforceApprovalRequired(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n if (!context.policy.approvalRequired) {\n return next();\n }\n\n return {\n allowed: false,\n code: \"REQUIRES_APPROVAL\",\n reason: \"Human approval is required by policy before executing this tool call.\"\n };\n}\n\n/**\n * Middleware that blocks tools with an open circuit.\n */\nasync function enforceCircuitState(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>,\n circuitBreaker: CircuitBreaker\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request) || !context.toolName) {\n return next();\n }\n\n const decision = circuitBreaker.canExecute(context.toolName);\n if (!decision.allowed) {\n const retryMs = Math.max(0, decision.retryAfterMs ?? 0);\n const retrySeconds = Math.ceil(retryMs / 1000);\n return {\n allowed: false,\n reason: `Circuit breaker open for tool '${context.toolName}'. Retry in ${retrySeconds}s.`\n };\n }\n\n return next();\n}\n\n/**\n * Middleware that enforces global max-calls-per-minute policy.\n */\nasync function enforceRateLimit(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>,\n rateLimiter: RateLimiter,\n nowProvider: () => number\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n const decision = rateLimiter.consume(nowProvider());\n if (!decision.allowed) {\n const retryMs = Math.max(0, decision.retryAfterMs ?? 0);\n const retrySeconds = Math.ceil(retryMs / 1000);\n return {\n allowed: false,\n reason: `Rate limit exceeded. Retry in ${retrySeconds}s.`\n };\n }\n\n return next();\n}\n\n/**\n * Core policy guard for intercepting JSON-RPC tool calls.\n *\n * This class can be used as a standalone validator (`validateRequest`) or as a\n * transport wrapper through `wrapHandler` to gate downstream request handlers.\n */\nexport class McpGuardian {\n private readonly policy: GuardianPolicy;\n\n private readonly isDryRun: boolean;\n\n private readonly logger: GuardianLogger;\n\n private readonly middlewares: GuardianMiddleware[];\n\n private readonly circuitBreaker: CircuitBreaker;\n\n private readonly injectionKeywords: readonly string[];\n\n private readonly redactToolOutputs: boolean;\n\n private readonly rateLimiter: RateLimiter;\n\n private readonly nowProvider: () => number;\n\n /**\n * Creates a guardian instance with policy and optional runtime controls.\n */\n public constructor(policy: GuardianPolicy, options: McpGuardianOptions = {}) {\n this.policy = GuardianPolicySchema.parse(policy);\n this.isDryRun = options.dryRun ?? false;\n this.logger = options.logger ?? ((violation) => console.warn(\"[mcp-warden]\", violation));\n this.circuitBreaker = new CircuitBreaker(options.circuitBreaker);\n this.injectionKeywords = options.injectionKeywords ?? DEFAULT_INJECTION_KEYWORDS;\n this.redactToolOutputs = options.redactToolOutputs ?? true;\n this.rateLimiter = new RateLimiter({\n maxCallsPerMinute: this.policy.maxCallsPerMinute\n });\n this.nowProvider = options.nowProvider ?? (() => Date.now());\n\n this.middlewares = [\n enforceAllowedTools,\n enforceRestrictedPaths,\n enforceApprovalRequired,\n async (context, next) =>\n enforceRateLimit(context, next, this.rateLimiter, this.nowProvider),\n async (context, next) => enforceInjectionPolicy(context, next, this.injectionKeywords),\n async (context, next) => enforceCircuitState(context, next, this.circuitBreaker)\n ];\n }\n\n /**\n * Registers additional middleware checks that run after the built-in policy check.\n */\n public use(middleware: GuardianMiddleware): this {\n this.middlewares.push(middleware);\n return this;\n }\n\n /**\n * Validates an incoming JSON-RPC request against the configured guardrail chain.\n */\n public async validateRequest(request: JsonRpcRequest): Promise<ValidationResult> {\n const toolName = extractToolName(request.params);\n const toolArgs = extractToolArgs(request.params);\n const context: GuardianContext = {\n request,\n policy: this.policy,\n isDryRun: this.isDryRun,\n ...(toolName ? { toolName } : {}),\n ...(toolArgs !== undefined ? { toolArgs } : {})\n };\n\n const decision = await this.runMiddlewares(context);\n if (decision.allowed) {\n return { isAllowed: true };\n }\n\n const violationCode = decision.code ?? \"PERMISSION_DENIED\";\n const violation: GuardianViolation = {\n code: violationCode,\n reason:\n decision.reason ??\n (violationCode === \"REQUIRES_APPROVAL\"\n ? \"Human approval is required before executing this request.\"\n : \"Request violated guardian policy.\"),\n method: request.method,\n ...(toolName ? { toolName } : {})\n };\n\n this.logger(violation);\n if (this.isDryRun) {\n return {\n isAllowed: true,\n violation\n };\n }\n\n return {\n isAllowed: false,\n violation,\n error: createPolicyError(request, violation.code, violation.reason, toolName)\n };\n }\n\n /**\n * Wraps a JSON-RPC handler and blocks requests that violate policy.\n */\n public wrapHandler<TResponse>(\n handler: (request: JsonRpcRequest) => Promise<TResponse | JsonRpcErrorResponse>\n ): (request: JsonRpcRequest) => Promise<TResponse | JsonRpcErrorResponse> {\n return async (request: JsonRpcRequest): Promise<TResponse | JsonRpcErrorResponse> => {\n const toolName = extractToolName(request.params);\n const shouldTrackCircuit = isToolCallRequest(request) && !!toolName;\n\n const result = await this.validateRequest(request);\n if (!result.isAllowed && result.error) {\n return result.error;\n }\n\n try {\n const response = await handler(request);\n\n if (shouldTrackCircuit && toolName) {\n if (isJsonRpcErrorResponse(response)) {\n this.circuitBreaker.recordFailure(toolName);\n } else {\n this.circuitBreaker.recordSuccess(toolName);\n }\n }\n\n if (!this.redactToolOutputs || !isToolCallRequest(request)) {\n return response;\n }\n\n return redactSensitiveData(response);\n } catch (error) {\n if (shouldTrackCircuit && toolName) {\n this.circuitBreaker.recordFailure(toolName);\n }\n\n throw error;\n }\n };\n }\n\n /**\n * Runs middleware chain using a deterministic Koa-style composition model.\n */\n private async runMiddlewares(context: GuardianContext): Promise<MiddlewareDecision> {\n const middlewares = this.middlewares;\n\n const dispatch = async (index: number): Promise<MiddlewareDecision> => {\n if (index >= middlewares.length) {\n return { allowed: true };\n }\n\n const middleware = middlewares[index];\n if (!middleware) {\n return { allowed: true };\n }\n\n return middleware(context, () => dispatch(index + 1));\n };\n\n return dispatch(0);\n }\n}"]}
1
+ {"version":3,"file":"interceptor.js","sourceRoot":"","sources":["../../src/core/interceptor.ts"],"names":[],"mappings":"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","sourcesContent":["import { GuardianPolicySchema, type GuardianPolicy } from \"../types/policy.js\";\nimport {\n CircuitBreaker,\n type CircuitBreakerOptions\n} from \"../security/circuit-breaker.js\";\nimport {\n DEFAULT_INJECTION_KEYWORDS,\n scanForPromptInjection\n} from \"../security/injection-scanner.js\";\nimport { redactSensitiveData } from \"../security/pii-redactor.js\";\nimport { RateLimiter } from \"../security/rate-limiter.js\";\n\n/**\n * JSON-RPC request identifier shape.\n */\nexport type JsonRpcId = string | number | null;\n\n/**\n * Minimal JSON-RPC 2.0 request payload used by MCP transports.\n */\nexport interface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id?: JsonRpcId;\n method: string;\n params?: unknown;\n}\n\n/**\n * Standardized JSON-RPC 2.0 error object.\n */\nexport interface JsonRpcError {\n code: number;\n message: string;\n data?: Record<string, unknown>;\n}\n\n/**\n * Standardized JSON-RPC 2.0 error response payload.\n */\nexport interface JsonRpcErrorResponse {\n jsonrpc: \"2.0\";\n id: JsonRpcId;\n error: JsonRpcError;\n}\n\nexport type GuardianViolationCode = \"PERMISSION_DENIED\" | \"REQUIRES_APPROVAL\";\n\n/**\n * Security violation metadata emitted by the guardian engine.\n */\nexport interface GuardianViolation {\n code: GuardianViolationCode;\n reason: string;\n method: string;\n toolName?: string;\n}\n\n/**\n * Validation result produced by the guardian engine.\n */\nexport interface ValidationResult {\n isAllowed: boolean;\n error?: JsonRpcErrorResponse;\n violation?: GuardianViolation;\n}\n\n/**\n * Decision object returned by guardian middleware.\n */\nexport interface MiddlewareDecision {\n allowed: boolean;\n code?: GuardianViolationCode;\n reason?: string;\n}\n\n/**\n * Execution context provided to each middleware stage.\n */\nexport interface GuardianContext {\n readonly request: JsonRpcRequest;\n readonly policy: GuardianPolicy;\n readonly isDryRun: boolean;\n readonly toolName?: string;\n readonly toolArgs?: unknown;\n}\n\n/**\n * Async middleware function signature used by the guardian engine.\n */\nexport type GuardianMiddleware = (\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>\n) => Promise<MiddlewareDecision>;\n\n/**\n * Logging function for policy violations.\n */\nexport type GuardianLogger = (violation: GuardianViolation) => void;\n\n/**\n * Metrics snapshot emitted after each validated request.\n */\nexport interface GuardianMetrics {\n /** Unix timestamp (ms) when the request was processed. */\n timestamp: number;\n /** JSON-RPC method name. */\n method: string;\n /** Tool name if this was a tools/call request. Undefined for non-tool-call methods. */\n toolName: string | undefined;\n /** Whether the request was allowed. */\n allowed: boolean;\n /** Violation code when blocked. Undefined when allowed. */\n violationCode: GuardianViolationCode | undefined;\n /** Processing time in milliseconds. */\n durationMs: number;\n}\n\n/**\n * Callback invoked after every request validation with observability data.\n */\nexport type GuardianMetricsHook = (metrics: GuardianMetrics) => void;\n\n/**\n * Per-tool rate-limit override. Takes precedence over the global\n * `maxCallsPerMinute` when the tool name matches.\n */\nexport interface ToolRateLimit {\n /** Exact tool name or regex pattern this override applies to. */\n tool: string | RegExp;\n /** Max calls per minute for this tool. */\n maxCallsPerMinute: number;\n}\n\n/**\n * Configuration object for McpGuardian construction.\n */\nexport interface McpGuardianOptions {\n dryRun?: boolean;\n logger?: GuardianLogger;\n injectionKeywords?: readonly string[];\n circuitBreaker?: CircuitBreakerOptions;\n redactToolOutputs?: boolean;\n nowProvider?: () => number;\n /** Called after every request with observability data. */\n metricsHook?: GuardianMetricsHook;\n /** Per-tool rate limit overrides. First match wins. */\n toolRateLimits?: ToolRateLimit[];\n /**\n * Maximum allowed depth of a tool arguments object.\n * Requests exceeding this are rejected. Default: 20.\n */\n maxArgDepth?: number;\n /**\n * Maximum allowed byte size of the serialized tool arguments.\n * Requests exceeding this are rejected. Default: 512 KB.\n */\n maxArgBytes?: number;\n}\n\n/**\n * Constant JSON-RPC code used for permission-denied failures.\n */\nconst PERMISSION_DENIED_NUMERIC_CODE = -32001;\nconst REQUIRES_APPROVAL_NUMERIC_CODE = -32002;\n\nconst DEFAULT_MAX_ARG_DEPTH = 20;\nconst DEFAULT_MAX_ARG_BYTES = 512 * 1024; // 512 KB\n\nconst PATH_ARG_KEYS = new Set([\n \"path\",\n \"paths\",\n \"root\",\n \"roots\",\n \"directory\",\n \"directories\",\n \"dir\",\n \"cwd\",\n \"filepath\",\n \"file_path\"\n]);\n\n/** Write-intent verbs used to detect mutating operations for read-only enforcement. */\nconst WRITE_INTENT_VERBS = new Set([\n \"write\",\n \"create\",\n \"delete\",\n \"remove\",\n \"rm\",\n \"mv\",\n \"move\",\n \"rename\",\n \"mkdir\",\n \"touch\",\n \"truncate\",\n \"append\",\n \"overwrite\",\n \"put\",\n \"patch\",\n \"post\",\n \"upload\"\n]);\n\n/**\n * Detects whether a request looks like an MCP tool call.\n */\nfunction isToolCallRequest(request: JsonRpcRequest): boolean {\n return request.method === \"tools/call\";\n}\n\n/**\n * Best-effort extraction of tool name from standard MCP params.\n */\nfunction extractToolName(params: unknown): string | undefined {\n if (!params || typeof params !== \"object\") {\n return undefined;\n }\n\n const payload = params as Record<string, unknown>;\n const candidate = payload.name ?? payload.toolName;\n return typeof candidate === \"string\" && candidate.length > 0 ? candidate : undefined;\n}\n\n/**\n * Best-effort extraction of tool arguments from standard MCP params.\n */\nfunction extractToolArgs(params: unknown): unknown {\n if (!params || typeof params !== \"object\") {\n return undefined;\n }\n\n const payload = params as Record<string, unknown>;\n if (\"arguments\" in payload) {\n return payload.arguments;\n }\n\n if (\"args\" in payload) {\n return payload.args;\n }\n\n if (\"input\" in payload) {\n return payload.input;\n }\n\n return undefined;\n}\n\n/**\n * Type guard for JSON-RPC error responses.\n */\nfunction isJsonRpcErrorResponse(response: unknown): response is JsonRpcErrorResponse {\n if (!response || typeof response !== \"object\") {\n return false;\n }\n\n const payload = response as Record<string, unknown>;\n return payload.jsonrpc === \"2.0\" && typeof payload.error === \"object\";\n}\n\n/**\n * Creates a standardized JSON-RPC permission error response.\n */\nfunction createPolicyError(\n request: JsonRpcRequest,\n code: GuardianViolationCode,\n reason: string,\n toolName?: string\n): JsonRpcErrorResponse {\n return {\n jsonrpc: \"2.0\",\n id: request.id ?? null,\n error: {\n code: code === \"REQUIRES_APPROVAL\" ? REQUIRES_APPROVAL_NUMERIC_CODE : PERMISSION_DENIED_NUMERIC_CODE,\n message: code,\n data: {\n reason,\n method: request.method,\n toolName\n }\n }\n };\n}\n\nfunction normalizePolicyPath(value: string): string {\n let normalized = value.trim().replaceAll(\"\\\\\", \"/\").toLowerCase();\n if (normalized.length > 1 && normalized.endsWith(\"/\")) {\n normalized = normalized.slice(0, -1);\n }\n\n return normalized;\n}\n\nfunction matchesBlockedPath(candidatePath: string, blockedPath: string): boolean {\n if (blockedPath === \"/\") {\n return candidatePath.startsWith(\"/\");\n }\n\n return candidatePath === blockedPath || candidatePath.startsWith(`${blockedPath}/`);\n}\n\nfunction collectCandidatePaths(payload: unknown, parentKey?: string): string[] {\n if (typeof payload === \"string\") {\n if (parentKey && PATH_ARG_KEYS.has(parentKey)) {\n return [payload];\n }\n\n return [];\n }\n\n if (Array.isArray(payload)) {\n return payload.flatMap((entry) => collectCandidatePaths(entry, parentKey));\n }\n\n if (!payload || typeof payload !== \"object\") {\n return [];\n }\n\n const result: string[] = [];\n for (const [key, value] of Object.entries(payload as Record<string, unknown>)) {\n result.push(...collectCandidatePaths(value, key.toLowerCase()));\n }\n\n return result;\n}\n\n/**\n * Returns the depth of a nested object/array structure.\n */\nfunction measureDepth(value: unknown, current: number = 0): number {\n if (current > DEFAULT_MAX_ARG_DEPTH) {\n return current;\n }\n\n if (Array.isArray(value)) {\n let max = current;\n for (const entry of value) {\n max = Math.max(max, measureDepth(entry, current + 1));\n }\n return max;\n }\n\n if (value && typeof value === \"object\") {\n let max = current;\n for (const entry of Object.values(value as Record<string, unknown>)) {\n max = Math.max(max, measureDepth(entry, current + 1));\n }\n return max;\n }\n\n return current;\n}\n\n/**\n * Returns true if the tool name suggests a write/mutating operation.\n */\nfunction toolImpliesWrite(toolName: string): boolean {\n const lower = toolName.toLowerCase();\n for (const verb of WRITE_INTENT_VERBS) {\n if (lower.includes(verb)) {\n return true;\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Built-in middleware\n// ---------------------------------------------------------------------------\n\n/**\n * Middleware that enforces `allowedTools` policy for tool calls.\n */\nasync function enforceAllowedTools(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n if (!context.toolName) {\n return {\n allowed: false,\n reason: \"Tool call did not include a valid tool name.\"\n };\n }\n\n const toolName = context.toolName;\n\n const isAllowed = context.policy.allowedTools.some((rule) => {\n if (typeof rule === \"string\") {\n return rule === toolName;\n }\n\n return rule.test(toolName);\n });\n\n if (!isAllowed) {\n return {\n allowed: false,\n reason: `Tool '${context.toolName}' is not allowed by policy.`\n };\n }\n\n return next();\n}\n\n/**\n * Middleware that rejects requests when prompt-injection signatures are present.\n */\nasync function enforceInjectionPolicy(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>,\n keywords: readonly string[]\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n const scanResult = scanForPromptInjection(context.toolArgs, keywords);\n if (scanResult.detected) {\n const signatures = scanResult.matchedKeywords.join(\", \");\n return {\n allowed: false,\n reason: `Prompt injection signature detected: ${signatures}.`\n };\n }\n\n return next();\n}\n\n/**\n * Middleware that denies tool calls targeting blocked filesystem paths, and\n * denies write-intent tool calls targeting read-only filesystem paths.\n */\nasync function enforceRestrictedPaths(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n const { restrictedPaths } = context.policy;\n if (restrictedPaths.length === 0) {\n return next();\n }\n\n const blockedPaths = restrictedPaths\n .filter((entry) => entry.mode === \"blocked\")\n .map((entry) => normalizePolicyPath(entry.path));\n\n const readOnlyPaths = restrictedPaths\n .filter((entry) => entry.mode === \"read-only\")\n .map((entry) => normalizePolicyPath(entry.path));\n\n const candidatePaths = collectCandidatePaths(context.toolArgs)\n .map((value) => normalizePolicyPath(value))\n .filter((value) => value.length > 0);\n\n for (const candidatePath of candidatePaths) {\n // Blocked paths: all access denied\n const matchedBlocked = blockedPaths.find((blocked) =>\n matchesBlockedPath(candidatePath, blocked)\n );\n if (matchedBlocked) {\n return {\n allowed: false,\n code: \"PERMISSION_DENIED\",\n reason: `Tool arguments include restricted path '${candidatePath}' blocked by policy.`\n };\n }\n\n // Read-only paths: write-intent operations denied\n if (context.toolName && toolImpliesWrite(context.toolName)) {\n const matchedReadOnly = readOnlyPaths.find((ro) =>\n matchesBlockedPath(candidatePath, ro)\n );\n if (matchedReadOnly) {\n return {\n allowed: false,\n code: \"PERMISSION_DENIED\",\n reason: `Tool '${context.toolName}' attempts a write operation on read-only path '${candidatePath}'.`\n };\n }\n }\n }\n\n return next();\n}\n\n/**\n * Middleware that marks tool calls as requiring human approval.\n */\nasync function enforceApprovalRequired(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n if (!context.policy.approvalRequired) {\n return next();\n }\n\n return {\n allowed: false,\n code: \"REQUIRES_APPROVAL\",\n reason: \"Human approval is required by policy before executing this tool call.\"\n };\n}\n\n/**\n * Middleware that blocks tools with an open circuit.\n */\nasync function enforceCircuitState(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>,\n circuitBreaker: CircuitBreaker,\n nowProvider: () => number\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request) || !context.toolName) {\n return next();\n }\n\n const decision = circuitBreaker.canExecute(context.toolName, nowProvider());\n if (!decision.allowed) {\n const retryMs = Math.max(0, decision.retryAfterMs ?? 0);\n const retrySeconds = Math.ceil(retryMs / 1000);\n return {\n allowed: false,\n reason: `Circuit breaker open for tool '${context.toolName}'. Retry in ${retrySeconds}s.`\n };\n }\n\n return next();\n}\n\n/**\n * Middleware that enforces global and per-tool max-calls-per-minute policy.\n */\nasync function enforceRateLimit(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>,\n globalLimiter: RateLimiter,\n toolLimiters: Map<string, RateLimiter>,\n toolRateLimits: ToolRateLimit[],\n nowProvider: () => number\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request)) {\n return next();\n }\n\n const now = nowProvider();\n\n // Per-tool rate limit (first matching rule wins)\n if (context.toolName) {\n const toolName = context.toolName;\n const matchingRule = toolRateLimits.find((rule) => {\n if (typeof rule.tool === \"string\") {\n return rule.tool === toolName;\n }\n return rule.tool.test(toolName);\n });\n\n if (matchingRule) {\n let limiter = toolLimiters.get(toolName);\n if (!limiter) {\n limiter = new RateLimiter({ maxCallsPerMinute: matchingRule.maxCallsPerMinute });\n toolLimiters.set(toolName, limiter);\n }\n\n const decision = limiter.consume(now);\n if (!decision.allowed) {\n const retrySeconds = Math.ceil(Math.max(0, decision.retryAfterMs ?? 0) / 1000);\n return {\n allowed: false,\n reason: `Per-tool rate limit exceeded for '${toolName}'. Retry in ${retrySeconds}s.`\n };\n }\n\n return next();\n }\n }\n\n // Global rate limit\n const decision = globalLimiter.consume(now);\n if (!decision.allowed) {\n const retrySeconds = Math.ceil(Math.max(0, decision.retryAfterMs ?? 0) / 1000);\n return {\n allowed: false,\n reason: `Rate limit exceeded. Retry in ${retrySeconds}s.`\n };\n }\n\n return next();\n}\n\n/**\n * Middleware that rejects payloads exceeding depth or byte-size limits.\n */\nasync function enforceInputLimits(\n context: GuardianContext,\n next: () => Promise<MiddlewareDecision>,\n maxDepth: number,\n maxBytes: number\n): Promise<MiddlewareDecision> {\n if (!isToolCallRequest(context.request) || context.toolArgs === undefined) {\n return next();\n }\n\n const depth = measureDepth(context.toolArgs);\n if (depth > maxDepth) {\n return {\n allowed: false,\n reason: `Tool arguments exceed maximum nesting depth of ${maxDepth}.`\n };\n }\n\n try {\n const serialized = JSON.stringify(context.toolArgs);\n if (serialized.length > maxBytes) {\n return {\n allowed: false,\n reason: `Tool arguments exceed maximum size of ${maxBytes} bytes.`\n };\n }\n } catch {\n return {\n allowed: false,\n reason: \"Tool arguments could not be serialized for size validation.\"\n };\n }\n\n return next();\n}\n\n// ---------------------------------------------------------------------------\n// McpGuardian\n// ---------------------------------------------------------------------------\n\n/**\n * Core policy guard for intercepting JSON-RPC tool calls.\n *\n * This class can be used as a standalone validator (`validateRequest`) or as a\n * transport wrapper through `wrapHandler` to gate downstream request handlers.\n */\nexport class McpGuardian {\n private readonly policy: GuardianPolicy;\n\n private readonly isDryRun: boolean;\n\n private readonly logger: GuardianLogger;\n\n private readonly middlewares: GuardianMiddleware[];\n\n private readonly circuitBreaker: CircuitBreaker;\n\n private readonly injectionKeywords: readonly string[];\n\n private readonly redactToolOutputs: boolean;\n\n private readonly rateLimiter: RateLimiter;\n\n private readonly toolLimiters: Map<string, RateLimiter>;\n\n private readonly nowProvider: () => number;\n\n private readonly metricsHook: GuardianMetricsHook | undefined;\n\n /**\n * Creates a guardian instance with policy and optional runtime controls.\n */\n public constructor(policy: GuardianPolicy, options: McpGuardianOptions = {}) {\n this.policy = GuardianPolicySchema.parse(policy);\n this.isDryRun = options.dryRun ?? false;\n this.logger = options.logger ?? ((violation) => console.warn(\"[mcp-warden]\", violation));\n this.circuitBreaker = new CircuitBreaker(options.circuitBreaker);\n this.injectionKeywords = options.injectionKeywords ?? DEFAULT_INJECTION_KEYWORDS;\n this.redactToolOutputs = options.redactToolOutputs ?? true;\n this.rateLimiter = new RateLimiter({ maxCallsPerMinute: this.policy.maxCallsPerMinute });\n this.toolLimiters = new Map<string, RateLimiter>();\n this.nowProvider = options.nowProvider ?? (() => Date.now());\n this.metricsHook = options.metricsHook;\n\n const toolRateLimits = options.toolRateLimits ?? [];\n const maxDepth = options.maxArgDepth ?? DEFAULT_MAX_ARG_DEPTH;\n const maxBytes = options.maxArgBytes ?? DEFAULT_MAX_ARG_BYTES;\n\n this.middlewares = [\n enforceAllowedTools,\n (context, next) => enforceInputLimits(context, next, maxDepth, maxBytes),\n enforceRestrictedPaths,\n enforceApprovalRequired,\n (context, next) =>\n enforceRateLimit(\n context,\n next,\n this.rateLimiter,\n this.toolLimiters,\n toolRateLimits,\n this.nowProvider\n ),\n (context, next) => enforceInjectionPolicy(context, next, this.injectionKeywords),\n (context, next) => enforceCircuitState(context, next, this.circuitBreaker, this.nowProvider)\n ];\n }\n\n /**\n * Registers additional middleware checks that run after the built-in policy checks.\n */\n public use(middleware: GuardianMiddleware): this {\n this.middlewares.push(middleware);\n return this;\n }\n\n /**\n * Validates an incoming JSON-RPC request against the configured guardrail chain.\n */\n public async validateRequest(request: JsonRpcRequest): Promise<ValidationResult> {\n const start = this.nowProvider();\n const toolName = extractToolName(request.params);\n const toolArgs = extractToolArgs(request.params);\n const context: GuardianContext = {\n request,\n policy: this.policy,\n isDryRun: this.isDryRun,\n ...(toolName ? { toolName } : {}),\n ...(toolArgs !== undefined ? { toolArgs } : {})\n };\n\n const decision = await this.runMiddlewares(context);\n const durationMs = this.nowProvider() - start;\n\n if (decision.allowed) {\n this.metricsHook?.({\n timestamp: start,\n method: request.method,\n toolName,\n allowed: true,\n violationCode: undefined,\n durationMs\n });\n return { isAllowed: true };\n }\n\n const violationCode = decision.code ?? \"PERMISSION_DENIED\";\n const violation: GuardianViolation = {\n code: violationCode,\n reason:\n decision.reason ??\n (violationCode === \"REQUIRES_APPROVAL\"\n ? \"Human approval is required before executing this request.\"\n : \"Request violated guardian policy.\"),\n method: request.method,\n ...(toolName ? { toolName } : {})\n };\n\n this.logger(violation);\n this.metricsHook?.({\n timestamp: start,\n method: request.method,\n toolName,\n allowed: false,\n violationCode: violationCode,\n durationMs\n });\n\n if (this.isDryRun) {\n return { isAllowed: true, violation };\n }\n\n return {\n isAllowed: false,\n violation,\n error: createPolicyError(request, violation.code, violation.reason, toolName)\n };\n }\n\n /**\n * Wraps a JSON-RPC handler and blocks requests that violate policy.\n */\n public wrapHandler<TResponse>(\n handler: (request: JsonRpcRequest) => Promise<TResponse | JsonRpcErrorResponse>\n ): (request: JsonRpcRequest) => Promise<TResponse | JsonRpcErrorResponse> {\n return async (request: JsonRpcRequest): Promise<TResponse | JsonRpcErrorResponse> => {\n const toolName = extractToolName(request.params);\n const shouldTrackCircuit = isToolCallRequest(request) && !!toolName;\n const now = this.nowProvider();\n\n const result = await this.validateRequest(request);\n if (!result.isAllowed && result.error) {\n return result.error;\n }\n\n try {\n const response = await handler(request);\n\n if (shouldTrackCircuit && toolName) {\n if (isJsonRpcErrorResponse(response)) {\n this.circuitBreaker.recordFailure(toolName, now);\n } else {\n this.circuitBreaker.recordSuccess(toolName, now);\n }\n }\n\n if (!this.redactToolOutputs || !isToolCallRequest(request)) {\n return response;\n }\n\n return redactSensitiveData(response);\n } catch (error) {\n if (shouldTrackCircuit && toolName) {\n this.circuitBreaker.recordFailure(toolName, now);\n }\n\n throw error;\n }\n };\n }\n\n /**\n * Runs middleware chain using a deterministic Koa-style composition model.\n */\n private async runMiddlewares(context: GuardianContext): Promise<MiddlewareDecision> {\n const middlewares = this.middlewares;\n\n const dispatch = async (index: number): Promise<MiddlewareDecision> => {\n if (index >= middlewares.length) {\n return { allowed: true };\n }\n\n const middleware = middlewares[index];\n if (!middleware) {\n return { allowed: true };\n }\n\n return middleware(context, () => dispatch(index + 1));\n };\n\n return dispatch(0);\n }\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Public API for policy validation primitives exposed by mcp-warden.
3
3
  */
4
4
  export { GuardianPolicySchema, GuardianPolicySchema as guardianPolicySchema, PathRestrictionModeSchema, PathRestrictionSchema, ToolRuleSchema, type GuardianPolicy, type PathRestriction } from "./types/policy.js";
5
- export { McpGuardian, type GuardianContext, type GuardianLogger, type GuardianMiddleware, type GuardianViolation, type JsonRpcError, type JsonRpcErrorResponse, type JsonRpcId, type JsonRpcRequest, type McpGuardianOptions, type MiddlewareDecision, type ValidationResult } from "./core/interceptor.js";
5
+ export { McpGuardian, type GuardianContext, type GuardianLogger, type GuardianMetrics, type GuardianMetricsHook, type GuardianMiddleware, type GuardianViolation, type JsonRpcError, type JsonRpcErrorResponse, type JsonRpcId, type JsonRpcRequest, type McpGuardianOptions, type MiddlewareDecision, type ToolRateLimit, type ValidationResult } from "./core/interceptor.js";
6
6
  export { CircuitBreaker, type CircuitBreakerOptions, type CircuitDecision, type CircuitState } from "./security/circuit-breaker.js";
7
7
  export { DEFAULT_INJECTION_KEYWORDS, scanForPromptInjection, type InjectionScanResult } from "./security/injection-scanner.js";
8
8
  export { REDACTION_TOKEN, redactSensitiveData as redactPii, redactSensitiveData, redactSensitiveText, type RedactionSummary } from "./security/pii-redactor.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,IAAI,oBAAoB,EAC5C,yBAAyB,EACzB,qBAAqB,EACrB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,EACtB,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,eAAe,EACf,mBAAmB,IAAI,SAAS,EAChC,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,WAAW,EACX,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,IAAI,oBAAoB,EAC5C,yBAAyB,EACzB,qBAAqB,EACrB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,EACtB,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,eAAe,EACf,mBAAmB,IAAI,SAAS,EAChC,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,WAAW,EACX,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,IAAI,oBAAoB,EAC5C,yBAAyB,EACzB,qBAAqB,EACrB,cAAc,EAGf,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,WAAW,EAYZ,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,cAAc,EAIf,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,EAEvB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,eAAe,EACf,mBAAmB,IAAI,SAAS,EAChC,mBAAmB,EACnB,mBAAmB,EAEpB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,WAAW,EAGZ,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * Public API for policy validation primitives exposed by mcp-warden.\n */\nexport {\n GuardianPolicySchema,\n GuardianPolicySchema as guardianPolicySchema,\n PathRestrictionModeSchema,\n PathRestrictionSchema,\n ToolRuleSchema,\n type GuardianPolicy,\n type PathRestriction\n} from \"./types/policy.js\";\n\nexport {\n McpGuardian,\n type GuardianContext,\n type GuardianLogger,\n type GuardianMiddleware,\n type GuardianViolation,\n type JsonRpcError,\n type JsonRpcErrorResponse,\n type JsonRpcId,\n type JsonRpcRequest,\n type McpGuardianOptions,\n type MiddlewareDecision,\n type ValidationResult\n} from \"./core/interceptor.js\";\n\nexport {\n CircuitBreaker,\n type CircuitBreakerOptions,\n type CircuitDecision,\n type CircuitState\n} from \"./security/circuit-breaker.js\";\n\nexport {\n DEFAULT_INJECTION_KEYWORDS,\n scanForPromptInjection,\n type InjectionScanResult\n} from \"./security/injection-scanner.js\";\n\nexport {\n REDACTION_TOKEN,\n redactSensitiveData as redactPii,\n redactSensitiveData,\n redactSensitiveText,\n type RedactionSummary\n} from \"./security/pii-redactor.js\";\n\nexport {\n RateLimiter,\n type RateLimitDecision,\n type RateLimiterOptions\n} from \"./security/rate-limiter.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,IAAI,oBAAoB,EAC5C,yBAAyB,EACzB,qBAAqB,EACrB,cAAc,EAGf,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,WAAW,EAeZ,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,cAAc,EAIf,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,EAEvB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,eAAe,EACf,mBAAmB,IAAI,SAAS,EAChC,mBAAmB,EACnB,mBAAmB,EAEpB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,WAAW,EAGZ,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * Public API for policy validation primitives exposed by mcp-warden.\n */\nexport {\n GuardianPolicySchema,\n GuardianPolicySchema as guardianPolicySchema,\n PathRestrictionModeSchema,\n PathRestrictionSchema,\n ToolRuleSchema,\n type GuardianPolicy,\n type PathRestriction\n} from \"./types/policy.js\";\n\nexport {\n McpGuardian,\n type GuardianContext,\n type GuardianLogger,\n type GuardianMetrics,\n type GuardianMetricsHook,\n type GuardianMiddleware,\n type GuardianViolation,\n type JsonRpcError,\n type JsonRpcErrorResponse,\n type JsonRpcId,\n type JsonRpcRequest,\n type McpGuardianOptions,\n type MiddlewareDecision,\n type ToolRateLimit,\n type ValidationResult\n} from \"./core/interceptor.js\";\n\nexport {\n CircuitBreaker,\n type CircuitBreakerOptions,\n type CircuitDecision,\n type CircuitState\n} from \"./security/circuit-breaker.js\";\n\nexport {\n DEFAULT_INJECTION_KEYWORDS,\n scanForPromptInjection,\n type InjectionScanResult\n} from \"./security/injection-scanner.js\";\n\nexport {\n REDACTION_TOKEN,\n redactSensitiveData as redactPii,\n redactSensitiveData,\n redactSensitiveText,\n type RedactionSummary\n} from \"./security/pii-redactor.js\";\n\nexport {\n RateLimiter,\n type RateLimitDecision,\n type RateLimiterOptions\n} from \"./security/rate-limiter.js\";\n"]}
@@ -4,6 +4,11 @@
4
4
  export interface CircuitBreakerOptions {
5
5
  threshold?: number;
6
6
  cooldownMs?: number;
7
+ /**
8
+ * How long after a circuit last had activity before its state entry is
9
+ * evicted from memory. Defaults to 10 minutes. Set to 0 to disable cleanup.
10
+ */
11
+ stateTtlMs?: number;
7
12
  }
8
13
  /**
9
14
  * Per-tool circuit state snapshot.
@@ -21,10 +26,13 @@ export interface CircuitDecision {
21
26
  }
22
27
  /**
23
28
  * Simple per-tool circuit breaker that opens after repeated failures.
29
+ * Stale entries are evicted after `stateTtlMs` of inactivity to prevent
30
+ * unbounded memory growth for ephemeral tool names.
24
31
  */
25
32
  export declare class CircuitBreaker {
26
33
  private readonly threshold;
27
34
  private readonly cooldownMs;
35
+ private readonly stateTtlMs;
28
36
  private readonly states;
29
37
  /**
30
38
  * Creates a circuit-breaker instance.
@@ -37,7 +45,7 @@ export declare class CircuitBreaker {
37
45
  /**
38
46
  * Records a successful execution and resets failure counters.
39
47
  */
40
- recordSuccess(toolName: string): void;
48
+ recordSuccess(toolName: string, now?: number): void;
41
49
  /**
42
50
  * Records a failed execution and opens the circuit when threshold is reached.
43
51
  */
@@ -46,5 +54,13 @@ export declare class CircuitBreaker {
46
54
  * Returns a copy of current state for observability.
47
55
  */
48
56
  getState(toolName: string): CircuitState;
57
+ /**
58
+ * Returns the number of tool entries currently tracked.
59
+ */
60
+ get size(): number;
61
+ /**
62
+ * Removes entries that have been idle longer than `stateTtlMs`.
63
+ */
64
+ private evictStaleEntries;
49
65
  }
50
66
  //# sourceMappingURL=circuit-breaker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/security/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IAEnD;;OAEG;gBACgB,OAAO,GAAE,qBAA0B;IAMtD;;OAEG;IACI,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,eAAe;IAoB9E;;OAEG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAO5C;;OAEG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI;IAqBtE;;OAEG;IACI,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY;CAShD"}
1
+ {"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/security/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAOD;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IAEnD;;OAEG;gBACgB,OAAO,GAAE,qBAA0B;IAOtD;;OAEG;IACI,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,eAAe;IAwB9E;;OAEG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI;IAQtE;;OAEG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI;IAwBtE;;OAEG;IACI,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY;IAO/C;;OAEG;IACH,IAAW,IAAI,IAAI,MAAM,CAExB;IAED;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAW1B"}
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * Simple per-tool circuit breaker that opens after repeated failures.
3
+ * Stale entries are evicted after `stateTtlMs` of inactivity to prevent
4
+ * unbounded memory growth for ephemeral tool names.
3
5
  */
4
6
  export class CircuitBreaker {
5
7
  threshold;
6
8
  cooldownMs;
9
+ stateTtlMs;
7
10
  states;
8
11
  /**
9
12
  * Creates a circuit-breaker instance.
@@ -11,12 +14,14 @@ export class CircuitBreaker {
11
14
  constructor(options = {}) {
12
15
  this.threshold = Math.max(1, options.threshold ?? 5);
13
16
  this.cooldownMs = Math.max(1, options.cooldownMs ?? 60_000);
17
+ this.stateTtlMs = options.stateTtlMs ?? 10 * 60_000;
14
18
  this.states = new Map();
15
19
  }
16
20
  /**
17
21
  * Determines if a tool is currently allowed to execute.
18
22
  */
19
23
  canExecute(toolName, now = Date.now()) {
24
+ this.evictStaleEntries(now);
20
25
  const state = this.states.get(toolName);
21
26
  if (!state || state.openUntil === null) {
22
27
  return { allowed: true };
@@ -24,10 +29,12 @@ export class CircuitBreaker {
24
29
  if (now >= state.openUntil) {
25
30
  this.states.set(toolName, {
26
31
  consecutiveFailures: 0,
27
- openUntil: null
32
+ openUntil: null,
33
+ lastUsedAt: now
28
34
  });
29
35
  return { allowed: true };
30
36
  }
37
+ state.lastUsedAt = now;
31
38
  return {
32
39
  allowed: false,
33
40
  retryAfterMs: state.openUntil - now
@@ -36,10 +43,11 @@ export class CircuitBreaker {
36
43
  /**
37
44
  * Records a successful execution and resets failure counters.
38
45
  */
39
- recordSuccess(toolName) {
46
+ recordSuccess(toolName, now = Date.now()) {
40
47
  this.states.set(toolName, {
41
48
  consecutiveFailures: 0,
42
- openUntil: null
49
+ openUntil: null,
50
+ lastUsedAt: now
43
51
  });
44
52
  }
45
53
  /**
@@ -48,19 +56,22 @@ export class CircuitBreaker {
48
56
  recordFailure(toolName, now = Date.now()) {
49
57
  const current = this.states.get(toolName) ?? {
50
58
  consecutiveFailures: 0,
51
- openUntil: null
59
+ openUntil: null,
60
+ lastUsedAt: now
52
61
  };
53
62
  const consecutiveFailures = current.consecutiveFailures + 1;
54
63
  if (consecutiveFailures >= this.threshold) {
55
64
  this.states.set(toolName, {
56
65
  consecutiveFailures,
57
- openUntil: now + this.cooldownMs
66
+ openUntil: now + this.cooldownMs,
67
+ lastUsedAt: now
58
68
  });
59
69
  return;
60
70
  }
61
71
  this.states.set(toolName, {
62
72
  consecutiveFailures,
63
- openUntil: null
73
+ openUntil: null,
74
+ lastUsedAt: now
64
75
  });
65
76
  }
66
77
  /**
@@ -69,11 +80,27 @@ export class CircuitBreaker {
69
80
  getState(toolName) {
70
81
  const state = this.states.get(toolName);
71
82
  return state
72
- ? { ...state }
73
- : {
74
- consecutiveFailures: 0,
75
- openUntil: null
76
- };
83
+ ? { consecutiveFailures: state.consecutiveFailures, openUntil: state.openUntil }
84
+ : { consecutiveFailures: 0, openUntil: null };
85
+ }
86
+ /**
87
+ * Returns the number of tool entries currently tracked.
88
+ */
89
+ get size() {
90
+ return this.states.size;
91
+ }
92
+ /**
93
+ * Removes entries that have been idle longer than `stateTtlMs`.
94
+ */
95
+ evictStaleEntries(now) {
96
+ if (this.stateTtlMs <= 0) {
97
+ return;
98
+ }
99
+ for (const [key, entry] of this.states) {
100
+ if (now - entry.lastUsedAt > this.stateTtlMs) {
101
+ this.states.delete(key);
102
+ }
103
+ }
77
104
  }
78
105
  }
79
106
  //# sourceMappingURL=circuit-breaker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/security/circuit-breaker.ts"],"names":[],"mappings":"AAwBA;;GAEG;AACH,MAAM,OAAO,cAAc;IACR,SAAS,CAAS;IAElB,UAAU,CAAS;IAEnB,MAAM,CAA4B;IAEnD;;OAEG;IACH,YAAmB,UAAiC,EAAE;QACpD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC;QACrD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,CAAC;QAC5D,IAAI,CAAC,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IAChD,CAAC;IAED;;OAEG;IACI,UAAU,CAAC,QAAgB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QAED,IAAI,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE;gBACxB,mBAAmB,EAAE,CAAC;gBACtB,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;YACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QAED,OAAO;YACL,OAAO,EAAE,KAAK;YACd,YAAY,EAAE,KAAK,CAAC,SAAS,GAAG,GAAG;SACpC,CAAC;IACJ,CAAC;IAED;;OAEG;IACI,aAAa,CAAC,QAAgB;QACnC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE;YACxB,mBAAmB,EAAE,CAAC;YACtB,SAAS,EAAE,IAAI;SAChB,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACI,aAAa,CAAC,QAAgB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI;YAC3C,mBAAmB,EAAE,CAAC;YACtB,SAAS,EAAE,IAAI;SAChB,CAAC;QAEF,MAAM,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,GAAG,CAAC,CAAC;QAC5D,IAAI,mBAAmB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC1C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE;gBACxB,mBAAmB;gBACnB,SAAS,EAAE,GAAG,GAAG,IAAI,CAAC,UAAU;aACjC,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE;YACxB,mBAAmB;YACnB,SAAS,EAAE,IAAI;SAChB,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACI,QAAQ,CAAC,QAAgB;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxC,OAAO,KAAK;YACV,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE;YACd,CAAC,CAAC;gBACE,mBAAmB,EAAE,CAAC;gBACtB,SAAS,EAAE,IAAI;aAChB,CAAC;IACR,CAAC;CACF","sourcesContent":["/**\n * Runtime configuration for circuit-breaker behavior.\n */\nexport interface CircuitBreakerOptions {\n threshold?: number;\n cooldownMs?: number;\n}\n\n/**\n * Per-tool circuit state snapshot.\n */\nexport interface CircuitState {\n consecutiveFailures: number;\n openUntil: number | null;\n}\n\n/**\n * Evaluation result for whether a tool may run.\n */\nexport interface CircuitDecision {\n allowed: boolean;\n retryAfterMs?: number;\n}\n\n/**\n * Simple per-tool circuit breaker that opens after repeated failures.\n */\nexport class CircuitBreaker {\n private readonly threshold: number;\n\n private readonly cooldownMs: number;\n\n private readonly states: Map<string, CircuitState>;\n\n /**\n * Creates a circuit-breaker instance.\n */\n public constructor(options: CircuitBreakerOptions = {}) {\n this.threshold = Math.max(1, options.threshold ?? 5);\n this.cooldownMs = Math.max(1, options.cooldownMs ?? 60_000);\n this.states = new Map<string, CircuitState>();\n }\n\n /**\n * Determines if a tool is currently allowed to execute.\n */\n public canExecute(toolName: string, now: number = Date.now()): CircuitDecision {\n const state = this.states.get(toolName);\n if (!state || state.openUntil === null) {\n return { allowed: true };\n }\n\n if (now >= state.openUntil) {\n this.states.set(toolName, {\n consecutiveFailures: 0,\n openUntil: null\n });\n return { allowed: true };\n }\n\n return {\n allowed: false,\n retryAfterMs: state.openUntil - now\n };\n }\n\n /**\n * Records a successful execution and resets failure counters.\n */\n public recordSuccess(toolName: string): void {\n this.states.set(toolName, {\n consecutiveFailures: 0,\n openUntil: null\n });\n }\n\n /**\n * Records a failed execution and opens the circuit when threshold is reached.\n */\n public recordFailure(toolName: string, now: number = Date.now()): void {\n const current = this.states.get(toolName) ?? {\n consecutiveFailures: 0,\n openUntil: null\n };\n\n const consecutiveFailures = current.consecutiveFailures + 1;\n if (consecutiveFailures >= this.threshold) {\n this.states.set(toolName, {\n consecutiveFailures,\n openUntil: now + this.cooldownMs\n });\n return;\n }\n\n this.states.set(toolName, {\n consecutiveFailures,\n openUntil: null\n });\n }\n\n /**\n * Returns a copy of current state for observability.\n */\n public getState(toolName: string): CircuitState {\n const state = this.states.get(toolName);\n return state\n ? { ...state }\n : {\n consecutiveFailures: 0,\n openUntil: null\n };\n }\n}"]}
1
+ {"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/security/circuit-breaker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime configuration for circuit-breaker behavior.\n */\nexport interface CircuitBreakerOptions {\n threshold?: number;\n cooldownMs?: number;\n /**\n * How long after a circuit last had activity before its state entry is\n * evicted from memory. Defaults to 10 minutes. Set to 0 to disable cleanup.\n */\n stateTtlMs?: number;\n}\n\n/**\n * Per-tool circuit state snapshot.\n */\nexport interface CircuitState {\n consecutiveFailures: number;\n openUntil: number | null;\n}\n\n/**\n * Evaluation result for whether a tool may run.\n */\nexport interface CircuitDecision {\n allowed: boolean;\n retryAfterMs?: number;\n}\n\n/** Internal state entry with last-touched timestamp for TTL eviction. */\ninterface CircuitEntry extends CircuitState {\n lastUsedAt: number;\n}\n\n/**\n * Simple per-tool circuit breaker that opens after repeated failures.\n * Stale entries are evicted after `stateTtlMs` of inactivity to prevent\n * unbounded memory growth for ephemeral tool names.\n */\nexport class CircuitBreaker {\n private readonly threshold: number;\n\n private readonly cooldownMs: number;\n\n private readonly stateTtlMs: number;\n\n private readonly states: Map<string, CircuitEntry>;\n\n /**\n * Creates a circuit-breaker instance.\n */\n public constructor(options: CircuitBreakerOptions = {}) {\n this.threshold = Math.max(1, options.threshold ?? 5);\n this.cooldownMs = Math.max(1, options.cooldownMs ?? 60_000);\n this.stateTtlMs = options.stateTtlMs ?? 10 * 60_000;\n this.states = new Map<string, CircuitEntry>();\n }\n\n /**\n * Determines if a tool is currently allowed to execute.\n */\n public canExecute(toolName: string, now: number = Date.now()): CircuitDecision {\n this.evictStaleEntries(now);\n\n const state = this.states.get(toolName);\n if (!state || state.openUntil === null) {\n return { allowed: true };\n }\n\n if (now >= state.openUntil) {\n this.states.set(toolName, {\n consecutiveFailures: 0,\n openUntil: null,\n lastUsedAt: now\n });\n return { allowed: true };\n }\n\n state.lastUsedAt = now;\n return {\n allowed: false,\n retryAfterMs: state.openUntil - now\n };\n }\n\n /**\n * Records a successful execution and resets failure counters.\n */\n public recordSuccess(toolName: string, now: number = Date.now()): void {\n this.states.set(toolName, {\n consecutiveFailures: 0,\n openUntil: null,\n lastUsedAt: now\n });\n }\n\n /**\n * Records a failed execution and opens the circuit when threshold is reached.\n */\n public recordFailure(toolName: string, now: number = Date.now()): void {\n const current = this.states.get(toolName) ?? {\n consecutiveFailures: 0,\n openUntil: null,\n lastUsedAt: now\n };\n\n const consecutiveFailures = current.consecutiveFailures + 1;\n if (consecutiveFailures >= this.threshold) {\n this.states.set(toolName, {\n consecutiveFailures,\n openUntil: now + this.cooldownMs,\n lastUsedAt: now\n });\n return;\n }\n\n this.states.set(toolName, {\n consecutiveFailures,\n openUntil: null,\n lastUsedAt: now\n });\n }\n\n /**\n * Returns a copy of current state for observability.\n */\n public getState(toolName: string): CircuitState {\n const state = this.states.get(toolName);\n return state\n ? { consecutiveFailures: state.consecutiveFailures, openUntil: state.openUntil }\n : { consecutiveFailures: 0, openUntil: null };\n }\n\n /**\n * Returns the number of tool entries currently tracked.\n */\n public get size(): number {\n return this.states.size;\n }\n\n /**\n * Removes entries that have been idle longer than `stateTtlMs`.\n */\n private evictStaleEntries(now: number): void {\n if (this.stateTtlMs <= 0) {\n return;\n }\n\n for (const [key, entry] of this.states) {\n if (now - entry.lastUsedAt > this.stateTtlMs) {\n this.states.delete(key);\n }\n }\n }\n}\n"]}
@@ -11,6 +11,9 @@ export interface InjectionScanResult {
11
11
  }
12
12
  /**
13
13
  * Scans tool arguments for known prompt-injection signatures.
14
+ *
15
+ * Uses per-keyword compiled regexes with word-boundary anchors to reduce
16
+ * false positives compared with plain substring matching.
14
17
  */
15
18
  export declare function scanForPromptInjection(payload: unknown, keywords?: readonly string[]): InjectionScanResult;
16
19
  //# sourceMappingURL=injection-scanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"injection-scanner.d.ts","sourceRoot":"","sources":["../../src/security/injection-scanner.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,0BAA0B,+HAK7B,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,QAAQ,GAAE,SAAS,MAAM,EAA+B,GACvD,mBAAmB,CA8CrB"}
1
+ {"version":3,"file":"injection-scanner.d.ts","sourceRoot":"","sources":["../../src/security/injection-scanner.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,0BAA0B,+HAK7B,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AA0BD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,QAAQ,GAAE,SAAS,MAAM,EAA+B,GACvD,mBAAmB,CAgDrB"}
@@ -7,22 +7,49 @@ export const DEFAULT_INJECTION_KEYWORDS = [
7
7
  "disregard all prior rules",
8
8
  "override your safety policy"
9
9
  ];
10
+ /**
11
+ * Escapes a string for safe use inside a RegExp pattern.
12
+ */
13
+ function escapeRegExp(value) {
14
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15
+ }
16
+ /**
17
+ * Builds a regex that matches the keyword as a whole phrase (word-boundary
18
+ * aware) to reduce false positives caused by substring coincidences.
19
+ *
20
+ * For multi-word phrases a leading/trailing `\b` is used where the edge
21
+ * character is alphanumeric; otherwise the boundary is a start/end anchor
22
+ * or a non-alphanumeric character boundary, which avoids false matches like
23
+ * "disregard" matching the phrase "disregard all prior rules" mid-word.
24
+ */
25
+ function buildKeywordRegex(keyword) {
26
+ const escaped = escapeRegExp(keyword.trim());
27
+ // Wrap in word-boundary anchors if the phrase starts/ends with a word char.
28
+ const leading = /^\w/.test(keyword) ? "\\b" : "";
29
+ const trailing = /\w$/.test(keyword) ? "\\b" : "";
30
+ return new RegExp(`${leading}${escaped}${trailing}`, "i");
31
+ }
10
32
  /**
11
33
  * Scans tool arguments for known prompt-injection signatures.
34
+ *
35
+ * Uses per-keyword compiled regexes with word-boundary anchors to reduce
36
+ * false positives compared with plain substring matching.
12
37
  */
13
38
  export function scanForPromptInjection(payload, keywords = DEFAULT_INJECTION_KEYWORDS) {
14
- const normalizedKeywords = keywords
15
- .map((keyword) => keyword.trim().toLowerCase())
16
- .filter((keyword) => keyword.length > 0);
39
+ const patterns = keywords
40
+ .map((kw) => kw.trim())
41
+ .filter((kw) => kw.length > 0)
42
+ .map((kw) => ({ keyword: kw, regex: buildKeywordRegex(kw) }));
17
43
  const matchedKeywords = new Set();
18
44
  const seen = new WeakSet();
19
45
  const visit = (candidate) => {
20
46
  if (typeof candidate === "string") {
21
- const normalized = candidate.toLowerCase();
22
- for (const keyword of normalizedKeywords) {
23
- if (normalized.includes(keyword)) {
47
+ for (const { keyword, regex } of patterns) {
48
+ if (regex.test(candidate)) {
24
49
  matchedKeywords.add(keyword);
25
50
  }
51
+ // Reset lastIndex for global/sticky flags (not used here, but safe).
52
+ regex.lastIndex = 0;
26
53
  }
27
54
  return;
28
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"injection-scanner.js","sourceRoot":"","sources":["../../src/security/injection-scanner.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,8BAA8B;IAC9B,sBAAsB;IACtB,2BAA2B;IAC3B,6BAA6B;CACrB,CAAC;AAUX;;GAEG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAgB,EAChB,WAA8B,0BAA0B;IAExD,MAAM,kBAAkB,GAAG,QAAQ;SAChC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;SAC9C,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE3C,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;IAEnC,MAAM,KAAK,GAAG,CAAC,SAAkB,EAAQ,EAAE;QACzC,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAClC,MAAM,UAAU,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;YAC3C,KAAK,MAAM,OAAO,IAAI,kBAAkB,EAAE,CAAC;gBACzC,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;oBACjC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;gBAC9B,KAAK,CAAC,KAAK,CAAC,CAAC;YACf,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,CAAC,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAChD,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAEpB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,SAAoC,CAAC,EAAE,CAAC;YACxE,KAAK,CAAC,KAAK,CAAC,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,CAAC;IAEf,OAAO;QACL,QAAQ,EAAE,eAAe,CAAC,IAAI,GAAG,CAAC;QAClC,eAAe,EAAE,CAAC,GAAG,eAAe,CAAC;KACtC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Default signatures associated with prompt-injection attempts.\n */\nexport const DEFAULT_INJECTION_KEYWORDS = [\n \"ignore previous instructions\",\n \"now you are an admin\",\n \"disregard all prior rules\",\n \"override your safety policy\"\n] as const;\n\n/**\n * Result produced by an injection scan.\n */\nexport interface InjectionScanResult {\n detected: boolean;\n matchedKeywords: string[];\n}\n\n/**\n * Scans tool arguments for known prompt-injection signatures.\n */\nexport function scanForPromptInjection(\n payload: unknown,\n keywords: readonly string[] = DEFAULT_INJECTION_KEYWORDS\n): InjectionScanResult {\n const normalizedKeywords = keywords\n .map((keyword) => keyword.trim().toLowerCase())\n .filter((keyword) => keyword.length > 0);\n\n const matchedKeywords = new Set<string>();\n const seen = new WeakSet<object>();\n\n const visit = (candidate: unknown): void => {\n if (typeof candidate === \"string\") {\n const normalized = candidate.toLowerCase();\n for (const keyword of normalizedKeywords) {\n if (normalized.includes(keyword)) {\n matchedKeywords.add(keyword);\n }\n }\n return;\n }\n\n if (Array.isArray(candidate)) {\n for (const entry of candidate) {\n visit(entry);\n }\n return;\n }\n\n if (!candidate || typeof candidate !== \"object\") {\n return;\n }\n\n if (seen.has(candidate)) {\n return;\n }\n seen.add(candidate);\n\n for (const entry of Object.values(candidate as Record<string, unknown>)) {\n visit(entry);\n }\n };\n\n visit(payload);\n\n return {\n detected: matchedKeywords.size > 0,\n matchedKeywords: [...matchedKeywords]\n };\n}"]}
1
+ {"version":3,"file":"injection-scanner.js","sourceRoot":"","sources":["../../src/security/injection-scanner.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,8BAA8B;IAC9B,sBAAsB;IACtB,2BAA2B;IAC3B,6BAA6B;CACrB,CAAC;AAUX;;GAEG;AACH,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CAAC,OAAe;IACxC,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,4EAA4E;IAC5E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD,OAAO,IAAI,MAAM,CAAC,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAgB,EAChB,WAA8B,0BAA0B;IAExD,MAAM,QAAQ,GAAG,QAAQ;SACtB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;SACtB,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;SAC7B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,iBAAiB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAEhE,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;IAEnC,MAAM,KAAK,GAAG,CAAC,SAAkB,EAAQ,EAAE;QACzC,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAClC,KAAK,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,QAAQ,EAAE,CAAC;gBAC1C,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;oBAC1B,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC/B,CAAC;gBACD,qEAAqE;gBACrE,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;YACtB,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;gBAC9B,KAAK,CAAC,KAAK,CAAC,CAAC;YACf,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,CAAC,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAChD,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAEpB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,SAAoC,CAAC,EAAE,CAAC;YACxE,KAAK,CAAC,KAAK,CAAC,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,CAAC;IAEf,OAAO;QACL,QAAQ,EAAE,eAAe,CAAC,IAAI,GAAG,CAAC;QAClC,eAAe,EAAE,CAAC,GAAG,eAAe,CAAC;KACtC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Default signatures associated with prompt-injection attempts.\n */\nexport const DEFAULT_INJECTION_KEYWORDS = [\n \"ignore previous instructions\",\n \"now you are an admin\",\n \"disregard all prior rules\",\n \"override your safety policy\"\n] as const;\n\n/**\n * Result produced by an injection scan.\n */\nexport interface InjectionScanResult {\n detected: boolean;\n matchedKeywords: string[];\n}\n\n/**\n * Escapes a string for safe use inside a RegExp pattern.\n */\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Builds a regex that matches the keyword as a whole phrase (word-boundary\n * aware) to reduce false positives caused by substring coincidences.\n *\n * For multi-word phrases a leading/trailing `\\b` is used where the edge\n * character is alphanumeric; otherwise the boundary is a start/end anchor\n * or a non-alphanumeric character boundary, which avoids false matches like\n * \"disregard\" matching the phrase \"disregard all prior rules\" mid-word.\n */\nfunction buildKeywordRegex(keyword: string): RegExp {\n const escaped = escapeRegExp(keyword.trim());\n // Wrap in word-boundary anchors if the phrase starts/ends with a word char.\n const leading = /^\\w/.test(keyword) ? \"\\\\b\" : \"\";\n const trailing = /\\w$/.test(keyword) ? \"\\\\b\" : \"\";\n return new RegExp(`${leading}${escaped}${trailing}`, \"i\");\n}\n\n/**\n * Scans tool arguments for known prompt-injection signatures.\n *\n * Uses per-keyword compiled regexes with word-boundary anchors to reduce\n * false positives compared with plain substring matching.\n */\nexport function scanForPromptInjection(\n payload: unknown,\n keywords: readonly string[] = DEFAULT_INJECTION_KEYWORDS\n): InjectionScanResult {\n const patterns = keywords\n .map((kw) => kw.trim())\n .filter((kw) => kw.length > 0)\n .map((kw) => ({ keyword: kw, regex: buildKeywordRegex(kw) }));\n\n const matchedKeywords = new Set<string>();\n const seen = new WeakSet<object>();\n\n const visit = (candidate: unknown): void => {\n if (typeof candidate === \"string\") {\n for (const { keyword, regex } of patterns) {\n if (regex.test(candidate)) {\n matchedKeywords.add(keyword);\n }\n // Reset lastIndex for global/sticky flags (not used here, but safe).\n regex.lastIndex = 0;\n }\n return;\n }\n\n if (Array.isArray(candidate)) {\n for (const entry of candidate) {\n visit(entry);\n }\n return;\n }\n\n if (!candidate || typeof candidate !== \"object\") {\n return;\n }\n\n if (seen.has(candidate)) {\n return;\n }\n seen.add(candidate);\n\n for (const entry of Object.values(candidate as Record<string, unknown>)) {\n visit(entry);\n }\n };\n\n visit(payload);\n\n return {\n detected: matchedKeywords.size > 0,\n matchedKeywords: [...matchedKeywords]\n };\n}\n"]}
@@ -10,7 +10,7 @@ export interface RedactionSummary {
10
10
  redactedCount: number;
11
11
  }
12
12
  /**
13
- * Redacts sensitive values from a plain string.
13
+ * Redacts sensitive values from a plain string in a single regex pass.
14
14
  */
15
15
  export declare function redactSensitiveText(input: string): RedactionSummary;
16
16
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"pii-redactor.d.ts","sourceRoot":"","sources":["../../src/security/pii-redactor.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,eAAe,eAAe,CAAC;AAkB5C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAgBnE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAgClD"}
1
+ {"version":3,"file":"pii-redactor.d.ts","sourceRoot":"","sources":["../../src/security/pii-redactor.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,eAAe,eAAe,CAAC;AAyB5C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CASnE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAgClD"}
@@ -3,33 +3,34 @@
3
3
  */
4
4
  export const REDACTION_TOKEN = "[REDACTED]";
5
5
  /**
6
- * Finds common email address patterns.
6
+ * Combined pattern that matches emails, API keys, IPv4, IPv6, and phone numbers
7
+ * in a single pass for efficiency.
8
+ *
9
+ * Named capture groups identify which category matched so the replacer
10
+ * can be extended without reordering.
7
11
  */
8
- const EMAIL_REGEX = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
12
+ const COMBINED_SENSITIVE_REGEX = new RegExp([
13
+ // Email addresses
14
+ "(?<email>\\b[A-Z0-9._%+\\-]+@[A-Z0-9.\\-]+\\.[A-Z]{2,}\\b)",
15
+ // Stripe-style and generic API keys / tokens
16
+ "(?<apikey>\\b(?:sk_(?:test|live)_[A-Z0-9]{8,}|(?:sk|key|api|token)-[A-Z0-9_\\-]{8,})\\b)",
17
+ // IPv6 — full and compressed forms (must come before IPv4 to avoid partial matches)
18
+ "(?<ipv6>(?:[0-9A-F]{1,4}:){7}[0-9A-F]{1,4}|(?:[0-9A-F]{1,4}:){1,7}:|(?:[0-9A-F]{1,4}:){1,6}:[0-9A-F]{1,4}|::(?:[0-9A-F]{1,4}:){0,5}[0-9A-F]{1,4}|::)",
19
+ // IPv4 addresses with valid octet ranges
20
+ "(?<ipv4>\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b)",
21
+ // Phone numbers — E.164, US, and common international formats
22
+ "(?<phone>(?:\\+?1[\\s.\\-]?)?\\(?\\d{3}\\)?[\\s.\\-]?\\d{3}[\\s.\\-]?\\d{4})"
23
+ ].join("|"), "gi");
9
24
  /**
10
- * Finds key-like secrets with common prefixes such as sk- and key-.
11
- */
12
- const API_KEY_REGEX = /\b(?:sk|key|api|token)-[A-Z0-9_-]{8,}\b/gi;
13
- /**
14
- * Finds IPv4 addresses and excludes impossible octets.
15
- */
16
- const IPV4_REGEX = /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g;
17
- /**
18
- * Redacts sensitive values from a plain string.
25
+ * Redacts sensitive values from a plain string in a single regex pass.
19
26
  */
20
27
  export function redactSensitiveText(input) {
21
28
  let redactedCount = 0;
22
- const replacer = () => {
29
+ const value = input.replace(COMBINED_SENSITIVE_REGEX, () => {
23
30
  redactedCount += 1;
24
31
  return REDACTION_TOKEN;
25
- };
26
- let value = input.replace(EMAIL_REGEX, replacer);
27
- value = value.replace(API_KEY_REGEX, replacer);
28
- value = value.replace(IPV4_REGEX, replacer);
29
- return {
30
- value,
31
- redactedCount
32
- };
32
+ });
33
+ return { value, redactedCount };
33
34
  }
34
35
  /**
35
36
  * Recursively redacts sensitive content from arbitrary JSON-like payloads.
@@ -1 +1 @@
1
- {"version":3,"file":"pii-redactor.js","sourceRoot":"","sources":["../../src/security/pii-redactor.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,GAAG,6CAA6C,CAAC;AAElE;;GAEG;AACH,MAAM,aAAa,GAAG,2CAA2C,CAAC;AAElE;;GAEG;AACH,MAAM,UAAU,GACd,sFAAsF,CAAC;AAUzF;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,IAAI,aAAa,GAAG,CAAC,CAAC;IAEtB,MAAM,QAAQ,GAAG,GAAW,EAAE;QAC5B,aAAa,IAAI,CAAC,CAAC;QACnB,OAAO,eAAe,CAAC;IACzB,CAAC,CAAC;IAEF,IAAI,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACjD,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;IAC/C,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAE5C,OAAO;QACL,KAAK;QACL,aAAa;KACd,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAI,KAAQ;IAC7C,MAAM,IAAI,GAAG,IAAI,OAAO,EAAmB,CAAC;IAE5C,MAAM,SAAS,GAAG,CAAC,SAAkB,EAAW,EAAE;QAChD,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAClC,OAAO,mBAAmB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC;QAC9C,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QACpD,CAAC;QAED,IAAI,CAAC,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAChD,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;QAED,MAAM,WAAW,GAAG,SAAoC,CAAC;QACzD,MAAM,YAAY,GAA4B,EAAE,CAAC;QACjD,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QAElC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YACvD,YAAY,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;QAED,OAAO,YAAY,CAAC;IACtB,CAAC,CAAC;IAEF,OAAO,SAAS,CAAC,KAAK,CAAM,CAAC;AAC/B,CAAC","sourcesContent":["/**\n * Placeholder used when sensitive values are removed.\n */\nexport const REDACTION_TOKEN = \"[REDACTED]\";\n\n/**\n * Finds common email address patterns.\n */\nconst EMAIL_REGEX = /\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}\\b/gi;\n\n/**\n * Finds key-like secrets with common prefixes such as sk- and key-.\n */\nconst API_KEY_REGEX = /\\b(?:sk|key|api|token)-[A-Z0-9_-]{8,}\\b/gi;\n\n/**\n * Finds IPv4 addresses and excludes impossible octets.\n */\nconst IPV4_REGEX =\n /\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b/g;\n\n/**\n * Summary of a single text redaction operation.\n */\nexport interface RedactionSummary {\n value: string;\n redactedCount: number;\n}\n\n/**\n * Redacts sensitive values from a plain string.\n */\nexport function redactSensitiveText(input: string): RedactionSummary {\n let redactedCount = 0;\n\n const replacer = (): string => {\n redactedCount += 1;\n return REDACTION_TOKEN;\n };\n\n let value = input.replace(EMAIL_REGEX, replacer);\n value = value.replace(API_KEY_REGEX, replacer);\n value = value.replace(IPV4_REGEX, replacer);\n\n return {\n value,\n redactedCount\n };\n}\n\n/**\n * Recursively redacts sensitive content from arbitrary JSON-like payloads.\n */\nexport function redactSensitiveData<T>(value: T): T {\n const seen = new WeakMap<object, unknown>();\n\n const transform = (candidate: unknown): unknown => {\n if (typeof candidate === \"string\") {\n return redactSensitiveText(candidate).value;\n }\n\n if (Array.isArray(candidate)) {\n return candidate.map((entry) => transform(entry));\n }\n\n if (!candidate || typeof candidate !== \"object\") {\n return candidate;\n }\n\n if (seen.has(candidate)) {\n return seen.get(candidate);\n }\n\n const inputRecord = candidate as Record<string, unknown>;\n const outputRecord: Record<string, unknown> = {};\n seen.set(candidate, outputRecord);\n\n for (const [key, entry] of Object.entries(inputRecord)) {\n outputRecord[key] = transform(entry);\n }\n\n return outputRecord;\n };\n\n return transform(value) as T;\n}"]}
1
+ {"version":3,"file":"pii-redactor.js","sourceRoot":"","sources":["../../src/security/pii-redactor.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,wBAAwB,GAAG,IAAI,MAAM,CACzC;IACE,kBAAkB;IAClB,4DAA4D;IAC5D,6CAA6C;IAC7C,0FAA0F;IAC1F,oFAAoF;IACpF,sJAAsJ;IACtJ,yCAAyC;IACzC,yGAAyG;IACzG,8DAA8D;IAC9D,8EAA8E;CAC/E,CAAC,IAAI,CAAC,GAAG,CAAC,EACX,IAAI,CACL,CAAC;AAUF;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,IAAI,aAAa,GAAG,CAAC,CAAC;IAEtB,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,wBAAwB,EAAE,GAAG,EAAE;QACzD,aAAa,IAAI,CAAC,CAAC;QACnB,OAAO,eAAe,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;AAClC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAI,KAAQ;IAC7C,MAAM,IAAI,GAAG,IAAI,OAAO,EAAmB,CAAC;IAE5C,MAAM,SAAS,GAAG,CAAC,SAAkB,EAAW,EAAE;QAChD,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAClC,OAAO,mBAAmB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC;QAC9C,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QACpD,CAAC;QAED,IAAI,CAAC,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAChD,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;QAED,MAAM,WAAW,GAAG,SAAoC,CAAC;QACzD,MAAM,YAAY,GAA4B,EAAE,CAAC;QACjD,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QAElC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YACvD,YAAY,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;QAED,OAAO,YAAY,CAAC;IACtB,CAAC,CAAC;IAEF,OAAO,SAAS,CAAC,KAAK,CAAM,CAAC;AAC/B,CAAC","sourcesContent":["/**\n * Placeholder used when sensitive values are removed.\n */\nexport const REDACTION_TOKEN = \"[REDACTED]\";\n\n/**\n * Combined pattern that matches emails, API keys, IPv4, IPv6, and phone numbers\n * in a single pass for efficiency.\n *\n * Named capture groups identify which category matched so the replacer\n * can be extended without reordering.\n */\nconst COMBINED_SENSITIVE_REGEX = new RegExp(\n [\n // Email addresses\n \"(?<email>\\\\b[A-Z0-9._%+\\\\-]+@[A-Z0-9.\\\\-]+\\\\.[A-Z]{2,}\\\\b)\",\n // Stripe-style and generic API keys / tokens\n \"(?<apikey>\\\\b(?:sk_(?:test|live)_[A-Z0-9]{8,}|(?:sk|key|api|token)-[A-Z0-9_\\\\-]{8,})\\\\b)\",\n // IPv6 full and compressed forms (must come before IPv4 to avoid partial matches)\n \"(?<ipv6>(?:[0-9A-F]{1,4}:){7}[0-9A-F]{1,4}|(?:[0-9A-F]{1,4}:){1,7}:|(?:[0-9A-F]{1,4}:){1,6}:[0-9A-F]{1,4}|::(?:[0-9A-F]{1,4}:){0,5}[0-9A-F]{1,4}|::)\",\n // IPv4 addresses with valid octet ranges\n \"(?<ipv4>\\\\b(?:(?:25[0-5]|2[0-4]\\\\d|1\\\\d\\\\d|[1-9]?\\\\d)\\\\.){3}(?:25[0-5]|2[0-4]\\\\d|1\\\\d\\\\d|[1-9]?\\\\d)\\\\b)\",\n // Phone numbers — E.164, US, and common international formats\n \"(?<phone>(?:\\\\+?1[\\\\s.\\\\-]?)?\\\\(?\\\\d{3}\\\\)?[\\\\s.\\\\-]?\\\\d{3}[\\\\s.\\\\-]?\\\\d{4})\"\n ].join(\"|\"),\n \"gi\"\n);\n\n/**\n * Summary of a single text redaction operation.\n */\nexport interface RedactionSummary {\n value: string;\n redactedCount: number;\n}\n\n/**\n * Redacts sensitive values from a plain string in a single regex pass.\n */\nexport function redactSensitiveText(input: string): RedactionSummary {\n let redactedCount = 0;\n\n const value = input.replace(COMBINED_SENSITIVE_REGEX, () => {\n redactedCount += 1;\n return REDACTION_TOKEN;\n });\n\n return { value, redactedCount };\n}\n\n/**\n * Recursively redacts sensitive content from arbitrary JSON-like payloads.\n */\nexport function redactSensitiveData<T>(value: T): T {\n const seen = new WeakMap<object, unknown>();\n\n const transform = (candidate: unknown): unknown => {\n if (typeof candidate === \"string\") {\n return redactSensitiveText(candidate).value;\n }\n\n if (Array.isArray(candidate)) {\n return candidate.map((entry) => transform(entry));\n }\n\n if (!candidate || typeof candidate !== \"object\") {\n return candidate;\n }\n\n if (seen.has(candidate)) {\n return seen.get(candidate);\n }\n\n const inputRecord = candidate as Record<string, unknown>;\n const outputRecord: Record<string, unknown> = {};\n seen.set(candidate, outputRecord);\n\n for (const [key, entry] of Object.entries(inputRecord)) {\n outputRecord[key] = transform(entry);\n }\n\n return outputRecord;\n };\n\n return transform(value) as T;\n}\n"]}
@@ -12,11 +12,16 @@ export interface RateLimitDecision {
12
12
  retryAfterMs?: number;
13
13
  }
14
14
  /**
15
- * In-memory sliding-window limiter for tool call volume.
15
+ * In-memory sliding-window limiter using a circular buffer for O(1) operations.
16
16
  */
17
17
  export declare class RateLimiter {
18
18
  private readonly maxCallsPerMinute;
19
- private readonly callTimestampsMs;
19
+ /** Circular buffer storing call timestamps. */
20
+ private readonly buffer;
21
+ /** Write head: next slot to write into. */
22
+ private head;
23
+ /** Number of valid entries currently in the buffer. */
24
+ private count;
20
25
  /**
21
26
  * Creates a new limiter using calls per rolling minute as the quota.
22
27
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/security/rate-limiter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE3C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAW;IAE5C;;OAEG;gBACgB,OAAO,EAAE,kBAAkB;IAK9C;;OAEG;IACI,OAAO,CAAC,GAAG,GAAE,MAAmB,GAAG,iBAAiB;IAwB3D;;OAEG;IACI,QAAQ,IAAI,MAAM;CAG1B"}
1
+ {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/security/rate-limiter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE3C,+CAA+C;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAEtC,2CAA2C;IAC3C,OAAO,CAAC,IAAI,CAAS;IAErB,uDAAuD;IACvD,OAAO,CAAC,KAAK,CAAS;IAEtB;;OAEG;gBACgB,OAAO,EAAE,kBAAkB;IAO9C;;OAEG;IACI,OAAO,CAAC,GAAG,GAAE,MAAmB,GAAG,iBAAiB;IA6B3D;;OAEG;IACI,QAAQ,IAAI,MAAM;CAG1B"}
@@ -1,43 +1,55 @@
1
1
  /**
2
- * In-memory sliding-window limiter for tool call volume.
2
+ * In-memory sliding-window limiter using a circular buffer for O(1) operations.
3
3
  */
4
4
  export class RateLimiter {
5
5
  maxCallsPerMinute;
6
- callTimestampsMs;
6
+ /** Circular buffer storing call timestamps. */
7
+ buffer;
8
+ /** Write head: next slot to write into. */
9
+ head;
10
+ /** Number of valid entries currently in the buffer. */
11
+ count;
7
12
  /**
8
13
  * Creates a new limiter using calls per rolling minute as the quota.
9
14
  */
10
15
  constructor(options) {
11
16
  this.maxCallsPerMinute = Math.max(1, options.maxCallsPerMinute);
12
- this.callTimestampsMs = [];
17
+ this.buffer = new Float64Array(this.maxCallsPerMinute);
18
+ this.head = 0;
19
+ this.count = 0;
13
20
  }
14
21
  /**
15
22
  * Checks whether a request can proceed and records it when allowed.
16
23
  */
17
24
  consume(now = Date.now()) {
18
- const oneMinuteAgo = now - 60_000;
19
- while (this.callTimestampsMs.length > 0) {
20
- const head = this.callTimestampsMs[0];
21
- if (head === undefined || head > oneMinuteAgo) {
25
+ const windowStart = now - 60_000;
26
+ // Evict expired entries from the tail (oldest) of the circular buffer.
27
+ while (this.count > 0) {
28
+ const tail = (this.head - this.count + this.maxCallsPerMinute) % this.maxCallsPerMinute;
29
+ const oldest = this.buffer[tail] ?? 0;
30
+ if (oldest > windowStart) {
22
31
  break;
23
32
  }
24
- this.callTimestampsMs.shift();
33
+ this.count -= 1;
25
34
  }
26
- if (this.callTimestampsMs.length >= this.maxCallsPerMinute) {
27
- const oldest = this.callTimestampsMs[0] ?? now;
35
+ if (this.count >= this.maxCallsPerMinute) {
36
+ const tail = (this.head - this.count + this.maxCallsPerMinute) % this.maxCallsPerMinute;
37
+ const oldest = this.buffer[tail] ?? now;
28
38
  return {
29
39
  allowed: false,
30
40
  retryAfterMs: Math.max(0, 60_000 - (now - oldest))
31
41
  };
32
42
  }
33
- this.callTimestampsMs.push(now);
43
+ this.buffer[this.head] = now;
44
+ this.head = (this.head + 1) % this.maxCallsPerMinute;
45
+ this.count += 1;
34
46
  return { allowed: true };
35
47
  }
36
48
  /**
37
49
  * Returns current limiter occupancy for diagnostics and testing.
38
50
  */
39
51
  getCount() {
40
- return this.callTimestampsMs.length;
52
+ return this.count;
41
53
  }
42
54
  }
43
55
  //# sourceMappingURL=rate-limiter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/security/rate-limiter.ts"],"names":[],"mappings":"AAeA;;GAEG;AACH,MAAM,OAAO,WAAW;IACL,iBAAiB,CAAS;IAE1B,gBAAgB,CAAW;IAE5C;;OAEG;IACH,YAAmB,OAA2B;QAC5C,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAChE,IAAI,CAAC,gBAAgB,GAAG,EAAE,CAAC;IAC7B,CAAC;IAED;;OAEG;IACI,OAAO,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;QACrC,MAAM,YAAY,GAAG,GAAG,GAAG,MAAM,CAAC;QAElC,OAAO,IAAI,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,GAAG,YAAY,EAAE,CAAC;gBAC9C,MAAM;YACR,CAAC;YAED,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAChC,CAAC;QAED,IAAI,IAAI,CAAC,gBAAgB,CAAC,MAAM,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;YAC/C,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC;aACnD,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED;;OAEG;IACI,QAAQ;QACb,OAAO,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;IACtC,CAAC;CACF","sourcesContent":["/**\n * Sliding-window limiter configuration.\n */\nexport interface RateLimiterOptions {\n maxCallsPerMinute: number;\n}\n\n/**\n * Result of evaluating a rate-limited request.\n */\nexport interface RateLimitDecision {\n allowed: boolean;\n retryAfterMs?: number;\n}\n\n/**\n * In-memory sliding-window limiter for tool call volume.\n */\nexport class RateLimiter {\n private readonly maxCallsPerMinute: number;\n\n private readonly callTimestampsMs: number[];\n\n /**\n * Creates a new limiter using calls per rolling minute as the quota.\n */\n public constructor(options: RateLimiterOptions) {\n this.maxCallsPerMinute = Math.max(1, options.maxCallsPerMinute);\n this.callTimestampsMs = [];\n }\n\n /**\n * Checks whether a request can proceed and records it when allowed.\n */\n public consume(now: number = Date.now()): RateLimitDecision {\n const oneMinuteAgo = now - 60_000;\n\n while (this.callTimestampsMs.length > 0) {\n const head = this.callTimestampsMs[0];\n if (head === undefined || head > oneMinuteAgo) {\n break;\n }\n\n this.callTimestampsMs.shift();\n }\n\n if (this.callTimestampsMs.length >= this.maxCallsPerMinute) {\n const oldest = this.callTimestampsMs[0] ?? now;\n return {\n allowed: false,\n retryAfterMs: Math.max(0, 60_000 - (now - oldest))\n };\n }\n\n this.callTimestampsMs.push(now);\n return { allowed: true };\n }\n\n /**\n * Returns current limiter occupancy for diagnostics and testing.\n */\n public getCount(): number {\n return this.callTimestampsMs.length;\n }\n}"]}
1
+ {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/security/rate-limiter.ts"],"names":[],"mappings":"AAeA;;GAEG;AACH,MAAM,OAAO,WAAW;IACL,iBAAiB,CAAS;IAE3C,+CAA+C;IAC9B,MAAM,CAAe;IAEtC,2CAA2C;IACnC,IAAI,CAAS;IAErB,uDAAuD;IAC/C,KAAK,CAAS;IAEtB;;OAEG;IACH,YAAmB,OAA2B;QAC5C,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACvD,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IACjB,CAAC;IAED;;OAEG;IACI,OAAO,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;QACrC,MAAM,WAAW,GAAG,GAAG,GAAG,MAAM,CAAC;QAEjC,uEAAuE;QACvE,OAAO,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC;YACxF,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,MAAM,GAAG,WAAW,EAAE,CAAC;gBACzB,MAAM;YACR,CAAC;YACD,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC;YACxF,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;YACxC,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC;aACnD,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC;QACrD,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QAEhB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED;;OAEG;IACI,QAAQ;QACb,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;CACF","sourcesContent":["/**\n * Sliding-window limiter configuration.\n */\nexport interface RateLimiterOptions {\n maxCallsPerMinute: number;\n}\n\n/**\n * Result of evaluating a rate-limited request.\n */\nexport interface RateLimitDecision {\n allowed: boolean;\n retryAfterMs?: number;\n}\n\n/**\n * In-memory sliding-window limiter using a circular buffer for O(1) operations.\n */\nexport class RateLimiter {\n private readonly maxCallsPerMinute: number;\n\n /** Circular buffer storing call timestamps. */\n private readonly buffer: Float64Array;\n\n /** Write head: next slot to write into. */\n private head: number;\n\n /** Number of valid entries currently in the buffer. */\n private count: number;\n\n /**\n * Creates a new limiter using calls per rolling minute as the quota.\n */\n public constructor(options: RateLimiterOptions) {\n this.maxCallsPerMinute = Math.max(1, options.maxCallsPerMinute);\n this.buffer = new Float64Array(this.maxCallsPerMinute);\n this.head = 0;\n this.count = 0;\n }\n\n /**\n * Checks whether a request can proceed and records it when allowed.\n */\n public consume(now: number = Date.now()): RateLimitDecision {\n const windowStart = now - 60_000;\n\n // Evict expired entries from the tail (oldest) of the circular buffer.\n while (this.count > 0) {\n const tail = (this.head - this.count + this.maxCallsPerMinute) % this.maxCallsPerMinute;\n const oldest = this.buffer[tail] ?? 0;\n if (oldest > windowStart) {\n break;\n }\n this.count -= 1;\n }\n\n if (this.count >= this.maxCallsPerMinute) {\n const tail = (this.head - this.count + this.maxCallsPerMinute) % this.maxCallsPerMinute;\n const oldest = this.buffer[tail] ?? now;\n return {\n allowed: false,\n retryAfterMs: Math.max(0, 60_000 - (now - oldest))\n };\n }\n\n this.buffer[this.head] = now;\n this.head = (this.head + 1) % this.maxCallsPerMinute;\n this.count += 1;\n\n return { allowed: true };\n }\n\n /**\n * Returns current limiter occupancy for diagnostics and testing.\n */\n public getCount(): number {\n return this.count;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "mcp-warden",
3
- "version": "0.1.4",
3
+ "version": "1.0.0",
4
4
  "description": "Policy enforcement and guardrails for MCP-compatible tool execution.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "sideEffects": false,
7
8
  "main": "./dist/index.js",
8
9
  "types": "./dist/index.d.ts",
9
10
  "bin": {
@@ -18,17 +19,31 @@
18
19
  "files": [
19
20
  "dist"
20
21
  ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/vikrantwiz02/mcp-warden.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/vikrantwiz02/mcp-warden/issues"
28
+ },
21
29
  "scripts": {
22
30
  "build": "tsc -p tsconfig.json",
23
31
  "typecheck": "tsc --noEmit -p tsconfig.json",
24
32
  "test": "vitest run",
25
- "test:watch": "vitest"
33
+ "test:watch": "vitest",
34
+ "coverage": "vitest run --coverage",
35
+ "lint": "eslint src",
36
+ "format": "prettier --write \"src/**/*.ts\"",
37
+ "prepublishOnly": "npm run typecheck && npm run build"
26
38
  },
27
39
  "keywords": [
28
40
  "mcp",
29
41
  "guardian",
30
42
  "policy",
31
- "security"
43
+ "security",
44
+ "rate-limit",
45
+ "circuit-breaker",
46
+ "pii-redaction"
32
47
  ],
33
48
  "engines": {
34
49
  "node": ">=20"
@@ -39,7 +54,13 @@
39
54
  "zod": "^3.24.2"
40
55
  },
41
56
  "devDependencies": {
57
+ "@eslint/js": "^9.20.0",
42
58
  "@types/node": "^24.0.0",
59
+ "@typescript-eslint/eslint-plugin": "^8.20.0",
60
+ "@typescript-eslint/parser": "^8.20.0",
61
+ "@vitest/coverage-v8": "^3.1.4",
62
+ "eslint": "^9.20.0",
63
+ "prettier": "^3.5.0",
43
64
  "typescript": "^5.8.3",
44
65
  "vitest": "^3.1.4"
45
66
  }