@wardnmesh/sdk-node 0.2.1

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.
Files changed (42) hide show
  1. package/LICENSE +47 -0
  2. package/README.md +178 -0
  3. package/bin/setup.js +119 -0
  4. package/dist/agent-guard.d.ts +28 -0
  5. package/dist/agent-guard.js +206 -0
  6. package/dist/config/security-limits.d.ts +42 -0
  7. package/dist/config/security-limits.js +52 -0
  8. package/dist/detectors/base.d.ts +22 -0
  9. package/dist/detectors/base.js +51 -0
  10. package/dist/detectors/pattern.d.ts +7 -0
  11. package/dist/detectors/pattern.js +76 -0
  12. package/dist/detectors/semantic-detector.d.ts +16 -0
  13. package/dist/detectors/semantic-detector.js +96 -0
  14. package/dist/detectors/sequence.d.ts +11 -0
  15. package/dist/detectors/sequence.js +182 -0
  16. package/dist/detectors/state.d.ts +8 -0
  17. package/dist/detectors/state.js +86 -0
  18. package/dist/index.d.ts +15 -0
  19. package/dist/index.js +31 -0
  20. package/dist/integrations/vercel.d.ts +7 -0
  21. package/dist/integrations/vercel.js +34 -0
  22. package/dist/middleware/express.d.ts +36 -0
  23. package/dist/middleware/express.js +54 -0
  24. package/dist/middleware/nextjs.d.ts +3 -0
  25. package/dist/middleware/nextjs.js +40 -0
  26. package/dist/state/session-manager.d.ts +13 -0
  27. package/dist/state/session-manager.js +22 -0
  28. package/dist/telemetry/reporter.d.ts +32 -0
  29. package/dist/telemetry/reporter.js +86 -0
  30. package/dist/types.d.ts +206 -0
  31. package/dist/types.js +14 -0
  32. package/dist/update-checker.d.ts +21 -0
  33. package/dist/update-checker.js +218 -0
  34. package/dist/utils/logger.d.ts +40 -0
  35. package/dist/utils/logger.js +79 -0
  36. package/dist/utils/rule-validator.d.ts +15 -0
  37. package/dist/utils/rule-validator.js +143 -0
  38. package/dist/utils/safe-regex.d.ts +53 -0
  39. package/dist/utils/safe-regex.js +220 -0
  40. package/dist/wardn.d.ts +67 -0
  41. package/dist/wardn.js +443 -0
  42. package/package.json +47 -0
package/dist/wardn.js ADDED
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Wardn = void 0;
4
+ const pattern_1 = require("./detectors/pattern");
5
+ const sequence_1 = require("./detectors/sequence");
6
+ const state_1 = require("./detectors/state");
7
+ const safe_regex_1 = require("./utils/safe-regex");
8
+ const update_checker_1 = require("./update-checker");
9
+ const session_manager_1 = require("./state/session-manager");
10
+ const reporter_1 = require("./telemetry/reporter");
11
+ const semantic_detector_1 = require("./detectors/semantic-detector");
12
+ const rule_validator_1 = require("./utils/rule-validator");
13
+ const logger_1 = require("./utils/logger");
14
+ const security_limits_1 = require("./config/security-limits");
15
+ /** Synchronous state adapter for in-request state management */
16
+ class SyncStateAdapter {
17
+ constructor(initialState, maxHistory = 50) {
18
+ this.maxHistory = maxHistory;
19
+ this.state = {
20
+ startTime: initialState.startTime || new Date().toISOString(),
21
+ toolCalls: initialState.toolCalls || [],
22
+ recentTools: initialState.recentTools || [],
23
+ detectedViolations: initialState.detectedViolations || [],
24
+ customState: initialState.customState || {},
25
+ currentFile: initialState.currentFile,
26
+ };
27
+ }
28
+ getRecentTools(count) {
29
+ const tools = this.state.recentTools || [];
30
+ return count ? tools.slice(-count) : tools;
31
+ }
32
+ setCustomState(key, value) {
33
+ this.state.customState[key] = value;
34
+ }
35
+ getCustomState(key) {
36
+ return this.state.customState[key];
37
+ }
38
+ addToolCall(tool) {
39
+ if (!this.state.recentTools)
40
+ this.state.recentTools = [];
41
+ this.state.recentTools.push(tool);
42
+ if (this.state.recentTools.length > this.maxHistory) {
43
+ this.state.recentTools.shift();
44
+ }
45
+ }
46
+ exportState() {
47
+ return this.state;
48
+ }
49
+ addViolation(violation) {
50
+ if (!this.state.detectedViolations)
51
+ this.state.detectedViolations = [];
52
+ this.state.detectedViolations.push(violation);
53
+ }
54
+ }
55
+ class Wardn {
56
+ constructor(config, stateProvider) {
57
+ this.pollTimer = null;
58
+ this.isShutdown = false;
59
+ // Rate limiting for rule updates
60
+ this.lastRuleUpdate = 0;
61
+ this.updateCount = 0;
62
+ this.updateCountResetTime = Date.now();
63
+ this.config = config;
64
+ this.stateProvider = stateProvider || new session_manager_1.InMemorySessionStateProvider();
65
+ this.telemetry = this.initTelemetry();
66
+ this.patternDetector = new pattern_1.PatternDetector();
67
+ this.sequenceDetector = new sequence_1.SequenceDetector();
68
+ this.stateDetector = new state_1.StateDetector();
69
+ this.semanticDetector = semantic_detector_1.SemanticDetector.getInstance();
70
+ // Emit startup event
71
+ this.telemetry.emit({
72
+ eventType: "agent_started",
73
+ timestamp: new Date().toISOString(),
74
+ data: {
75
+ config: {
76
+ appName: this.config.telemetry?.serviceName,
77
+ ruleCount: this.config.rules.length,
78
+ },
79
+ },
80
+ });
81
+ // Start polling if configured (with URL validation)
82
+ if (this.config.remoteRulesUrl) {
83
+ const urlValidation = (0, rule_validator_1.validateRemoteUrl)(this.config.remoteRulesUrl);
84
+ if (urlValidation.valid) {
85
+ this.pollRemoteRules();
86
+ }
87
+ else {
88
+ logger_1.logger.error("Invalid remote rules URL:", urlValidation.errors.join(", "));
89
+ }
90
+ }
91
+ }
92
+ initTelemetry() {
93
+ if (this.config.telemetry?.enabled && process.env.WARDN_API_URL) {
94
+ const apiKey = this.config.apiKey;
95
+ return new reporter_1.WardnReporter(process.env.WARDN_API_URL, apiKey || "", this.config.telemetry.serviceName || "unknown-service");
96
+ }
97
+ return new reporter_1.ConsoleReporter();
98
+ }
99
+ static getInstance() {
100
+ if (!Wardn.instance) {
101
+ throw new Error("Wardn not initialized. Call Wardn.init() first.");
102
+ }
103
+ return Wardn.instance;
104
+ }
105
+ static init(config, stateProvider) {
106
+ const apiKey = config.apiKey || process.env.WARDN_API_KEY;
107
+ if (!apiKey) {
108
+ const errorMsg = "\n\n" +
109
+ "[Wardn] ACTIVE DEFENSE NOT INITIALIZED\n" +
110
+ "===================================================\n" +
111
+ "The immune system requires an identity to function.\n" +
112
+ "\n" +
113
+ "1. Get your API Key: https://wardnmesh.ai (Free for Devs)\n" +
114
+ "2. Set Variable: WARDNMESH_API_KEY=wk_...\n" +
115
+ "\n" +
116
+ "Your agent is currently UNPROTECTED from prompt injection.\n" +
117
+ "===================================================\n";
118
+ logger_1.logger.error(errorMsg);
119
+ throw new Error("[Wardn] Missing API Key. Registration required.");
120
+ }
121
+ // Ensure config has the key
122
+ const finalConfig = { ...config, apiKey };
123
+ Wardn.instance = new Wardn(finalConfig, stateProvider);
124
+ // Check for updates (non-blocking, don't await)
125
+ // Skip in test environment to avoid async warnings
126
+ if (security_limits_1.FEATURE_FLAGS.ENABLE_UPDATE_CHECK && process.env.NODE_ENV !== "test") {
127
+ (0, update_checker_1.notifyIfUpdateAvailable)().catch(() => {
128
+ // Silently fail - don't disrupt initialization
129
+ });
130
+ }
131
+ return Wardn.instance;
132
+ }
133
+ /**
134
+ * Reset the singleton instance (useful for testing)
135
+ */
136
+ static reset() {
137
+ if (Wardn.instance) {
138
+ Wardn.instance.shutdown();
139
+ Wardn.instance = null;
140
+ }
141
+ }
142
+ /**
143
+ * Shutdown the Wardn instance, cleaning up resources
144
+ */
145
+ shutdown() {
146
+ if (this.isShutdown)
147
+ return;
148
+ this.isShutdown = true;
149
+ // Clear polling timer
150
+ if (this.pollTimer) {
151
+ clearTimeout(this.pollTimer);
152
+ this.pollTimer = null;
153
+ }
154
+ // Emit shutdown event with pattern error metrics
155
+ const patternMetrics = (0, safe_regex_1.getPatternErrorMetrics)();
156
+ this.telemetry.emit({
157
+ eventType: "agent_shutdown",
158
+ timestamp: new Date().toISOString(),
159
+ data: {
160
+ uptime: Date.now() - new Date(this.stateProvider ? Date.now() : 0).getTime(),
161
+ patternErrorMetrics: patternMetrics,
162
+ },
163
+ });
164
+ // Stop telemetry auto-flush timer
165
+ if (this.telemetry.stop) {
166
+ this.telemetry.stop();
167
+ }
168
+ logger_1.logger.info("Shutdown complete");
169
+ }
170
+ /**
171
+ * Scan a request for security violations
172
+ * @param request The agent request context (prompt, tools, etc.)
173
+ * @returns ScanResult with allowed/blocked status and violations
174
+ */
175
+ async scan(request) {
176
+ if (this.isShutdown) {
177
+ return {
178
+ allowed: true,
179
+ violations: [],
180
+ latencyMs: 0,
181
+ metadata: { error: true, errorDetails: "Wardn instance is shutdown" },
182
+ };
183
+ }
184
+ const startTime = Date.now();
185
+ const failMode = this.config.failMode || "open";
186
+ try {
187
+ const { toolName = "llm_input", parameters = {}, sessionId, } = request;
188
+ const currentSessionId = sessionId || "default";
189
+ const rawState = await this.stateProvider.getState(currentSessionId);
190
+ const stateAdapter = new SyncStateAdapter(rawState, this.config.maxHistorySize);
191
+ const toolData = this.normalizeRequest(request);
192
+ stateAdapter.addToolCall(toolData);
193
+ // Lazy load semantic model if needed
194
+ if (this.config.rules.some((r) => r.detector.type === "semantic")) {
195
+ await this.semanticDetector.init();
196
+ }
197
+ const violations = [];
198
+ // Run all detectors
199
+ for (const rule of this.config.rules) {
200
+ const violation = await this.detectViolation(rule, toolData, stateAdapter);
201
+ if (violation) {
202
+ violations.push(violation);
203
+ stateAdapter.addViolation(violation);
204
+ this.reportViolation(violation, toolData, currentSessionId);
205
+ }
206
+ }
207
+ await this.stateProvider.setState(currentSessionId, stateAdapter.exportState());
208
+ const result = {
209
+ allowed: !violations.some((v) => v.severity === "critical"),
210
+ violations,
211
+ latencyMs: Date.now() - startTime,
212
+ metadata: {
213
+ analyzedRules: this.config.rules.length,
214
+ sessionId: currentSessionId,
215
+ },
216
+ };
217
+ this.reportScanComplete(result, currentSessionId);
218
+ return result;
219
+ }
220
+ catch (error) {
221
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
222
+ logger_1.logger.error(`Scan failed, failing ${failMode}:`, error);
223
+ this.telemetry.emit({
224
+ eventType: "error",
225
+ timestamp: new Date().toISOString(),
226
+ data: {
227
+ error: errorMessage,
228
+ sessionId: request.sessionId || "unknown",
229
+ },
230
+ });
231
+ // Configurable fail mode
232
+ const allowed = failMode === "open";
233
+ return {
234
+ allowed,
235
+ violations: [],
236
+ latencyMs: Date.now() - startTime,
237
+ metadata: {
238
+ error: true,
239
+ errorDetails: errorMessage,
240
+ failMode,
241
+ },
242
+ };
243
+ }
244
+ }
245
+ /** Normalize request to ToolData format */
246
+ normalizeRequest(request) {
247
+ return {
248
+ toolName: request.toolName || "llm_input",
249
+ parameters: request.parameters || { prompt: request.prompt || "" },
250
+ result: { success: true },
251
+ duration: 0,
252
+ timestamp: new Date().toISOString(),
253
+ };
254
+ }
255
+ /** Run detector for a single rule */
256
+ async detectViolation(rule, toolData, stateAdapter) {
257
+ switch (rule.detector.type) {
258
+ case "pattern":
259
+ return this.patternDetector.detect(toolData, rule, stateAdapter);
260
+ case "sequence":
261
+ return this.sequenceDetector.detect(toolData, rule, stateAdapter);
262
+ case "state":
263
+ return this.stateDetector.detect(toolData, rule, stateAdapter);
264
+ case "semantic":
265
+ return this.detectSemanticViolation(rule, toolData);
266
+ default:
267
+ return null;
268
+ }
269
+ }
270
+ /** Handle semantic detection */
271
+ async detectSemanticViolation(rule, toolData) {
272
+ const prompt = toolData.parameters.prompt ||
273
+ JSON.stringify(toolData.parameters);
274
+ const config = rule.detector.config;
275
+ const { detected, reason, score } = await this.semanticDetector.scan(prompt, config.threshold || 0.75);
276
+ if (!detected)
277
+ return null;
278
+ return {
279
+ id: crypto.randomUUID(),
280
+ ruleId: rule.id,
281
+ ruleName: rule.name,
282
+ severity: rule.severity,
283
+ description: reason || "Semantic Violation",
284
+ context: {
285
+ toolName: toolData.toolName,
286
+ toolData,
287
+ additionalInfo: { score },
288
+ },
289
+ timestamp: new Date().toISOString(),
290
+ };
291
+ }
292
+ /** Report violation to telemetry */
293
+ reportViolation(violation, toolData, sessionId) {
294
+ // REDACTION: Create a safe copy for the cloud
295
+ const safeViolation = { ...violation };
296
+ if (safeViolation.context) {
297
+ // Remove raw toolData (prompts) from the cloud payload
298
+ const { toolData: _removed, ...safeContext } = safeViolation.context;
299
+ safeViolation.context = safeContext;
300
+ }
301
+ this.telemetry.emit({
302
+ eventType: "violation_detected",
303
+ timestamp: new Date().toISOString(),
304
+ data: { violation: safeViolation },
305
+ metadata: { sessionId },
306
+ });
307
+ }
308
+ /** Report scan completion to telemetry */
309
+ reportScanComplete(result, sessionId) {
310
+ this.telemetry.emit({
311
+ eventType: "scan_complete",
312
+ timestamp: new Date().toISOString(),
313
+ data: {
314
+ allowed: result.allowed,
315
+ latencyMs: result.latencyMs,
316
+ violationCount: result.violations.length,
317
+ },
318
+ metadata: { sessionId },
319
+ });
320
+ }
321
+ /**
322
+ * Update rules dynamically
323
+ * This allows external systems (like MCP server with Supabase threat rules)
324
+ * to update the rule set without restarting
325
+ *
326
+ * @param rules New rule set to use
327
+ * @returns void
328
+ */
329
+ updateRules(rules) {
330
+ if (this.isShutdown) {
331
+ logger_1.logger.warn("Cannot update rules: Wardn instance is shutdown");
332
+ return;
333
+ }
334
+ // Rate limiting: Prevent update spam
335
+ const now = Date.now();
336
+ const timeSinceLastUpdate = now - this.lastRuleUpdate;
337
+ if (timeSinceLastUpdate < security_limits_1.SECURITY_LIMITS.MIN_UPDATE_INTERVAL_MS) {
338
+ throw new Error(`Rate limit: Cannot update rules more than once per ${security_limits_1.SECURITY_LIMITS.MIN_UPDATE_INTERVAL_MS}ms. ` +
339
+ `Last update was ${timeSinceLastUpdate}ms ago.`);
340
+ }
341
+ // Reset update counter every minute
342
+ if (now - this.updateCountResetTime > 60000) {
343
+ this.updateCount = 0;
344
+ this.updateCountResetTime = now;
345
+ }
346
+ if (this.updateCount >= security_limits_1.SECURITY_LIMITS.MAX_UPDATES_PER_MINUTE) {
347
+ throw new Error(`Rate limit: Maximum ${security_limits_1.SECURITY_LIMITS.MAX_UPDATES_PER_MINUTE} updates per minute exceeded.`);
348
+ }
349
+ // Validate rule count before validation (fail fast)
350
+ if (rules.length > security_limits_1.SECURITY_LIMITS.MAX_RULES) {
351
+ throw new Error(`Too many rules: ${rules.length}. Maximum allowed: ${security_limits_1.SECURITY_LIMITS.MAX_RULES}`);
352
+ }
353
+ // Validate rules before applying
354
+ const validation = (0, rule_validator_1.validateRules)(rules);
355
+ if (!validation.valid) {
356
+ logger_1.logger.error("Invalid rules provided to updateRules():", validation.errors.slice(0, 5).join("; "));
357
+ throw new Error(`Invalid rules: ${validation.errors[0]}`);
358
+ }
359
+ const oldCount = this.config.rules.length;
360
+ // Atomic update: Create defensive copy then assign atomically
361
+ // This prevents race conditions where scan() might iterate during update
362
+ const newRules = [...rules]; // Defensive copy
363
+ this.config.rules = newRules; // Atomic assignment (V8 guarantees atomicity)
364
+ logger_1.logger.info(`Rules updated: ${oldCount} → ${this.config.rules.length} rules`);
365
+ // Emit telemetry event for rule update
366
+ this.telemetry.emit({
367
+ eventType: "rules_updated",
368
+ timestamp: new Date().toISOString(),
369
+ data: {
370
+ oldCount,
371
+ newCount: this.config.rules.length,
372
+ },
373
+ });
374
+ // Update rate limiting tracking
375
+ this.lastRuleUpdate = now;
376
+ this.updateCount++;
377
+ }
378
+ /**
379
+ * Get current rules
380
+ * @returns Current rule set
381
+ */
382
+ getRules() {
383
+ return [...this.config.rules];
384
+ }
385
+ /**
386
+ * Get pattern error metrics for monitoring
387
+ * @returns Pattern error statistics
388
+ */
389
+ getPatternErrorMetrics() {
390
+ return (0, safe_regex_1.getPatternErrorMetrics)();
391
+ }
392
+ /** Poll remote rules for updates with validation */
393
+ async pollRemoteRules() {
394
+ if (!this.config.remoteRulesUrl || this.isShutdown)
395
+ return;
396
+ const FETCH_TIMEOUT_MS = 10000; // 10 second timeout
397
+ const poll = async () => {
398
+ if (this.isShutdown)
399
+ return;
400
+ try {
401
+ // Create AbortController for timeout
402
+ const controller = new AbortController();
403
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
404
+ const response = await fetch(this.config.remoteRulesUrl, {
405
+ headers: {
406
+ "Accept": "application/json",
407
+ "X-Wardn-Client": "sdk-node",
408
+ },
409
+ signal: controller.signal,
410
+ });
411
+ clearTimeout(timeoutId);
412
+ if (response.ok) {
413
+ const remoteRules = await response.json();
414
+ // Validate rules before applying
415
+ const validation = (0, rule_validator_1.validateRules)(remoteRules);
416
+ if (validation.valid) {
417
+ this.config.rules = remoteRules;
418
+ logger_1.logger.info(`Rules synced from remote: ${this.config.rules.length} rules`);
419
+ }
420
+ else {
421
+ logger_1.logger.error("Invalid remote rules:", validation.errors.slice(0, 5).join("; "));
422
+ }
423
+ }
424
+ else {
425
+ logger_1.logger.warn("Failed to fetch rules:", response.statusText);
426
+ }
427
+ }
428
+ catch (error) {
429
+ logger_1.logger.error("Rule polling error:", error);
430
+ }
431
+ finally {
432
+ // Schedule next poll if not shutdown
433
+ if (!this.isShutdown) {
434
+ this.pollTimer = setTimeout(poll, this.config.pollingIntervalMs || 60000);
435
+ }
436
+ }
437
+ };
438
+ // Initial call
439
+ poll();
440
+ }
441
+ }
442
+ exports.Wardn = Wardn;
443
+ Wardn.instance = null;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@wardnmesh/sdk-node",
3
+ "version": "0.2.1",
4
+ "description": "WardnMesh.AI Node.js SDK - Active Defense Middleware for AI Agents",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest"
10
+ },
11
+ "bin": {
12
+ "wardn-init": "./bin/setup.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "bin",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "ai-security",
22
+ "prompt-injection",
23
+ "middleware",
24
+ "agent-guard"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/PCIRCLE-AI/wardnmesh.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/PCIRCLE-AI/wardnmesh/issues"
32
+ },
33
+ "homepage": "https://wardnmesh.ai",
34
+ "author": "KT",
35
+ "license": "Elastic-2.0",
36
+ "devDependencies": {
37
+ "@types/jest": "^29.5.11",
38
+ "@types/node": "^20.10.0",
39
+ "jest": "^29.7.0",
40
+ "next": "^16.1.1",
41
+ "ts-jest": "^29.1.1",
42
+ "typescript": "^5.0.0"
43
+ },
44
+ "dependencies": {
45
+ "@xenova/transformers": "^2.17.2"
46
+ }
47
+ }