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.
- package/dist/core/interceptor.d.ts +48 -1
- package/dist/core/interceptor.d.ts.map +1 -1
- package/dist/core/interceptor.js +188 -28
- package/dist/core/interceptor.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/security/circuit-breaker.d.ts +17 -1
- package/dist/security/circuit-breaker.d.ts.map +1 -1
- package/dist/security/circuit-breaker.js +38 -11
- package/dist/security/circuit-breaker.js.map +1 -1
- package/dist/security/injection-scanner.d.ts +3 -0
- package/dist/security/injection-scanner.d.ts.map +1 -1
- package/dist/security/injection-scanner.js +33 -6
- package/dist/security/injection-scanner.js.map +1 -1
- package/dist/security/pii-redactor.d.ts +1 -1
- package/dist/security/pii-redactor.d.ts.map +1 -1
- package/dist/security/pii-redactor.js +21 -20
- package/dist/security/pii-redactor.js.map +1 -1
- package/dist/security/rate-limiter.d.ts +7 -2
- package/dist/security/rate-limiter.d.ts.map +1 -1
- package/dist/security/rate-limiter.js +24 -12
- package/dist/security/rate-limiter.js.map +1 -1
- package/package.json +24 -3
|
@@ -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
|
|
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;
|
|
1
|
+
{"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../../src/core/interceptor.ts"],"names":[],"mappings":"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"}
|
package/dist/core/interceptor.js
CHANGED
|
@@ -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
|
|
172
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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,
|
|
306
|
+
async function enforceRateLimit(context, next, globalLimiter, toolLimiters, toolRateLimits, nowProvider) {
|
|
230
307
|
if (!isToolCallRequest(context.request)) {
|
|
231
308
|
return next();
|
|
232
309
|
}
|
|
233
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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;
|
|
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
|
-
? {
|
|
73
|
-
: {
|
|
74
|
-
|
|
75
|
-
|
|
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":"
|
|
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;
|
|
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
|
|
15
|
-
.map((
|
|
16
|
-
.filter((
|
|
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
|
|
22
|
-
|
|
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,
|
|
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;
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
29
|
+
const value = input.replace(COMBINED_SENSITIVE_REGEX, () => {
|
|
23
30
|
redactedCount += 1;
|
|
24
31
|
return REDACTION_TOKEN;
|
|
25
|
-
};
|
|
26
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
33
|
+
this.count -= 1;
|
|
25
34
|
}
|
|
26
|
-
if (this.
|
|
27
|
-
const
|
|
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.
|
|
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.
|
|
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;
|
|
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.
|
|
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
|
}
|