Package Exports
- @sandrobuilds/tracerney
Readme
Tracerney
Transparent Proxy Runtime Sentinel — Enterprise-Grade Prompt Injection Defense for LLM Applications
Tracerney is a lightweight, free SDK for detecting prompt injection attacks. It runs 100% locally with no dependencies and no data collection.
Free SDK includes:
- Layer 1 (Pattern Detection): 238 embedded attack patterns with Unicode normalization
- <2ms detection latency per prompt
- Zero network overhead — all detection is local
- Works offline — no backend required
🚀 Quick Start
Install:
npm install @sandrobuilds/tracerneySimplest Setup (Free SDK — Pattern Detection):
import { Tracerney } from '@sandrobuilds/tracerney';
const tracer = new Tracerney({
allowedTools: ['search', 'calculator'],
});
// Check if a prompt is suspicious
const result = await tracer.scanPrompt(userInput);
console.log(result);
// {
// suspicious: true, // Layer 1 detected pattern match
// patternName: "Ignore Instructions",
// severity: "CRITICAL",
// blocked: false // No backend verification yet
// }
if (result.suspicious) {
console.log(`⚠️ Suspicious: ${result.patternName}`);
// Handle the suspicious prompt (log, rate-limit, etc.)
}Advanced Setup (with Backend LLM Verification):
import { Tracerney, ShieldBlockError } from '@sandrobuilds/tracerney';
const shield = new Tracerney({
baseUrl: 'http://localhost:3000', // Backend with LLM Sentinel
allowedTools: ['search', 'calculator'],
apiKey: process.env.TRACERNY_API_KEY,
});
try {
const response = await shield.wrap(() =>
openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: userInput }],
})
);
console.log(response);
} catch (error) {
if (error instanceof ShieldBlockError) {
console.error('🛡️ Attack blocked:', error.event.blockReason);
// Only throws if LLM Sentinel confirms attack
}
}Start with the simple setup. Add backend verification when ready!
Philosophy
We are not a "platform" where you send data to our servers first. We are a Runtime Sentinel that lives inside your process:
- 🚀 No latency friction: Detection happens locally in <2ms
- 🔒 No privacy concerns: Your prompts never leave your infrastructure
- 📦 3 lines of code: Wrap your existing LLM call
- 👨💻 Developer-first: Fast integration, zero configuration needed
Common Setups
Next.js (API Route)
// app/api/chat/route.ts
import { Tracerney, ShieldBlockError } from '@sandrobuilds/tracerney';
import OpenAI from 'openai';
const openai = new OpenAI();
export async function POST(req: Request) {
const { userMessage } = await req.json();
try {
const response = await shield.wrap(
() => openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: userMessage }],
}),
{ prompt: userMessage } // Pre-scan before LLM
);
return Response.json({ success: true, data: response });
} catch (error) {
if (error instanceof ShieldBlockError) {
return Response.json(
{ error: 'Request blocked for security' },
{ status: 403 }
);
}
throw error;
}
}Node.js / Express
import { Tracerney, ShieldBlockError } from '@sandrobuilds/tracerney';
import OpenAI from 'openai';
const shield = new Tracerney({ allowedTools: ['search'] });
const openai = new OpenAI();
app.post('/chat', async (req, res) => {
try {
const response = await shield.wrap(() =>
openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: req.body.message }],
})
);
res.json(response);
} catch (error) {
if (error instanceof ShieldBlockError) {
return res.status(403).json({ error: 'Blocked' });
}
throw error;
}
});Minimal Setup (No Telemetry)
// Just blocking, no monitoring
const shield = new Tracerney({
allowedTools: ['search'],
// No apiEndpoint = no telemetry
});
await shield.wrap(() => llmCall());With Monitoring Dashboard
// Using baseUrl — all endpoints auto-configured
const shield = new Tracerney({
baseUrl: 'http://localhost:3000',
allowedTools: ['search', 'calculator'],
apiKey: process.env.TRACERNY_API_KEY,
enableTelemetry: true, // Events + patterns auto-configured
});Architecture
The Request-Response Lifecycle
Request → Vanguard (Regex Check) → LLM Provider
↓
← Tool-Guard (Schema Check) ← Response
↓
App Logic (if clean) / Block (if dirty)
↓
Async Signal (Non-blocking Telemetry)Three-Layer Defense Architecture
Layer 1: Vanguard (Pattern Matching)
Speed: <2ms | Coverage: Known attacks
Fast regex-based detection with Unicode normalization to prevent homoglyph evasion.
Improved patterns detect:
ignore [your|my] instructions(not just "previous")forget|disregard [all] [rules|guidelines]reveal|show system promptact as unrestricted AI(jailbreak)- SQL/code injection
- Token smuggling
- 20+ OWASP-mapped patterns
Example: Homoglyph evasion blocked
Input: "ignore your instructions" (fullwidth)
Normalized: "ignore your instructions"
Result: ✅ BLOCKED by Pattern_001Layer 2: Sentinel (LLM Verification)
Speed: 200-1500ms | Coverage: Novel attacks
Backend-side LLM classifier runs only if Layer 1 misses. Uses OpenRouter Gemini with:
- Rate limiting: 5 calls/min per API key (prevents botnet cost spikes)
- Cost protection: Only hits for suspicious prompts
- Fallback: Non-blocking—if backend unavailable, execution continues
Example: Novel attack caught
Input: "explain social engineering vectors for credential theft"
Layer 1: ❌ No regex match
Layer 2: ✅ LLM classifier: "YES, this is an injection"
Result: BLOCKED + logged to databaseLayer 3: Hardened Middleware
Automatic protections applied to all requests:
Normalization: Removes Unicode tricks before pattern matching Jitter: Random 300-500ms delay masks which layer blocked the attack Rate Limiting: Backend enforces 5 verifications/min per key
// All automatic—no config needed
// Attacks from any layer see ~300-500ms latency
await shield.scanPrompt(userInput); // Returns in 300-500ms
// (attacker can't tell if Layer 1 or Layer 2 executed)// Optionally scan raw prompts before LLM call
try {
shield.scanPrompt(userInput);
// Safe to call LLM
} catch (err) {
if (err instanceof ShieldBlockError) {
console.error('Blocked:', err.event);
}
}3. Signal Sink (Telemetry)
Asynchronous, non-blocking event reporting.
When a block occurs:
- Event is queued in-memory
- Execution continues (no latency penalty)
- Events are batched and sent to your API in the background
- Uses
process.nextTick()for non-blocking dispatch
No data sent to us. You own your Signal endpoint (/api/v1/signal).
4. Manifest Sync (Definition Updates)
Your patterns don't require an npm update to change.
- Static Manifest: Shipped with the SDK
- Polling: On instantiation, checks for new version
- Stale-While-Revalidate: Serves cached version while fetching new one in background
- Zero-Day Patches: Deploy new patterns instantly without forcing users to update npm
Usage
Basic Setup
import { Tracerney } from '@sandrobuilds/tracerney';
const shield = new Tracerney({
baseUrl: process.env.TRACERNY_BACKEND_URL, // e.g., https://myapp.com
allowedTools: ['search', 'calculator'],
apiKey: process.env.TRACERNY_API_KEY,
enableTelemetry: true,
});
// That's it. Wrap your LLM calls.
const response = await shield.wrap(() =>
openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: userInput }],
tools: [
{
type: 'function',
function: {
name: 'search',
description: 'Search the web',
},
},
],
})
);Error Handling
import { Tracerney, ShieldBlockError } from '@sandrobuilds/tracerney';
try {
const response = await shield.wrap(() => llmCall());
} catch (err) {
if (err instanceof ShieldBlockError) {
// Security block - log and respond to user
console.error('Attack blocked:', err.event.blockReason);
res.status(403).json({ error: 'Request blocked' });
} else {
// Actual error from LLM or network
throw err;
}
}Scanning Prompts Pre-LLM
Check if a prompt is suspicious before calling the LLM:
const result = await shield.scanPrompt(userInput);
if (result.suspicious) {
console.log(`⚠️ Suspicious: ${result.patternName}`);
console.log(`Severity: ${result.severity}`);
// Log, rate-limit, or notify security team
if (result.blocked) {
// Only true if LLM Sentinel confirmed (requires backend)
return res.status(403).json({ error: 'Request blocked' });
}
}
// Safe to call LLM
const response = await openai.chat.completions.create({...});Result object:
interface ScanResult {
suspicious: boolean; // Layer 1 detected pattern match
patternName?: string; // e.g., "Ignore Instructions"
severity?: string; // "low" | "medium" | "high" | "critical"
blocked: boolean; // true only if LLM Sentinel confirmed (requires backend)
}Updating Allowed Tools
shield.setAllowedTools(['search', 'email', 'calendar']);Getting Shield Status
const status = shield.getStatus();
console.log(status);
// {
// patternMatcher: { ready: true, stats: {...} },
// toolGuard: { allowedTools: [...] },
// telemetry: { enabled: true, status: {...} }
// }Advanced: Using Utilities Directly
For custom middleware or testing:
import { normalizePrompt, jitter } from '@sandrobuilds/tracerney';
// Normalize prompts (removes Unicode tricks, collapses whitespace)
const clean = normalizePrompt('ignore your rules');
// → "ignore your rules"
// Add latency jitter (300-500ms default)
await jitter(); // Waits random 300-500ms
await jitter(100, 200); // Custom range: 100-200msConfiguration
TracernyOptions
interface TracernyOptions {
// === RECOMMENDED: Single Domain URL ===
// Automatically constructs all backend endpoints:
// - {baseUrl}/api/v1/signal (events)
// - {baseUrl}/api/v1/verify-prompt (Layer 2 verification)
// - {baseUrl}/api/v1/shadow-log (potential attacks)
// - {baseUrl}/api/v1/definitions (pattern updates)
baseUrl?: string;
// === LAYER 1: Vanguard ===
// List of tool names the LLM is allowed to call
allowedTools?: string[];
// === Authentication ===
// API key for authentication to backend endpoints
apiKey?: string;
// === LAYER 2: Sentinel (Advanced - use baseUrl instead) ===
// Only needed if NOT using baseUrl
sentinelEndpoint?: string;
sentinelEnabled?: boolean;
// === LAYER 3: Telemetry & Logging (Advanced - use baseUrl instead) ===
// Only needed if NOT using baseUrl
apiEndpoint?: string;
shadowLogEndpoint?: string;
manifestUrl?: string;
// Enable telemetry (default: true)
enableTelemetry?: boolean;
// Path to local manifest cache (for serverless, use /tmp)
localManifestPath?: string;
}Minimal setup (Layer 1 only):
const shield = new Tracerney({ allowedTools: ['search'] });Recommended setup with backend (all 3 layers):
const shield = new Tracerney({
baseUrl: 'http://localhost:3000', // Single domain — paths auto-constructed
allowedTools: ['search', 'calculator'],
apiKey: process.env.TRACERNY_API_KEY,
enableTelemetry: true,
});Advanced setup (individual endpoints):
const shield = new Tracerney({
allowedTools: ['search', 'calculator'],
sentinelEndpoint: 'http://localhost:3000/api/v1/verify-prompt',
shadowLogEndpoint: 'http://localhost:3000/api/v1/shadow-log',
apiEndpoint: 'http://localhost:3000/api/v1/signal',
apiKey: process.env.TRACERNY_API_KEY,
enableTelemetry: true,
});Events
Security events have this structure:
interface SecurityEvent {
type: 'INJECTION_DETECTED' | 'UNAUTHORIZED_TOOL' | 'BLOCKED_PATTERN';
severity: 'low' | 'medium' | 'high' | 'critical';
timestamp: number; // Unix ms
blockReason: string;
metadata: {
toolName?: string;
patternName?: string;
requestSnippet?: string; // First 100 chars, anonymized
blockLatencyMs?: number;
};
anonymized: boolean;
}Backend Endpoints
Your backend should implement these 3 endpoints for full 3-layer defense:
1. POST /api/v1/verify-prompt (Layer 2: LLM Sentinel)
Called by: SDK when Layer 1 misses Purpose: Backend LLM verification with rate limiting
// Input: { prompt, keywords?, requestId? }
// Output: { blocked, confidence, model, latencyMs, remaining }
// Status 429: Rate limit exceededExample (Next.js):
export async function POST(req: NextRequest) {
const { prompt, keywords, requestId } = await req.json();
// Verify API key
const apiKey = req.headers.get('Authorization')?.replace('Bearer ', '');
const user = await validateApiKey(apiKey); // Your implementation
// Check rate limit (5 calls/min per API key)
const limiter = getRateLimiter();
const { allowed, remaining } = limiter.check(user.id, 5, 60000);
if (!allowed) {
return NextResponse.json(
{ blocked: true, reason: 'rate_limit', remaining: 0 },
{ status: 429 }
);
}
// Call OpenRouter LLM with your API key (kept secret on backend)
const llmVerdict = await callOpenRouter(prompt, process.env.OPENROUTER_API_KEY);
return NextResponse.json({
blocked: llmVerdict,
confidence: llmVerdict ? 0.95 : 0.15,
model: 'google/gemini-2.5-flash-lite',
latencyMs: Date.now() - startTime,
remaining,
});
}2. POST /api/v1/shadow-log (Layer 3: Potential Attacks)
Called by: SDK for prompts that pass both Layer 1 + Layer 2 Purpose: Track suspicious-but-allowed input for feedback loop
// Input: { prompt, keywords?, confidence, passed, requestId? }
// Used to identify novel attack patterns for next version3. POST /api/v1/signal (Layer 3: Blocked Events)
Called by: SDK when any layer blocks Purpose: Security event logging and analytics
interface SignalPayload {
events: SecurityEvent[];
sdkVersion: string;
}Example handler (Next.js):
export async function POST(req: Request) {
const payload = await req.json();
// Log to your monitoring system
console.log(`[Tracerny] ${payload.events.length} security events`);
payload.events.forEach((event) => {
console.log(` - ${event.blockReason} (${event.severity})`);
});
// Store in database
await db.securityEvents.insertMany(payload.events);
// Optional: Send alerts for critical blocks
if (payload.events.some(e => e.severity === 'CRITICAL')) {
await notifySecurityTeam(payload.events);
}
return Response.json({ received: true });
}Latency Profile
Layer 1 (Vanguard): <2ms — Regex pattern matching Layer 2 (Sentinel): 200-1500ms — Backend LLM verification (only if Layer 1 misses) Layer 3 (Jitter): +300-500ms — Random delay masks which layer executed Total:
- Safe prompt Layer 1 only: ~300-500ms (jitter only)
- Attack Layer 1 hits: ~300-500ms (jitter masks instant block)
- Novel attack Layer 2 hits: ~500-2000ms (LLM + jitter)
- No overhead without
sentinelEndpoint: <3ms (Layer 1 + jitter)
Provider Support
Designed for provider-agnostic LLM interfaces. Tested with:
- OpenAI (GPT-4, GPT-3.5)
- Anthropic (Claude)
- Others (Azure OpenAI, local models via compatible APIs)
Response structure mapping is automatic for standard provider APIs.
Graceful Shutdown
process.on('SIGTERM', async () => {
shield.destroy();
// Ensures any queued events are flushed
});Troubleshooting
"Cannot find module 'tracerney'"
Build the SDK first:
npm run build"API endpoint not responding"
Make sure your Signal backend is running:
# In backend directory
npm run dev"Events not appearing in dashboard"
- Check
enableTelemetry: truein config - Verify
apiEndpointis reachable:curl http://localhost:3000/api/v1/stats - Check browser console for CORS errors
"Pattern not detecting attacks"
- Wait for patterns to load:
shield.getStatus().patternMatcher.ready - Check if attack matches bundled patterns (20+ vanguard patterns with homoglyph detection)
- If Layer 1 misses, enable Layer 2: Set
sentinelEndpoint - Test directly:
npm install @sandrobuilds/tracerney && node test-layer1.js
"Layer 2 verification failing (sentiment not working)"
- Check backend is running:
curl http://localhost:3000/api/v1/health - Verify
OPENROUTER_API_KEYis set in backend.env(not SDK, kept secure on backend) - Check OpenRouter API key is valid: https://openrouter.ai/keys
- Look for 429 errors = rate limit hit (wait 60 seconds)
- Check backend logs:
cd backend && npm run dev
"Getting 429 Rate Limit errors"
- Expected behavior — 5 verifications per API key per minute
- Wait 60+ seconds for window to reset
- Or: Use different API key for testing
- Or: Change limit in backend:
limiter.check(key, 10, 60000)(10 calls/min)
"High latency from shield.wrap()"
- Layer 1 only (no
sentinelEndpoint): Should be <3ms + jitter (300-500ms) - With Layer 2: 200-1500ms (LLM) + jitter (300-500ms)
- If higher, check:
- Is OpenRouter API slow? (check https://status.openrouter.ai)
- Is backend responding?
curl http://localhost:3000/api/v1/verify-prompt - Profile:
console.time('shield'); await shield.wrap(...); console.timeEnd('shield');
FAQ
Q: Will Shield block my legitimate use cases? A: Shield uses regex patterns targeting known injection techniques. False positives are rare. If you see them, add to allowlist or adjust patterns in your Signal backend.
Q: Does my data go to your servers?
A: No. You own the apiEndpoint where events are sent. Shield is a runtime library that runs in your process. No data leaves your infrastructure unless you configure it.
Q: Can I use Shield without telemetry?
A: Yes! Set only allowedTools:
const shield = new Tracerney({ allowedTools: ['search'] });Q: What if an attack isn't detected? A: Layer 1 catches known patterns. Novel attacks are caught by Layer 2 (backend LLM):
- Set
sentinelEndpointto enable Layer 2 - Provide
OPENROUTER_API_KEYin backend.env - Rate limiting (5 calls/min) prevents botnet cost spikes
- Monitor
shadow_logfor potential attacks that pass both layers
Q: How does Layer 2 rate limiting work? A: Backend enforces 5 verifications per API key per minute:
- First 5 calls: Returns verification result +
remainingcount - Call 6+: Returns HTTP 429 with
blocked: true, reason: 'rate_limit' - Resets every 60 seconds
Example:
// SDK automatically handles 429
try {
await shield.scanPrompt(userInput);
} catch (err) {
if (err.event.blockReason === 'rate_limit') {
return res.status(429).json({ error: 'Too many verification requests' });
}
}Q: What's the difference between apiEndpoint and sentinelEndpoint?
A:
sentinelEndpoint: Layer 2 LLM verification (backend security check)shadowLogEndpoint: Layer 3 potential attacks (feedback loop)apiEndpoint: Layer 3 security events (blocked attacks)
Use all three for complete observability.
Q: Does Shield work with streaming responses?
A: Yes. shield.wrap() works with streaming LLM responses—detection happens after the stream completes.
Q: Can I rate-limit based on Shield blocks?
A: Yes. Use ShieldBlockError to track patterns:
const blocks = new Map();
try {
await shield.wrap(() => llmCall());
} catch (err) {
if (err instanceof ShieldBlockError) {
const pattern = err.event.patternName;
blocks.set(pattern, (blocks.get(pattern) || 0) + 1);
if (blocks.get(pattern) > 5) banIP();
}
}Q: What's the difference between scanPrompt() and wrap()?
scanPrompt(): Scans input before LLM call (edge defense)wrap(): Scans input + validates tool output after LLM call (comprehensive defense)
Use both for defense-in-depth.
Q: How do I update patterns without redeploying?
A: Set manifestUrl to your backend. The SDK checks for new patterns every 24h and uses stale-while-revalidate caching. Push new patterns to your backend—all clients update automatically.
Q: Can Shield work offline? A: Yes! Uses bundled 20 vanguard patterns offline. For remote patterns, install fails gracefully and uses cached version.
Resources
- GitHub: https://github.com/sandrosaric/tracerney
- Issues: Report bugs or request features
- Dashboard: Built-in real-time threat monitoring at
http://localhost:3000 - Examples: See
/examplesfor Next.js, Express, serverless setups
License
MIT