shrike-guard 4.2.0 → 4.3.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/CHANGELOG.md +33 -2
- package/dist/ai/index.d.mts +2 -3
- package/dist/ai/index.d.ts +2 -3
- package/dist/ai/index.js +26 -2
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/index.mjs +26 -2
- package/dist/ai/index.mjs.map +1 -1
- package/dist/anthropic/index.d.mts +1 -1
- package/dist/anthropic/index.d.ts +1 -1
- package/dist/anthropic/index.js +4 -1
- package/dist/anthropic/index.js.map +1 -1
- package/dist/anthropic/index.mjs +4 -1
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/api/index.d.mts +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/index.mjs +1 -1
- package/dist/api/index.mjs.map +1 -1
- package/dist/claude-agent/index.d.mts +10 -3
- package/dist/claude-agent/index.d.ts +10 -3
- package/dist/claude-agent/index.js +43 -4
- package/dist/claude-agent/index.js.map +1 -1
- package/dist/claude-agent/index.mjs +43 -4
- package/dist/claude-agent/index.mjs.map +1 -1
- package/dist/gemini/index.d.mts +1 -1
- package/dist/gemini/index.d.ts +1 -1
- package/dist/gemini/index.js +4 -1
- package/dist/gemini/index.js.map +1 -1
- package/dist/gemini/index.mjs +4 -1
- package/dist/gemini/index.mjs.map +1 -1
- package/dist/{scanner-U1SygSM2.d.mts → govern-olQtsipX.d.mts} +262 -1
- package/dist/{scanner-U1SygSM2.d.ts → govern-olQtsipX.d.ts} +262 -1
- package/dist/index.d.mts +42 -16
- package/dist/index.d.ts +42 -16
- package/dist/index.js +68 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +68 -7
- package/dist/index.mjs.map +1 -1
- package/dist/langchain/index.d.mts +2 -3
- package/dist/langchain/index.d.ts +2 -3
- package/dist/langchain/index.js +34 -3
- package/dist/langchain/index.js.map +1 -1
- package/dist/langchain/index.mjs +34 -3
- package/dist/langchain/index.mjs.map +1 -1
- package/dist/openai/index.d.mts +1 -1
- package/dist/openai/index.d.ts +1 -1
- package/dist/openai/index.js +4 -1
- package/dist/openai/index.js.map +1 -1
- package/dist/openai/index.mjs +4 -1
- package/dist/openai/index.mjs.map +1 -1
- package/dist/openai-agents/index.d.mts +2 -3
- package/dist/openai-agents/index.d.ts +2 -3
- package/dist/openai-agents/index.js +18 -1
- package/dist/openai-agents/index.js.map +1 -1
- package/dist/openai-agents/index.mjs +18 -1
- package/dist/openai-agents/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/govern-R_Lx6i1l.d.ts +0 -202
- package/dist/govern-rW3QdqsQ.d.mts +0 -202
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config.ts","../../src/errors.ts","../../src/sanitizer.ts","../../package.json","../../src/version.ts","../../src/scanner.ts","../../src/anthropic/client.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Configuration constants and types for the Shrike Guard SDK.\n */\n\nimport { randomUUID } from 'node:crypto';\n\n/**\n * Defines behavior when scan operations fail.\n */\nexport enum FailMode {\n /**\n * Allow the request to proceed (fail-open). Use this mode when\n * availability is prioritized over strict security.\n */\n OPEN = 'open',\n\n /**\n * Block the request and raise an exception (fail-closed). This is the\n * default behavior — matching the MCP server's security posture. Use\n * fail-open only when you've explicitly decided availability matters more.\n */\n CLOSED = 'closed',\n}\n\n/** Default timeout for scan requests in milliseconds */\nexport const DEFAULT_SCAN_TIMEOUT = 10000;\n\n/** Default fail mode - block requests when scan fails (fail-closed) */\nexport const DEFAULT_FAIL_MODE = FailMode.CLOSED;\n\n/** Default Shrike API endpoint (uses load balancer for scalability) */\nexport const DEFAULT_ENDPOINT = 'https://api.shrikesecurity.com/agent';\n\n/** Default rate limit (requests per minute) */\nexport const DEFAULT_RATE_LIMIT_PER_MINUTE = 100;\n\n// Note: Scan depth is set by the backend from the license tier (community = L1-L5 deterministic layers; Pro and above = full L1-L9 including LLM semantic, response intel and session correlation).\n// Detection logic is backend-side. The one deliberate exception is PII redaction,\n// which runs locally from a bundled pattern set so redaction survives a backend\n// outage; see piiRedactor / piiSync.\n\n/** SDK identification */\nexport const SDK_NAME = 'typescript';\nexport const SDK_USER_AGENT = 'shrike-guard-typescript';\n\n/**\n * Process-wide session and agent identity. The session id is the backend's\n * multi-turn correlation key; the agent id is what a declared scope is\n * enforced against. Both can be overridden per client (`sessionId` /\n * `agentId` options and `forSession()`).\n */\nconst SESSION_ID = randomUUID();\nconst AGENT_ID = `sdk-ts-${randomUUID().slice(0, 8)}`;\n\n/** Returns the stable session ID for this SDK process. */\nexport function getSessionId(): string {\n return SESSION_ID;\n}\n\n/**\n * Returns the agent ID: SHRIKE_AGENT_ID when set, so a deployment can name its\n * agents; otherwise an id generated once per process. The variable is read at\n * call time rather than at import, matching the Go and Python SDKs, so setting\n * it after import still takes effect and the shared contract\n * (`agent_id_env_override` in canonical-request-shapes.json) can be tested\n * in-process.\n */\nexport function getAgentId(): string {\n return process.env.SHRIKE_AGENT_ID || AGENT_ID;\n}\n\nlet processSessionWarned = false;\n\n/**\n * Log, once per process, that scans are using the process-wide session id.\n *\n * The process-wide default suits a CLI, a worker or a single agent, and gives\n * those callers multi-turn correlation without configuration.\n *\n * It does not suit a server handling many end users: session identity is the\n * key the backend accumulates risk against, so every user sharing one id shares\n * one risk score, and one user's refusal counts against the next user's action.\n * The SDK cannot tell the two deployments apart, so the default is kept and\n * stated once. Pass `sessionId` to the client, or derive a per-request client\n * with `forSession()`, and the message is not emitted.\n *\n * Silence it with `SHRIKE_SUPPRESS_SESSION_WARNING=1`.\n */\nexport function warnOnceAboutTheProcessSession(): void {\n if (processSessionWarned) return;\n processSessionWarned = true;\n\n if (process.env.SHRIKE_SUPPRESS_SESSION_WARNING) return;\n\n console.warn(\n '[shrike-guard] Using the process-wide session id. This suits a single ' +\n 'agent; a server handling many end users should pass sessionId or use ' +\n 'client.forSession(<per-request id>) so each user has its own session. ' +\n 'Set SHRIKE_SUPPRESS_SESSION_WARNING=1 to silence this message.'\n );\n}\n\n/** Test seam: forget that the warning was emitted. */\nexport function resetProcessSessionWarningForTests(): void {\n processSessionWarned = false;\n}\n\n/**\n * Approval information returned when a policy requires human sign-off.\n */\nexport interface ApprovalInfo {\n requires_approval: boolean;\n approval_id: string;\n approval_level: string;\n action_summary: string;\n policy_name: string;\n expires_in_seconds: number;\n threat_type?: string;\n severity?: string;\n owasp_category?: string;\n risk_factors?: string[];\n original_action?: string;\n}\n\n/**\n * Retry configuration for transient failure resilience.\n * Matches MCP server's RETRY_CONFIG for consistent behavior.\n */\nexport const RETRY_CONFIG = {\n maxRetries: 2,\n initialDelayMs: 200,\n maxDelayMs: 2000,\n backoffMultiplier: 2,\n retryableErrors: ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'fetch failed'],\n};\n","/**\n * Custom exceptions for the Shrike Guard SDK.\n */\n\n/**\n * Base exception for all Shrike SDK errors.\n */\nexport class ShrikeError extends Error {\n public readonly details: Record<string, unknown>;\n\n constructor(message: string, details: Record<string, unknown> = {}) {\n super(message);\n this.name = 'ShrikeError';\n this.details = details;\n // Maintain proper prototype chain\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * Raised when a scan operation fails and fail_mode is 'closed'.\n *\n * This exception is raised when:\n * - The Shrike API times out\n * - A network error occurs\n * - The API returns an unexpected error\n *\n * The default fail_mode is 'closed' (secure by default), so a scan\n * failure raises this exception. Set fail_mode='open' to instead\n * silently handle these errors and allow the request to proceed.\n */\nexport class ShrikeScanError extends ShrikeError {\n constructor(message: string, details: Record<string, unknown> = {}) {\n super(message, details);\n this.name = 'ShrikeScanError';\n }\n}\n\n/**\n * Raised when a prompt is blocked by Shrike security checks.\n *\n * This exception indicates that the prompt was scanned and determined\n * to be unsafe. The scan result details are available in the `details`\n * attribute.\n */\nexport class ShrikeBlockedError extends ShrikeError {\n /** The type of threat detected (e.g., 'prompt_injection', 'pii') */\n public readonly threatType?: string;\n\n /** The confidence level of the detection ('high', 'medium', 'low') or raw score (0.0-1.0) */\n public readonly confidence?: number | string;\n\n /** List of specific violations detected */\n public readonly violations: unknown[];\n\n constructor(\n message: string,\n threatType?: string,\n confidence?: number | string,\n violations: unknown[] = []\n ) {\n super(message, {\n threat_type: threatType,\n confidence,\n violations,\n });\n this.name = 'ShrikeBlockedError';\n this.threatType = threatType;\n this.confidence = confidence;\n this.violations = violations;\n }\n}\n\n/**\n * Raised when there's a configuration error in the SDK.\n */\nexport class ShrikeConfigError extends ShrikeError {\n constructor(message: string, details: Record<string, unknown> = {}) {\n super(message, details);\n this.name = 'ShrikeConfigError';\n }\n}\n\n/**\n * Raised when the client-side rate limit is exceeded.\n */\nexport class ShrikeRateLimitError extends ShrikeError {\n /** Milliseconds until the next request will be allowed */\n public readonly retryAfterMs: number;\n\n constructor(retryAfterMs: number) {\n super(`Rate limit exceeded. Retry after ${retryAfterMs}ms`, {\n retry_after_ms: retryAfterMs,\n });\n this.name = 'ShrikeRateLimitError';\n this.retryAfterMs = retryAfterMs;\n }\n}\n","/**\n * Response sanitization for IP protection.\n *\n * Mirrors the MCP server's responseFormatter.ts to ensure SDK responses\n * do not expose internal detection methodology, layer details, or patterns.\n */\n\nimport type { ScanResult, ScanViolation } from './scanner';\n\n/**\n * Standard threat types exposed to SDK users (matches MCP ThreatType enum).\n */\nconst THREAT_TYPE_MAP: Record<string, string> = {\n // Prompt injection variants\n prompt_injection: 'prompt_injection',\n injection: 'prompt_injection',\n inject: 'prompt_injection',\n instruction_override: 'prompt_injection',\n role_hijacking: 'prompt_injection',\n context_manipulation: 'prompt_injection',\n token_manipulation: 'prompt_injection',\n indirect_injection: 'prompt_injection',\n context_poisoning: 'prompt_injection',\n function_injection: 'prompt_injection',\n memory_injection: 'prompt_injection',\n topic_mismatch: 'prompt_injection',\n // Jailbreak\n jailbreak: 'jailbreak',\n jailbreak_attempt: 'jailbreak',\n safety_bypass: 'jailbreak',\n roleplay: 'jailbreak',\n hypothetical: 'jailbreak',\n completion_baiting: 'jailbreak',\n override: 'jailbreak',\n manipulate: 'jailbreak',\n // L8 tonality drift — backend normalizes profanity + hostile as toxic_content;\n // casual stays as jailbreak (matches platform/common/models/response.go:309-313).\n tonality_drift_profanity: 'toxic_content',\n tonality_drift_hostile: 'toxic_content',\n tonality_drift_casual: 'jailbreak',\n // System prompt leak\n system_prompt_leak: 'system_prompt_leak',\n system_prompt_extraction: 'system_prompt_leak',\n // Data exfiltration\n data_exfiltration: 'data_exfiltration',\n exfiltration: 'data_exfiltration',\n exfiltrate: 'data_exfiltration',\n extract: 'data_exfiltration',\n data_leak: 'data_exfiltration',\n information_disclosure: 'data_exfiltration',\n credential_extraction: 'data_exfiltration',\n // SQL injection\n sql_injection: 'sql_injection',\n sqli: 'sql_injection',\n tautology: 'sql_injection',\n tautology_or: 'sql_injection',\n tautology_and: 'sql_injection',\n union_injection: 'sql_injection',\n stacked_query: 'sql_injection',\n // Path traversal\n path_traversal: 'path_traversal',\n directory_traversal: 'path_traversal',\n path_violation: 'path_traversal',\n file_access: 'path_traversal',\n sensitive_path: 'path_traversal',\n sensitive_extension: 'path_traversal',\n blocked_extension: 'path_traversal',\n // Secrets\n secrets_exposure: 'secrets_exposure',\n secrets: 'secrets_exposure',\n api_key: 'secrets_exposure',\n credential: 'secrets_exposure',\n sensitive_file: 'secrets_exposure',\n content_violation: 'secrets_exposure',\n sensitive_content: 'secrets_exposure',\n secret_key: 'secrets_exposure',\n aws_key: 'secrets_exposure',\n private_key: 'secrets_exposure',\n // PII\n pii_exposure: 'pii_exposure',\n pii: 'pii_exposure',\n pii_leak: 'pii_exposure',\n personal_data: 'pii_exposure',\n pii_in_search: 'pii_exposure',\n pii_extraction: 'pii_exposure',\n ssn: 'pii_exposure',\n credit_card: 'pii_exposure',\n email_exposure: 'pii_exposure',\n phone_number: 'pii_exposure',\n unexpected_pii_leakage: 'pii_exposure',\n // Domain blocking\n blocked_domain: 'blocked_domain',\n suspicious_tld: 'blocked_domain',\n suspicious_domain: 'blocked_domain',\n malicious_url: 'blocked_domain',\n // Toxic content (canonical name as of the L7 rewrite). `toxicity` kept as a\n // legacy alias so older backend builds + customer code don't break.\n toxic_content: 'toxic_content',\n toxicity: 'toxic_content',\n harmful_content: 'toxic_content',\n // Malicious code\n malicious_content: 'malicious_code',\n malicious_code: 'malicious_code',\n reverse_shell: 'malicious_code',\n web_shell: 'malicious_code',\n fork_bomb: 'malicious_code',\n crypto_miner: 'malicious_code',\n persistence: 'malicious_code',\n shell_injection: 'malicious_code',\n // Harmful intent\n harmful_intent: 'harmful_intent',\n dangerous_request: 'harmful_intent',\n // Social engineering\n social_engineering: 'social_engineering',\n emotional: 'social_engineering',\n authority_claim: 'social_engineering',\n // Privilege escalation\n privilege_escalation: 'privilege_escalation',\n // Destructive operation\n destructive_operation: 'destructive_operation',\n // L9 multi-turn correlation — the pseudo-category emitted when any\n // multi_turn_* pattern fires (crescendo, blocked_retry, trust-building,\n // memory_poisoning, etc.). The normalizeThreatType prefix check below\n // handles the specific pattern names; this entry catches the pseudo\n // value if the backend pre-normalizes.\n multi_turn_attack: 'multi_turn_attack',\n // Errors\n scan_error: 'scan_error',\n size_limit_exceeded: 'size_limit_exceeded',\n size_limit: 'size_limit_exceeded',\n timeout: 'scan_error',\n};\n\n/**\n * User-friendly guidance (matches MCP THREAT_GUIDANCE).\n */\nconst THREAT_GUIDANCE: Record<string, string> = {\n prompt_injection:\n 'This prompt contains patterns consistent with instruction override attempts.',\n jailbreak:\n 'This prompt attempts to bypass safety guidelines. The request has been blocked.',\n system_prompt_leak:\n 'The response contains system prompt disclosure. The response has been blocked.',\n data_exfiltration:\n 'This prompt may attempt to extract sensitive information.',\n sql_injection: 'This query contains potentially dangerous SQL patterns.',\n path_traversal:\n 'This file path attempts to access directories outside the allowed scope.',\n secrets_exposure:\n 'This content contains patterns matching API keys, tokens, or credentials.',\n pii_exposure: 'This content contains personally identifiable information.',\n blocked_domain: 'This web search targets a restricted domain.',\n toxic_content:\n 'This content contains potentially harmful or inappropriate language.',\n toxicity:\n 'This content contains potentially harmful or inappropriate language.',\n multi_turn_attack:\n 'A pattern was detected across multiple turns of this session that suggests a coordinated attempt to bypass safety controls.',\n malicious_code:\n 'This content contains patterns associated with malicious code.',\n harmful_intent:\n 'This request contains content associated with harmful intent.',\n social_engineering:\n 'This prompt contains social engineering patterns.',\n privilege_escalation:\n 'This query attempts to escalate privileges or gain unauthorized access.',\n destructive_operation:\n 'This query contains destructive operations. Review carefully.',\n scan_error:\n 'The security scan could not be completed. Blocked as precaution.',\n size_limit_exceeded: 'The content exceeds the maximum allowed size.',\n unknown: 'A security concern was detected. Please review the content.',\n};\n\n/**\n * Default severity per normalized threat type (matches MCP severity semantics).\n * critical > high > medium > low\n */\nconst THREAT_SEVERITY: Record<string, string> = {\n prompt_injection: 'high',\n jailbreak: 'high',\n system_prompt_leak: 'high',\n data_exfiltration: 'high',\n sql_injection: 'critical',\n path_traversal: 'high',\n secrets_exposure: 'critical',\n pii_exposure: 'high',\n blocked_domain: 'medium',\n toxic_content: 'medium',\n toxicity: 'medium',\n multi_turn_attack: 'high',\n malicious_code: 'critical',\n harmful_intent: 'high',\n social_engineering: 'medium',\n privilege_escalation: 'critical',\n destructive_operation: 'critical',\n scan_error: 'medium',\n size_limit_exceeded: 'low',\n unknown: 'medium',\n};\n\n/**\n * Fields that expose internal detection methodology - must be stripped.\n */\nconst INTERNAL_FIELDS = new Set([\n 'detected_by',\n 'policy_id',\n 'policy_name',\n 'matched_pattern',\n 'matched_text',\n 'pattern',\n 'scan_stage',\n 'ai_reasoning',\n 'llm_analysis',\n 'performance_metrics',\n 'performance',\n]);\n\n/**\n * Normalize internal threat type to standard enum.\n */\nexport function normalizeThreatType(rawType?: string): string {\n if (!rawType) {\n return 'unknown';\n }\n const normalized = rawType.toLowerCase().replace(/-/g, '_');\n if (THREAT_TYPE_MAP[normalized]) {\n return THREAT_TYPE_MAP[normalized];\n }\n // L9 multi-turn correlation patterns flow as `multi_turn_<pattern>`\n // (crescendo, blocked_retry, topic_pivot, threat_diversity, safe_then_unsafe,\n // tool_sequence_anomaly, coded_language_setup, context_overflow,\n // memory_poisoning, velocity_burst). Backend collapses these to\n // multi_turn_attack — mirror the prefix logic from\n // platform/common/models/response.go:380.\n if (normalized.startsWith('multi_turn_')) {\n return 'multi_turn_attack';\n }\n return 'unknown';\n}\n\n/**\n * Derive severity from threat type or pass through raw severity.\n *\n * If the backend provides a severity, validate and use it.\n * Otherwise, derive from the normalized threat type.\n */\nexport function deriveSeverity(\n threatType: string,\n rawSeverity?: string\n): string {\n const validSeverities = new Set(['critical', 'high', 'medium', 'low']);\n if (rawSeverity && validSeverities.has(rawSeverity.toLowerCase())) {\n return rawSeverity.toLowerCase();\n }\n return THREAT_SEVERITY[threatType] || 'medium';\n}\n\n/**\n * Convert raw confidence score to bucketed level.\n *\n * Protects IP by not exposing exact detection thresholds.\n */\nexport function bucketConfidence(score?: number): string {\n if (score === undefined || score === null) {\n return 'medium';\n }\n if (score >= 0.9) {\n return 'high';\n }\n if (score >= 0.7) {\n return 'medium';\n }\n return 'low';\n}\n\n/**\n * Governance-outcome fields the sanitizer preserves verbatim on BOTH safe\n * and unsafe branches. These are OUTCOME state (customer sees outcomes;\n * timings/attribution stay provider-only) — the contract-symmetry surface\n * (safe/refuse_tier/recovery/session_state on every response).\n */\nconst PRESERVED_GOVERNANCE_FIELDS = [\n 'action',\n 'refuse_tier',\n 'recovery',\n 'session_state',\n 'content_type',\n // Provenance: human_prompt / agent_output / agent_action / third_party.\n // An OUTCOME field, not detection attribution — it says who is answerable\n // for the scanned content, which the caller needs in order to know who a\n // refusal is even addressed to. It shipped on the backend in 2cfaa5f and was\n // silently dropped here, because this is an allow-list and nobody added it:\n // a new backend field reaches no caller until it appears in this array.\n 'content_origin',\n 'approval_info',\n 'client_session_rotation',\n] as const;\n\n/**\n * Sanitize one entry from the backend violations[] array.\n * Preserves customer-visible outcome fields (severity, action, threat_type,\n * owasp_category, user_message, suggested_action); drops attribution\n * fields (policy_id, policy_name, matched_pattern, ai_reasoning, etc.).\n */\nexport function sanitizeViolation(raw: unknown): ScanViolation | null {\n if (!raw || typeof raw !== 'object') {\n return null;\n }\n const source = raw as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(source)) {\n if (!INTERNAL_FIELDS.has(k)) {\n out[k] = v;\n }\n }\n return out as ScanViolation;\n}\n\n/**\n * Sanitize a raw backend scan response for IP protection.\n *\n * Strips internal detection attribution (layer timings, per-detector\n * confidences, pattern names, policy IDs) — NOT outcome state.\n * Preserves the four-state governance surface (`action`, `refuse_tier`,\n * `recovery`, `session_state`) so callers can distinguish\n * allow/warn/require_approval/block. Contract symmetry — safe and refuse\n * verdicts carry the same governance fields.\n */\nexport function sanitizeScanResponse(raw: ScanResult): ScanResult {\n const safe = raw.safe !== false;\n const result: ScanResult = { safe };\n\n // Governance-outcome fields pass through on BOTH branches. This is the\n // symmetric contract — a `warn` verdict (safe=true, action=\"warn\") must\n // carry recovery/refuse_tier the same as a `block` (safe=false).\n for (const field of PRESERVED_GOVERNANCE_FIELDS) {\n const value = (raw as Record<string, unknown>)[field];\n if (value !== undefined && value !== null) {\n (result as Record<string, unknown>)[field] = value;\n }\n }\n\n // violations[] is a mixed shape — pass each entry through per-item\n // sanitization to drop policy_id / policy_name / matched_pattern while\n // keeping customer-visible severity, threat_type, owasp_category,\n // user_message, suggested_action. Empty array is dropped (noise).\n const rawViolations = raw.violations;\n if (Array.isArray(rawViolations) && rawViolations.length > 0) {\n const cleaned = rawViolations\n .map((v) => sanitizeViolation(v))\n .filter((v): v is ScanViolation => v !== null);\n if (cleaned.length > 0) {\n result.violations = cleaned;\n }\n }\n\n if (safe) {\n // Safe branch: reason may carry advisory copy for warn tier.\n result.reason = raw.reason || '';\n return result;\n }\n\n // Unsafe branch: derive threat classification for legacy callers that\n // read top-level threat_type/severity/guidance. Backend /api/scan/enforce\n // returns top-level threat_type=null and puts detail inside violations[];\n // for those responses, prefer the first violation's threat_type when\n // the raw top-level field is missing.\n let rawThreatType = raw.threat_type;\n if (!rawThreatType && Array.isArray(rawViolations) && rawViolations.length > 0) {\n const first = rawViolations[0];\n if (first && typeof first === 'object' && !Array.isArray(first)) {\n const firstThreat = (first as Record<string, unknown>).threat_type;\n if (typeof firstThreat === 'string') {\n rawThreatType = firstThreat;\n }\n }\n }\n\n const threatType = normalizeThreatType(rawThreatType);\n const confidence = bucketConfidence(\n typeof raw.confidence === 'number' ? raw.confidence : undefined\n );\n const severity = deriveSeverity(\n threatType,\n typeof raw.severity === 'string' ? raw.severity : undefined\n );\n const guidance =\n THREAT_GUIDANCE[threatType] || THREAT_GUIDANCE['unknown'];\n\n result.threat_type = threatType;\n result.severity = severity;\n result.confidence = confidence;\n result.reason = raw.reason || guidance;\n result.guidance = guidance;\n return result;\n}\n\nexport { THREAT_TYPE_MAP, THREAT_GUIDANCE, THREAT_SEVERITY, INTERNAL_FIELDS };\n","{\n \"name\": \"shrike-guard\",\n \"version\": \"4.2.0\",\n \"description\": \"Shrike SDK for AI agent action governance — drop-in wrapper for OpenAI, Anthropic, and Gemini that governs every tool call, prompt, and response server-side. Allow / approve / block. Free tier available.\",\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./openai\": {\n \"types\": \"./dist/openai/index.d.ts\",\n \"import\": \"./dist/openai/index.mjs\",\n \"require\": \"./dist/openai/index.js\"\n },\n \"./anthropic\": {\n \"types\": \"./dist/anthropic/index.d.ts\",\n \"import\": \"./dist/anthropic/index.mjs\",\n \"require\": \"./dist/anthropic/index.js\"\n },\n \"./gemini\": {\n \"types\": \"./dist/gemini/index.d.ts\",\n \"import\": \"./dist/gemini/index.mjs\",\n \"require\": \"./dist/gemini/index.js\"\n },\n \"./api\": {\n \"types\": \"./dist/api/index.d.ts\",\n \"import\": \"./dist/api/index.mjs\",\n \"require\": \"./dist/api/index.js\"\n },\n \"./claude-agent\": {\n \"types\": \"./dist/claude-agent/index.d.ts\",\n \"import\": \"./dist/claude-agent/index.mjs\",\n \"require\": \"./dist/claude-agent/index.js\"\n },\n \"./openai-agents\": {\n \"types\": \"./dist/openai-agents/index.d.ts\",\n \"import\": \"./dist/openai-agents/index.mjs\",\n \"require\": \"./dist/openai-agents/index.js\"\n },\n \"./ai\": {\n \"types\": \"./dist/ai/index.d.ts\",\n \"import\": \"./dist/ai/index.mjs\",\n \"require\": \"./dist/ai/index.js\"\n },\n \"./langchain\": {\n \"types\": \"./dist/langchain/index.d.ts\",\n \"import\": \"./dist/langchain/index.mjs\",\n \"require\": \"./dist/langchain/index.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"test\": \"jest\",\n \"test:coverage\": \"jest --coverage\",\n \"lint\": \"eslint src --ext .ts\",\n \"typecheck\": \"tsc --noEmit\",\n \"prepublishOnly\": \"npm run build\"\n },\n \"keywords\": [\n \"action-governance\",\n \"ai-agents\",\n \"agent-security\",\n \"agent-governance\",\n \"tool-call-gating\",\n \"ai-governance\",\n \"openai\",\n \"anthropic\",\n \"claude\",\n \"gemini\",\n \"llm\",\n \"prompt-injection\",\n \"pii\",\n \"jailbreak\",\n \"ai-safety\",\n \"claude-agent-sdk\",\n \"openai-agents\",\n \"vercel-ai-sdk\",\n \"langchain\",\n \"langgraph\",\n \"agent-framework\"\n ],\n \"author\": \"Shrike Security <security@shrikesecurity.com>\",\n \"license\": \"Apache-2.0\",\n \"homepage\": \"https://shrikesecurity.com\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Shrike-Security/shrike-guard-js\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/Shrike-Security/shrike-guard-js/issues\"\n },\n \"peerDependencies\": {\n \"@anthropic-ai/sdk\": \">=0.18.0\",\n \"@google/genai\": \">=1.0.0\",\n \"openai\": \">=4.0.0\",\n \"@anthropic-ai/claude-agent-sdk\": \">=0.1.0\",\n \"@openai/agents\": \">=0.1.0\",\n \"ai\": \">=5.0.0\",\n \"langchain\": \">=1.0.0\",\n \"@langchain/core\": \">=1.0.0\",\n \"zod\": \">=3.23.0\"\n },\n \"peerDependenciesMeta\": {\n \"openai\": {\n \"optional\": true\n },\n \"@anthropic-ai/sdk\": {\n \"optional\": true\n },\n \"@google/genai\": {\n \"optional\": true\n },\n \"@anthropic-ai/claude-agent-sdk\": {\n \"optional\": true\n },\n \"@openai/agents\": {\n \"optional\": true\n },\n \"ai\": {\n \"optional\": true\n },\n \"langchain\": {\n \"optional\": true\n },\n \"@langchain/core\": {\n \"optional\": true\n },\n \"zod\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@anthropic-ai/sdk\": \"^0.126.0\",\n \"@google/genai\": \"^2.12.0\",\n \"@types/jest\": \"^29.5.12\",\n \"@types/node\": \"^20.11.0\",\n \"jest\": \"^29.7.0\",\n \"openai\": \"^7.15.0\",\n \"ts-jest\": \"^29.1.2\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^5.3.3\",\n \"@anthropic-ai/claude-agent-sdk\": \"^0.3.273\",\n \"@openai/agents\": \"^0.18.0\",\n \"ai\": \"^7.0.102\",\n \"langchain\": \"^1.5.11\",\n \"@langchain/core\": \"^1.1.0\",\n \"zod\": \"^4.0.0\"\n },\n \"engines\": {\n \"node\": \">=18.0.0\"\n }\n}\n","// SDK version — derived from package.json at build time (tsup/esbuild inlines\n// the JSON literal, resolveJsonModule is on), so it can never drift from the\n// published version. It was previously a hardcoded '1.5.0' that fell out of\n// sync with package.json (4.0.2) and mis-stamped X-Shrike-SDK-Version on every\n// scan. Guarded by tests/unit/version.test.ts. Mirrors the Python SDK, which\n// derives __version__ from installed distribution metadata.\nimport { version } from '../package.json';\n\nexport const VERSION: string = version;\n","/**\n * HTTP client for the Shrike scan API.\n * Includes retry with exponential backoff, session correlation,\n * approval workflow, 401 key refresh, and rate limiting.\n */\n\nimport {\n DEFAULT_ENDPOINT,\n DEFAULT_SCAN_TIMEOUT,\n DEFAULT_RATE_LIMIT_PER_MINUTE,\n SDK_NAME,\n RETRY_CONFIG,\n getSessionId,\n getAgentId,\n warnOnceAboutTheProcessSession,\n} from './config';\nimport type { ApprovalInfo } from './config';\nimport { AUTO_CHUNK_THRESHOLD, aggregateChunkResults, chunkContent } from './chunker';\nimport { ShrikeRateLimitError } from './errors';\nimport { RateLimiter } from './rateLimiter';\nimport { sanitizeScanResponse } from './sanitizer';\nimport { VERSION } from './version';\n\n/**\n * Phase 8b: Client-side size limits to fail fast before network round-trip.\n * These limits match the backend limits for consistency.\n */\nconst MAX_CONTENT_SIZE = 100 * 1024; // 100KB - matches backend MaxRequestBodySize\n\n/**\n * Result from a scan operation.\n */\n/**\n * Correlation pattern from L9 session-aware correlation engine.\n */\nexport interface CorrelationPattern {\n pattern_id: string;\n pattern_name: string;\n category: string;\n confidence: number;\n description: string;\n}\n\n/**\n * Governance action per Cooperative Governance 4-state contract (allow / warn / require_approval / block).\n * The `action` field is the server-authoritative signal for proceed-vs-refuse;\n * `isBlocked()` in this module consumes it directly.\n */\nexport type ScanAction = 'allow' | 'warn' | 'require_approval' | 'block';\n\n/**\n * Recovery guidance emitted by the backend on refuse verdicts.\n * `available_tools` names the MCP tools the caller can still invoke while\n * under quarantine (typically read-only surfaces like session_status).\n */\nexport interface ScanRecovery {\n instruction?: string;\n available_tools?: string[];\n patterns_triggered?: string[];\n [key: string]: unknown;\n}\n\n/**\n * Session-level state carried on every scan response (contract symmetry).\n * Fields are outcome state per the dashboard IP boundary — customer-visible.\n */\nexport interface ScanSessionState {\n session_risk_score?: number;\n session_turn_number?: number;\n session_patterns?: string[];\n session_locked?: boolean;\n [key: string]: unknown;\n}\n\n/**\n * A single policy violation as emitted by the /api/scan/enforce endpoint.\n * The sanitizer strips policy_id / policy_name / matched_pattern / other\n * attribution before this reaches callers; what remains here is the\n * customer-visible outcome surface.\n */\nexport interface ScanViolation {\n threat_type?: string;\n owasp_category?: string;\n severity?: string;\n action?: string;\n user_message?: string;\n suggested_action?: string;\n [key: string]: unknown;\n}\n\n/**\n * Provenance of scanned content. See {@link ScanResult.content_origin}.\n */\nexport type ContentOrigin =\n | 'human_prompt'\n | 'agent_output'\n | 'agent_action'\n | 'third_party';\n\n/**\n * True when the operator is answerable for this content — i.e. a person\n * typed it. Everything else was produced by the agent or arrived from\n * outside, and a refusal on it is not something the operator did.\n *\n * Use it to decide who a refusal message is addressed to: telling a user\n * \"your request was blocked\" when the agent poisoned its own context is\n * both wrong and unhelpful.\n */\nexport function attributableToOperator(origin?: ContentOrigin): boolean {\n return origin === 'human_prompt';\n}\n\nexport interface ScanResult {\n safe: boolean;\n reason?: string;\n threat_type?: string;\n severity?: string;\n confidence?: number | string;\n violations?: ScanViolation[];\n guidance?: string;\n approval_info?: ApprovalInfo;\n /** Cooperative Governance action — server-authoritative proceed/refuse signal */\n action?: ScanAction;\n /** Refuse tier: allow / warn / require_approval / block */\n refuse_tier?: ScanAction;\n /** Recovery guidance, present on refuse verdicts */\n recovery?: ScanRecovery;\n /** L9 session correlation state — outcome, customer-visible */\n session_state?: ScanSessionState;\n /** Specialized scan input type, e.g. 'sql' / 'file_path' / 'a2a_message' */\n content_type?: string;\n /**\n * Where the scanned content came from — who is answerable for it.\n *\n * - `human_prompt` the operator typed it\n * - `agent_output` the model generated it\n * - `agent_action` the agent is about to do it (every act-plane surface)\n * - `third_party` it arrived from outside: a tool result, a retrieved\n * document, a peer agent\n *\n * This is the field that answers \"was that my prompt, or the agent acting\n * on its own?\" — the question you cannot reconstruct after the fact from a\n * verdict alone. Unknown content types resolve to `agent_action`, never to\n * `human_prompt`: attributing an unattributable action to the operator is\n * the one error that is never safe to make.\n */\n content_origin?: ContentOrigin;\n /**\n * True when the agent's declared scope held this action (expired, tool\n * outside the scope, or action ceiling reached). The hold never skips the\n * content scan: a content block wins and stands as the verdict; otherwise\n * the verdict is require_approval and content_verdict carries what the\n * content scan said.\n */\n held_by_scope?: boolean;\n /** The content scan's own answer on a held action; absent when the content was blocked */\n content_verdict?: {\n safe: boolean;\n refuse_tier: ScanAction;\n threat_type?: string;\n severity?: string;\n };\n /** Client-side session rotation guidance */\n client_session_rotation?: unknown;\n /** L9 session risk score (0.0-1.0), present when session correlation is active */\n session_risk_score?: number;\n /** L9 matched correlation patterns, present when multi-turn patterns are detected */\n correlation_patterns?: CorrelationPattern[];\n /**\n * True when this is a fail-open verdict returned without a completed scan —\n * i.e. `failMode: 'open'` was set and the backend was unreachable, so\n * enforcement was skipped. Lets a caller distinguish \"scanned and clean\"\n * from \"not scanned, allowed anyway.\"\n */\n degraded?: boolean;\n [key: string]: unknown;\n}\n\n/**\n * Build a fail-open ALLOW verdict. Returned only when `failMode === 'open'` is\n * explicitly set and the scan could not complete (timeout, backend error).\n * Marked `degraded: true` — centralize all fail-open returns through this so the\n * marker can never be forgotten on a new wrapper.\n */\nexport function failOpenResult(reason: string): ScanResult {\n return { safe: true, reason, degraded: true };\n}\n\n/**\n * Result of ScanClient.declareScope — the persisted scope row plus derived\n * active_until + expired fields. Mirrors the shape returned by\n * POST /api/v1/agent/scope/declare on the backend.\n */\nexport interface DeclareScopeResult {\n scope_id?: string;\n agent_id?: string;\n purpose?: string;\n allowed_tools?: string[];\n forbidden_tools?: string[];\n max_duration_seconds?: number;\n expires_at?: string;\n active_until?: string;\n expired?: boolean;\n /** Renewal window: until when this key may refresh the scope itself.\n Absent means no ceiling; ceiling_reached means only an operator can now. */\n renewable_seconds?: number;\n renewable_until?: string;\n ceiling_reached?: boolean;\n created_at?: string;\n updated_at?: string;\n}\n\n/**\n * Options for the ScanClient.\n */\nexport interface ScanClientOptions {\n /** Shrike API key for authentication */\n apiKey: string;\n /** Shrike API endpoint URL */\n endpoint?: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n /** Rate limit in requests per minute (default: 100) */\n rateLimitPerMinute?: number;\n /** Optional callback to refresh API key on 401. Return new key or null. */\n onKeyRefresh?: () => Promise<string | null>;\n /**\n * The session this client scans under. Session identity is the key the\n * backend accumulates multi-turn risk against, so it should mean one unit\n * of work: one agent run, one conversation, one user's request. Defaults to\n * a process-wide id, which suits a CLI or a worker but not a server serving\n * many end users, where each user needs its own session. Prefer\n * `client.forSession(id)` per request.\n */\n sessionId?: string;\n /**\n * The agent this client scans as. Defaults to the process-wide id\n * (`SHRIKE_AGENT_ID` when set). Set it when one process drives several\n * distinct agents, so scope enforcement and agent attribution land on the\n * right one.\n */\n agentId?: string;\n}\n\n/**\n * Generate a UUID v4 string.\n */\nfunction generateUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Generate headers for scan API requests.\n */\nexport function getScanHeaders(\n shrikeApiKey: string,\n requestId?: string\n): Record<string, string> {\n return {\n Authorization: `Bearer ${shrikeApiKey}`,\n 'Content-Type': 'application/json',\n 'X-Shrike-SDK': SDK_NAME,\n 'X-Shrike-SDK-Version': VERSION,\n 'X-Shrike-Request-ID': requestId || generateUUID(),\n };\n}\n\n/** Check if an error is transient and retryable. */\nfunction isRetryableError(error: unknown): boolean {\n if (error instanceof Error) {\n const cause = 'cause' in error ? String((error as { cause?: unknown }).cause) : '';\n const errorStr = `${error.name} ${error.message} ${cause}`;\n return RETRY_CONFIG.retryableErrors.some((e) => errorStr.includes(e));\n }\n return false;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Fetch with retry for cold-start and transient failure resilience.\n * Matches MCP server's fetchWithRetry pattern.\n */\nasync function fetchWithRetry(\n url: string,\n options: RequestInit,\n timeoutMs: number,\n apiKey: string,\n onKeyRefresh?: () => Promise<string | null>\n): Promise<Response> {\n let lastError: Error | null = null;\n let delay = RETRY_CONFIG.initialDelayMs;\n\n for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n ...options,\n signal: controller.signal,\n });\n clearTimeout(timeoutId);\n\n // On 401, try key refresh and retry once\n if (response.status === 401 && onKeyRefresh && attempt === 0) {\n const newKey = await onKeyRefresh();\n if (newKey && newKey !== apiKey) {\n const retryController = new AbortController();\n const retryTimeout = setTimeout(() => retryController.abort(), timeoutMs);\n try {\n const headers = {\n ...(options.headers as Record<string, string>),\n Authorization: `Bearer ${newKey}`,\n };\n const retryResponse = await fetch(url, {\n ...options,\n headers,\n signal: retryController.signal,\n });\n clearTimeout(retryTimeout);\n return retryResponse;\n } catch {\n clearTimeout(retryTimeout);\n // Fall through to return original 401\n }\n }\n }\n\n return response;\n } catch (error) {\n clearTimeout(timeoutId);\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry on abort (timeout) or non-retryable errors\n if (lastError.name === 'AbortError' || !isRetryableError(error)) {\n throw lastError;\n }\n\n if (attempt < RETRY_CONFIG.maxRetries) {\n await sleep(delay);\n delay = Math.min(delay * RETRY_CONFIG.backoffMultiplier, RETRY_CONFIG.maxDelayMs);\n }\n }\n }\n\n throw lastError || new Error('Fetch failed after retries');\n}\n\n/**\n * Build the session context object for backend requests.\n *\n * `sessionId` / `agentId` override the process-wide defaults. When no session\n * id is supplied we warn once — see warnOnceAboutTheProcessSession for why the\n * default stays rather than being removed.\n */\nfunction buildSessionContext(\n extraContext?: Record<string, unknown>,\n sessionId?: string,\n agentId?: string\n): Record<string, unknown> {\n if (!sessionId) warnOnceAboutTheProcessSession();\n\n return {\n ...extraContext,\n session_id: sessionId || getSessionId(),\n agent_id: agentId || getAgentId(),\n source_application: 'shrike-guard-ts',\n };\n}\n\n/**\n * HTTP client for the Shrike scan API.\n */\n/**\n * Decide whether a scan verdict should be enforced as a block.\n *\n * Prefers the server-authoritative `action` field emitted by\n * /api/scan/enforce. Falls back to the legacy\n * `safe` boolean for verdicts that predate the enforce endpoint (older\n * backends, circuit-breaker fail-open synthetic verdicts, cached responses).\n *\n * Behavior:\n * - action = \"block\" → true\n * - action = \"require_approval\" → true (held actions are refused at the\n * tool-call boundary; caller can re-issue\n * after approval lands)\n * - action = \"allow\" → false\n * - action = \"warn\" → false (advisory; caller should surface\n * via block-feedback but not refuse)\n * - action absent → fall back to `!safe`\n *\n * This helper is the ONLY place the SDK converts a verdict into a\n * proceed-vs-refuse decision. Every LLM wrapper (OpenAI, Anthropic, Gemini)\n * routes through it so behavior stays uniform.\n *\n * Contract-symmetry pin: mirrors Python `_is_blocked` in shrike_guard.scanner.\n * If either SDK diverges, that is a bug in one of the two.\n */\nexport function isBlocked(verdict: ScanResult | Record<string, unknown>): boolean {\n const action = (verdict as Record<string, unknown>).action;\n if (typeof action === 'string' && action.length > 0) {\n // Known non-blocking tiers proceed.\n if (action === 'allow' || action === 'warn') return false;\n // Known blocking tiers refuse.\n if (action === 'block' || action === 'require_approval') return true;\n // Unknown/future tier (e.g. a new \"quarantine\" the backend adds): do NOT\n // fail open on an unrecognized name — fall through to `safe`. A new\n // blocking tier also sets safe:false, so this fails closed until the\n // client learns the tier explicitly. Forward-compat contract, see\n // tests/unit/contract-forward-compat.test.ts.\n }\n const safe = (verdict as Record<string, unknown>).safe;\n if (typeof safe === 'boolean') {\n return !safe;\n }\n return false;\n}\n\n/**\n * When running without an API key, appends a signup hint to scan results\n * so agents/users know they can upgrade from regex-only to full scanning.\n */\nexport function maybeAddSignupHint(result: ScanResult, apiKey: string): ScanResult {\n if (apiKey) return result;\n // Don't override if backend already provided upgrade_hint\n if ((result as any).upgrade_hint) return result;\n return {\n ...result,\n _note: 'Running without API key (L1-L5 only). Register free for cognitive threat detection: npx shrike-mcp --signup',\n };\n}\n\nexport class ScanClient {\n private apiKey: string;\n private readonly endpoint: string;\n private readonly timeout: number;\n private readonly rateLimiter: RateLimiter;\n private readonly onKeyRefresh?: () => Promise<string | null>;\n private readonly sessionId?: string;\n private readonly agentId?: string;\n\n constructor(options: ScanClientOptions) {\n this.apiKey = options.apiKey;\n this.endpoint = (options.endpoint || DEFAULT_ENDPOINT).replace(/\\/$/, '');\n this.timeout = options.timeout || DEFAULT_SCAN_TIMEOUT;\n this.rateLimiter = new RateLimiter(options.rateLimitPerMinute || DEFAULT_RATE_LIMIT_PER_MINUTE);\n this.onKeyRefresh = options.onKeyRefresh;\n this.sessionId = options.sessionId;\n this.agentId = options.agentId;\n\n if (!this.apiKey) {\n console.warn('[shrike-guard] No API key provided — running in free tier (regex-only).');\n console.warn('[shrike-guard] For full scanning (LLM analysis, session correlation): npx shrike-mcp --signup');\n }\n }\n\n /**\n * Return a view of this client that scans under `sessionId`.\n *\n * The returned client SHARES this one's rate limiter, so deriving one per\n * request is cheap and cannot multiply your rate budget. Build one ScanClient\n * at startup and derive a per-request view from it:\n *\n * ```ts\n * const guard = new ScanClient({ apiKey: KEY }); // once, at startup\n *\n * app.post('/act', async (req, res) => { // per request\n * const scoped = guard.forSession(req.session.id);\n * const verdict = await scoped.scanCommand(req.body.command);\n * });\n * ```\n *\n * Without this, every end user shares one session id and therefore one risk\n * score, and one user's refusal counts against the next user's action.\n */\n forSession(sessionId: string, agentId?: string): ScanClient {\n const view: ScanClient = Object.create(ScanClient.prototype);\n // Written through a mutable alias because the fields are readonly on the\n // class: this is a copy constructor, not a mutation of an existing client.\n const w = view as unknown as Record<string, unknown>;\n w.apiKey = this.apiKey;\n w.endpoint = this.endpoint;\n w.timeout = this.timeout;\n // Shared deliberately — a per-request client with its own limiter would\n // give every request a full rate budget and defeat the limit entirely.\n w.rateLimiter = this.rateLimiter;\n w.onKeyRefresh = this.onKeyRefresh;\n w.sessionId = sessionId;\n w.agentId = agentId !== undefined ? agentId : this.agentId;\n return view;\n }\n\n /** Session identity for this client, with any per-call extras merged. */\n private sessionContext(extraContext?: Record<string, unknown>): Record<string, unknown> {\n return buildSessionContext(extraContext, this.sessionId, this.agentId);\n }\n\n /** Check rate limit before each request. */\n private checkRateLimit(): void {\n const result = this.rateLimiter.consume();\n if (!result.allowed) {\n throw new ShrikeRateLimitError(result.retryAfterMs!);\n }\n }\n\n /**\n * Scan a prompt for security threats.\n *\n * @param prompt - The user prompt to scan.\n * @param context - Optional conversation context for better analysis.\n * @returns Scan result with 'safe' boolean and additional details.\n * @throws Error if the request fails or times out.\n */\n async scan(\n prompt: string,\n context?: string,\n options?: { plane?: string }\n ): Promise<ScanResult> {\n // Client-side size validation to fail fast\n const totalSize = prompt.length + (context?.length || 0);\n if (totalSize > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `Content too large (${Math.round(totalSize / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n // Auto-chunk large prompts. Below the threshold we keep the single-shot\n // path unchanged; above it we split on natural boundaries, scan each\n // chunk sequentially, and stop on the first block verdict. The\n // conversation_history context is NOT chunked — it rides on every scan\n // so downstream layers see the same session shape.\n if (prompt.length > AUTO_CHUNK_THRESHOLD) {\n return this.scanChunked(prompt, context, options);\n }\n\n this.checkRateLimit();\n\n const payload: Record<string, unknown> = {\n prompt,\n scan_type: 'full',\n context: this.sessionContext(options?.plane ? { plane: options.plane } : undefined),\n };\n if (context) {\n payload.conversation_history = context;\n }\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`Scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Chunk-and-scan path for prompts above AUTO_CHUNK_THRESHOLD. Splits on\n * natural boundaries, scans sequentially, aggregates. Stops on the first\n * block verdict (fail-fast) so a compromised chunk doesn't pay for the\n * remaining scans.\n *\n * Known limit: the same session id rides every chunk, so the session turn\n * count grows by the number of chunks.\n */\n private async scanChunked(\n prompt: string,\n context?: string,\n options?: { plane?: string }\n ): Promise<ScanResult> {\n const chunks = chunkContent(prompt);\n const results: ScanResult[] = [];\n for (const chunk of chunks) {\n const chunkResult = await this.scan(chunk, context, options);\n results.push(chunkResult);\n const action = chunkResult.action || chunkResult.refuse_tier;\n if (action === 'block') {\n break;\n }\n }\n return maybeAddSignupHint(aggregateChunkResults(results), this.apiKey);\n }\n\n /**\n * Scan a SQL query for injection attacks.\n *\n * @param query - The SQL query to scan.\n * @param database - Optional database name for context.\n * @param allowDestructive - If true, allows DROP/TRUNCATE operations.\n * @returns Scan result with 'safe' boolean and additional details.\n */\n async scanSql(\n query: string,\n database?: string,\n allowDestructive = false\n ): Promise<ScanResult> {\n if (query.length > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `SQL query too large (${Math.round(query.length / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Query exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const toolContext: Record<string, unknown> = {};\n if (database) {\n toolContext.database = database;\n }\n if (allowDestructive) {\n toolContext.allow_destructive = 'true';\n }\n\n const payload = {\n content: query,\n content_type: 'sql',\n context: this.sessionContext(toolContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`SQL scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan a file path for security risks.\n *\n * @param path - The file path to validate.\n * @param content - Optional file content to scan for secrets/PII.\n * @returns Scan result with 'safe' boolean and additional details.\n */\n async scanFile(path: string, content?: string): Promise<ScanResult> {\n const totalSize = path.length + (content?.length || 0);\n if (totalSize > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `File content too large (${Math.round(totalSize / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const contentType = content ? 'file_content' : 'file_path';\n const extraContext: Record<string, unknown> = {};\n if (content) {\n extraContext.file_content = content;\n }\n\n const payload = {\n content: path,\n content_type: contentType,\n context: this.sessionContext(extraContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`File scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan an A2A (Agent-to-Agent) protocol message for security threats.\n *\n * @param message - The A2A message text content to scan.\n * @param options - Optional context: sender/receiver agent IDs, task ID, role.\n * @returns Scan result with 'safe' boolean and additional details.\n */\n async scanA2AMessage(\n message: string,\n options?: {\n senderAgentId?: string;\n receiverAgentId?: string;\n taskId?: string;\n role?: 'user' | 'agent';\n }\n ): Promise<ScanResult> {\n if (message.length > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `Message too large (${Math.round(message.length / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const extraContext: Record<string, unknown> = {};\n if (options?.senderAgentId) extraContext.sender_agent_id = options.senderAgentId;\n if (options?.receiverAgentId) extraContext.receiver_agent_id = options.receiverAgentId;\n if (options?.taskId) extraContext.task_id = options.taskId;\n if (options?.role) extraContext.role = options.role;\n\n const payload = {\n content: message,\n content_type: 'a2a_message',\n context: this.sessionContext(extraContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`A2A message scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan an A2A AgentCard JSON for security threats.\n *\n * @param agentCard - The raw JSON string of the A2A AgentCard to scan.\n * @param verifySignature - Reserved for future JWS signature verification.\n * @returns Scan result with 'safe' boolean and additional details.\n */\n async scanAgentCard(\n agentCard: string,\n verifySignature = false\n ): Promise<ScanResult> {\n if (agentCard.length > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `Agent card too large (${Math.round(agentCard.length / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const extraContext: Record<string, unknown> = {};\n if (verifySignature) extraContext.verify_signature = 'true';\n\n const payload = {\n content: agentCard,\n content_type: 'agent_card',\n context: this.sessionContext(extraContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`Agent card scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan a shell command before executing it.\n *\n * The highest-volume act-plane surface, and the one the SDK could not\n * reach until 4.1.0: `shrike.scanCommand(cmd)` before you shell out is the\n * enforcement point. Catches destructive operations, fetch-and-execute\n * chains, reverse shells, credential reads, anti-forensics, and SQL\n * injection carried inside a database CLI argument (`psql -c \"...\"`),\n * which the command's own grammar cannot see.\n *\n * @param command - The command line about to be executed.\n * @param cwd - Optional working directory, for context.\n * @returns Scan result; check `safe` and `refuse_tier` before executing.\n */\n /**\n * Ask whether this agent may call a tool, without sending its arguments.\n *\n * For a tool this SDK has no reader for. The operator's declared scope\n * judges a tool by NAME, so a tool nobody can parse is still refused when\n * it is not on the allowlist, and still held when the scope has expired or\n * run out of actions.\n *\n * This answers one question. A permit says the agent was allowed to make\n * the call; it never says the arguments were inspected, because none were\n * sent. Where a tool maps to one of the scanned surfaces, scan that surface\n * instead and get both answers.\n *\n * @param toolName - The tool about to run.\n * @returns Scan result; check `safe` and `refuse_tier` before running.\n */\n async authorizeTool(toolName: string): Promise<ScanResult> {\n this.checkRateLimit();\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/authorize`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify({ tool_name: toolName, context: this.sessionContext() }),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n if (!response.ok) {\n throw new Error(`Tool authorization API returned error: ${response.status}`);\n }\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n async scanCommand(command: string, cwd?: string): Promise<ScanResult> {\n const toolContext: Record<string, unknown> = {};\n if (cwd) toolContext.cwd = cwd;\n return this.scanSpecialized(command, 'command', 'Command', toolContext);\n }\n\n /**\n * Scan a web search query before it reaches an external search engine.\n *\n * Catches PII and credentials leaving through a search box, credential\n * dorking, evasion tradecraft, illicit acquisition, and attack-tool\n * acquisition — while leaving ordinary defensive research alone.\n *\n * @param query - The search query about to be issued.\n * @returns Scan result; check `safe` before searching.\n */\n async scanWebSearch(query: string): Promise<ScanResult> {\n return this.scanSpecialized(query, 'web_search', 'Web search');\n }\n\n /**\n * Scan a single MCP tool definition before trusting or registering it.\n *\n * Detects tool poisoning: instructions hidden in a tool's own\n * `description`, which an agent reads as guidance and acts on without the\n * tool ever executing. Call this on every entry of a `tools/list` response\n * from a server you do not control.\n *\n * @param name - The tool name as advertised.\n * @param description - The tool description to screen.\n * @param inputSchema - Optional JSON schema; also screened.\n * @returns Scan result; do not register the tool when `safe` is false.\n */\n async scanMcpSchema(\n name: string,\n description: string,\n inputSchema?: Record<string, unknown>\n ): Promise<ScanResult> {\n this.checkRateLimit();\n\n const payload = {\n name,\n description,\n ...(inputSchema ? { input_schema: inputSchema } : {}),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/mcp_schema`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`MCP schema scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan retrieved context before feeding it to the model.\n *\n * RAG chunks are untrusted text from documents someone else wrote — the\n * standard carrier for indirect prompt injection. Scan them on the way in,\n * not after the model has already acted on them.\n *\n * @param chunks - The retrieved chunks, as text or an array of texts.\n * @param query - Optional user query the chunks were retrieved for.\n * @returns Scan result; check `safe` before including the context.\n */\n async scanRagContext(chunks: string | string[], query?: string): Promise<ScanResult> {\n const content = Array.isArray(chunks) ? JSON.stringify(chunks) : chunks;\n const toolContext: Record<string, unknown> = {};\n if (query) toolContext.query = query;\n return this.scanSpecialized(content, 'rag_context', 'RAG context', toolContext);\n }\n\n /**\n * Shared transport for the specialized (act-plane) surfaces.\n *\n * Every act-plane method above differs only in content type, the label used\n * in the error message, and any extra context. Before 4.1.0 each surface\n * hand-rolled this block, which is how the SDK ended up with five of the\n * eight channels missing: adding one meant copying forty lines. Now it\n * means one method.\n */\n private async scanSpecialized(\n content: string,\n contentType: string,\n label: string,\n toolContext: Record<string, unknown> = {}\n ): Promise<ScanResult> {\n if (content.length > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `${label} content too large (${Math.round(content.length / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const payload = {\n content,\n content_type: contentType,\n context: this.sessionContext(toolContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`${label} scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Declare (or refresh) the operating scope for a task-scoped agent.\n *\n * Once declared, every subsequent scan for the same agent_id is enforced\n * against the scope on the backend. Tool calls outside allowed_tools —\n * or explicitly on forbidden_tools — route to refuse_tier:\n * \"require_approval\" with threat_type: \"scope_violation\". Expired scopes\n * emit threat_type: \"scope_expired\". Absent a declaration, no scope\n * check runs; every existing SDK integration is unaffected until it\n * opts in.\n *\n * Wire contract mirrors ScanClient.declare_scope on the Python SDK and\n * scan_declare_scope on the shrike-mcp package — do not diverge without\n * updating those siblings.\n *\n * @param options.agentId - Agent identity this scope applies to.\n * @param options.allowedTools - Exact tool names permitted; [\"*\"] = any.\n * @param options.forbiddenTools - Optional. Wins over allowedTools.\n * @param options.purpose - Optional audit + dashboard label.\n * @param options.maxDurationSeconds - Optional TTL from the latest declaration.\n * @param options.expiresAt - Optional ISO-8601 absolute expiry.\n * @param options.renewableSeconds - Optional renewal window: how long, from\n * the operator's grant, this key may keep refreshing the scope. Honoured\n * on a first declaration; on a refresh the stored value always wins.\n * @returns The persisted scope row, including scope_id, active_until and\n * renewable_until.\n *\n * Refreshing: once the scope exists, calling again from the agent's own key\n * is a refresh. Any option left out is inherited from the scope on file, so\n * `{ agentId, maxDurationSeconds: 7200 }` is a complete refresh. A refresh\n * may narrow but never widen (403, reason \"widening\"), and stops working\n * once the operator's renewal window closes (403, reason \"ceiling_reached\").\n */\n async declareScope(options: {\n agentId: string;\n /** Required on a first declaration; omit on a refresh to inherit. */\n allowedTools?: string[];\n forbiddenTools?: string[];\n purpose?: string;\n maxDurationSeconds?: number;\n expiresAt?: string;\n renewableSeconds?: number;\n }): Promise<DeclareScopeResult> {\n this.checkRateLimit();\n\n const payload: Record<string, unknown> = {\n agent_id: options.agentId,\n };\n if (options.allowedTools !== undefined) payload.allowed_tools = options.allowedTools;\n if (options.purpose !== undefined) payload.purpose = options.purpose;\n if (options.forbiddenTools !== undefined) payload.forbidden_tools = options.forbiddenTools;\n if (options.maxDurationSeconds !== undefined) {\n payload.max_duration_seconds = options.maxDurationSeconds;\n }\n if (options.expiresAt !== undefined) payload.expires_at = options.expiresAt;\n if (options.renewableSeconds !== undefined) payload.renewable_seconds = options.renewableSeconds;\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/v1/agent/scope/declare`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new Error(\n `declareScope failed: ${response.status}${text ? ` — ${text.trim()}` : ''}`\n );\n }\n\n return (await response.json()) as DeclareScopeResult;\n }\n}\n","/**\n * ShrikeAnthropic - Drop-in replacement for Anthropic client with security scanning.\n */\n\nimport type Anthropic from '@anthropic-ai/sdk';\nimport type { MessageCreateParams, Message } from '@anthropic-ai/sdk/resources/messages';\n\nimport {\n FailMode,\n DEFAULT_ENDPOINT,\n DEFAULT_FAIL_MODE,\n DEFAULT_SCAN_TIMEOUT,\n getSessionId,\n getAgentId,\n} from '../config';\nimport { ShrikeBlockedError, ShrikeScanError } from '../errors';\nimport { sanitizeScanResponse } from '../sanitizer';\nimport { getScanHeaders, isBlocked, maybeAddSignupHint, ScanResult, failOpenResult } from '../scanner';\n\n/**\n * Simple logger for warnings.\n */\nfunction logWarning(message: string): void {\n console.warn(`⚠️ ${message}`);\n}\n\n/**\n * Content block type for Anthropic messages.\n */\ninterface ContentBlock {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\n/**\n * Anthropic message type.\n */\ninterface AnthropicMessage {\n role: string;\n content: string | ContentBlock[];\n}\n\n/**\n * Options for creating a ShrikeAnthropic client.\n */\nexport interface ShrikeAnthropicOptions {\n /** Anthropic API key. If not provided, uses ANTHROPIC_API_KEY env var. */\n apiKey?: string;\n /** Shrike API key for authentication with the scan service. */\n shrikeApiKey?: string;\n /** Shrike API endpoint URL. */\n shrikeEndpoint?: string;\n /** Behavior on scan failure - 'open' (allow) or 'closed' (block). */\n failMode?: FailMode | 'open' | 'closed';\n /** Timeout for scan requests in milliseconds. */\n scanTimeout?: number;\n /** Additional options passed to the Anthropic client. */\n anthropicOptions?: Record<string, unknown>;\n}\n\n/**\n * Drop-in replacement for Anthropic with Shrike security protection.\n *\n * This class wraps the official Anthropic client and automatically scans\n * all prompts before they are sent to Claude. If a prompt is detected\n * as unsafe, the request is blocked and a ShrikeBlockedError is raised.\n *\n * @example\n * ```typescript\n * import { ShrikeAnthropic } from 'shrike-guard/anthropic';\n *\n * const client = new ShrikeAnthropic({\n * apiKey: 'sk-ant-...',\n * shrikeApiKey: 'shrike-...',\n * });\n *\n * const message = await client.messages.create({\n * model: 'claude-3-opus-20240229',\n * max_tokens: 1024,\n * messages: [{ role: 'user', content: 'Hello!' }],\n * });\n * ```\n */\nexport class ShrikeAnthropic {\n private _anthropic: Anthropic;\n private _shrikeEndpoint: string;\n private _shrikeApiKey: string;\n private _failMode: FailMode;\n private _scanTimeout: number;\n\n /** Namespace for messages operations */\n public readonly messages: MessagesNamespace;\n\n constructor(options: ShrikeAnthropicOptions = {}) {\n // Dynamically import Anthropic to make it a peer dependency\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const AnthropicClient = require('@anthropic-ai/sdk').default;\n\n this._anthropic = new AnthropicClient({\n apiKey: options.apiKey,\n ...options.anthropicOptions,\n });\n this._shrikeEndpoint = (options.shrikeEndpoint || DEFAULT_ENDPOINT).replace(/\\/$/, '');\n this._shrikeApiKey = options.shrikeApiKey || '';\n this._failMode =\n typeof options.failMode === 'string'\n ? (options.failMode as FailMode)\n : options.failMode || DEFAULT_FAIL_MODE;\n this._scanTimeout = options.scanTimeout || DEFAULT_SCAN_TIMEOUT;\n\n // Note: Scan depth is set by the backend from the license tier (community = L1-L5 deterministic layers; Pro and above = full L1-L9 including LLM semantic, response intel and session correlation).\n // No local scanning - backend has full regex patterns (~50+) and normalizers\n\n if (!this._shrikeApiKey) {\n console.warn('[shrike-guard] No shrikeApiKey provided — running in free tier (regex-only).');\n console.warn('[shrike-guard] For full scanning (LLM analysis, session correlation): npx shrike-mcp --signup');\n }\n\n // Expose messages interface\n this.messages = new MessagesNamespace(this);\n }\n\n /**\n * Extract all user message content from a messages list.\n */\n _extractUserContent(messages: AnthropicMessage[]): string {\n const userContents: string[] = [];\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n if (typeof msg.content === 'string') {\n userContents.push(msg.content);\n } else if (Array.isArray(msg.content)) {\n for (const block of msg.content) {\n if (block.type === 'text' && block.text) {\n userContents.push(block.text);\n }\n }\n }\n }\n }\n\n return userContents.join('\\n');\n }\n\n /**\n * Scan user messages for security threats via backend API.\n *\n * Always calls backend - backend handles tier-based scanning:\n * - Community tier (no API key): L1-L5 (regex, unicode, malformed, encoding, token/semantic)\n * - Pro tier and above: L1-L9 (adds visual, LLM semantic, response intel, session correlation)\n */\n async _scanMessages(messages: AnthropicMessage[]): Promise<ScanResult> {\n const userContent = this._extractUserContent(messages);\n\n if (!userContent.trim()) {\n return { safe: true, reason: 'No user content to scan' };\n }\n\n // Always call backend API - tier detection happens server-side\n return this._remoteScan(userContent);\n }\n\n /**\n * Full scan via Shrike backend API.\n * Backend handles tier-based scanning automatically based on API key presence.\n */\n private async _remoteScan(prompt: string): Promise<ScanResult> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this._scanTimeout);\n\n try {\n const response = await fetch(`${this._shrikeEndpoint}/api/scan/enforce`, {\n method: 'POST',\n headers: getScanHeaders(this._shrikeApiKey),\n body: JSON.stringify({\n prompt,\n context: {\n session_id: getSessionId(),\n agent_id: getAgentId(),\n source_application: 'shrike-guard-ts',\n },\n }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n if (this._failMode === FailMode.OPEN) {\n return failOpenResult(`Scan API error: ${response.status}`);\n }\n throw new ShrikeScanError(`Scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this._shrikeApiKey);\n } catch (error) {\n if (error instanceof ShrikeScanError) {\n throw error;\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n if (errorMessage.includes('abort')) {\n if (this._failMode === FailMode.OPEN) {\n // No local fallback - just fail open\n logWarning('Scan request timed out, failing open (allowing request)');\n return failOpenResult('Scan timeout, failing open');\n }\n throw new ShrikeScanError(\"Scan request timed out and fail_mode is 'closed'\");\n }\n\n if (this._failMode === FailMode.OPEN) {\n return failOpenResult(`Scan error: ${errorMessage}`);\n }\n throw new ShrikeScanError(`Scan failed: ${errorMessage}`);\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /** Access the underlying Anthropic client */\n get anthropic(): Anthropic {\n return this._anthropic;\n }\n}\n\n/**\n * Namespace for messages operations.\n */\nclass MessagesNamespace {\n private _client: ShrikeAnthropic;\n\n constructor(client: ShrikeAnthropic) {\n this._client = client;\n }\n\n /**\n * Create a message with security scanning.\n *\n * This method scans the user messages before sending them to Anthropic.\n * If the scan detects a security threat, a ShrikeBlockedError is raised.\n */\n async create(params: MessageCreateParams): Promise<Message> {\n // 1. Scan messages for security threats\n const scanResult = await this._client._scanMessages(\n params.messages as unknown as AnthropicMessage[]\n );\n\n // 2. Block if unsafe\n if (isBlocked(scanResult)) {\n throw new ShrikeBlockedError(\n `Request blocked: ${scanResult.reason || 'Security threat detected'}`,\n scanResult.threat_type,\n scanResult.confidence,\n scanResult.violations || []\n );\n }\n\n // 3. Proxy to Anthropic (non-streaming)\n return this._client.anthropic.messages.create({\n ...params,\n stream: false,\n }) as unknown as Promise<Message>;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/config.ts","../../src/errors.ts","../../src/sanitizer.ts","../../package.json","../../src/version.ts","../../src/scanner.ts","../../src/anthropic/client.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Configuration constants and types for the Shrike Guard SDK.\n */\n\nimport { randomUUID } from 'node:crypto';\n\n/**\n * Defines behavior when scan operations fail.\n */\nexport enum FailMode {\n /**\n * Allow the request to proceed (fail-open). Use this mode when\n * availability is prioritized over strict security.\n */\n OPEN = 'open',\n\n /**\n * Block the request and raise an exception (fail-closed). This is the\n * default behavior — matching the MCP server's security posture. Use\n * fail-open only when you've explicitly decided availability matters more.\n */\n CLOSED = 'closed',\n}\n\n/** Default timeout for scan requests in milliseconds */\nexport const DEFAULT_SCAN_TIMEOUT = 10000;\n\n/** Default fail mode - block requests when scan fails (fail-closed) */\nexport const DEFAULT_FAIL_MODE = FailMode.CLOSED;\n\n/** Default Shrike API endpoint (uses load balancer for scalability) */\nexport const DEFAULT_ENDPOINT = 'https://api.shrikesecurity.com/agent';\n\n/** Default rate limit (requests per minute) */\nexport const DEFAULT_RATE_LIMIT_PER_MINUTE = 100;\n\n// Note: Scan depth is set by the backend from the license tier (community = L1-L5 deterministic layers; Pro and above = full L1-L9 including LLM semantic, response intel and session correlation).\n// Detection logic is backend-side. The one deliberate exception is PII redaction,\n// which runs locally from a bundled pattern set so redaction survives a backend\n// outage; see piiRedactor / piiSync.\n\n/** SDK identification */\nexport const SDK_NAME = 'typescript';\nexport const SDK_USER_AGENT = 'shrike-guard-typescript';\n\n/**\n * Process-wide session and agent identity. The session id is the backend's\n * multi-turn correlation key; the agent id is what a declared scope is\n * enforced against. Both can be overridden per client (`sessionId` /\n * `agentId` options and `forSession()`).\n */\nconst SESSION_ID = randomUUID();\nconst AGENT_ID = `sdk-ts-${randomUUID().slice(0, 8)}`;\n\n/** Returns the stable session ID for this SDK process. */\nexport function getSessionId(): string {\n return SESSION_ID;\n}\n\n/**\n * Returns the agent ID: SHRIKE_AGENT_ID when set, so a deployment can name its\n * agents; otherwise an id generated once per process. The variable is read at\n * call time rather than at import, matching the Go and Python SDKs, so setting\n * it after import still takes effect and the shared contract\n * (`agent_id_env_override` in canonical-request-shapes.json) can be tested\n * in-process.\n */\nexport function getAgentId(): string {\n return process.env.SHRIKE_AGENT_ID || AGENT_ID;\n}\n\nlet processSessionWarned = false;\n\n/**\n * Log, once per process, that scans are using the process-wide session id.\n *\n * The process-wide default suits a CLI, a worker or a single agent, and gives\n * those callers multi-turn correlation without configuration.\n *\n * It does not suit a server handling many end users: session identity is the\n * key the backend accumulates risk against, so every user sharing one id shares\n * one risk score, and one user's refusal counts against the next user's action.\n * The SDK cannot tell the two deployments apart, so the default is kept and\n * stated once. Pass `sessionId` to the client, or derive a per-request client\n * with `forSession()`, and the message is not emitted.\n *\n * Silence it with `SHRIKE_SUPPRESS_SESSION_WARNING=1`.\n */\nexport function warnOnceAboutTheProcessSession(): void {\n if (processSessionWarned) return;\n processSessionWarned = true;\n\n if (process.env.SHRIKE_SUPPRESS_SESSION_WARNING) return;\n\n console.warn(\n '[shrike-guard] Using the process-wide session id. This suits a single ' +\n 'agent; a server handling many end users should pass sessionId or use ' +\n 'client.forSession(<per-request id>) so each user has its own session. ' +\n 'Set SHRIKE_SUPPRESS_SESSION_WARNING=1 to silence this message.'\n );\n}\n\n/** Test seam: forget that the warning was emitted. */\nexport function resetProcessSessionWarningForTests(): void {\n processSessionWarned = false;\n}\n\n/**\n * Approval information returned when a policy requires human sign-off.\n */\nexport interface ApprovalInfo {\n requires_approval: boolean;\n approval_id: string;\n approval_level: string;\n action_summary: string;\n policy_name: string;\n expires_in_seconds: number;\n threat_type?: string;\n severity?: string;\n owasp_category?: string;\n risk_factors?: string[];\n original_action?: string;\n}\n\n/**\n * Retry configuration for transient failure resilience.\n * Matches MCP server's RETRY_CONFIG for consistent behavior.\n */\nexport const RETRY_CONFIG = {\n maxRetries: 2,\n initialDelayMs: 200,\n maxDelayMs: 2000,\n backoffMultiplier: 2,\n retryableErrors: ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'fetch failed'],\n};\n","/**\n * Custom exceptions for the Shrike Guard SDK.\n */\n\n/**\n * Base exception for all Shrike SDK errors.\n */\nexport class ShrikeError extends Error {\n public readonly details: Record<string, unknown>;\n\n constructor(message: string, details: Record<string, unknown> = {}) {\n super(message);\n this.name = 'ShrikeError';\n this.details = details;\n // Maintain proper prototype chain\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * Raised when a scan operation fails and fail_mode is 'closed'.\n *\n * This exception is raised when:\n * - The Shrike API times out\n * - A network error occurs\n * - The API returns an unexpected error\n *\n * The default fail_mode is 'closed' (secure by default), so a scan\n * failure raises this exception. Set fail_mode='open' to instead\n * silently handle these errors and allow the request to proceed.\n */\nexport class ShrikeScanError extends ShrikeError {\n constructor(message: string, details: Record<string, unknown> = {}) {\n super(message, details);\n this.name = 'ShrikeScanError';\n }\n}\n\n/**\n * Raised when a prompt is blocked by Shrike security checks.\n *\n * This exception indicates that the prompt was scanned and determined\n * to be unsafe. The scan result details are available in the `details`\n * attribute.\n */\nexport class ShrikeBlockedError extends ShrikeError {\n /** The type of threat detected (e.g., 'prompt_injection', 'pii') */\n public readonly threatType?: string;\n\n /** The confidence level of the detection ('high', 'medium', 'low') or raw score (0.0-1.0) */\n public readonly confidence?: number | string;\n\n /** List of specific violations detected */\n public readonly violations: unknown[];\n\n constructor(\n message: string,\n threatType?: string,\n confidence?: number | string,\n violations: unknown[] = []\n ) {\n super(message, {\n threat_type: threatType,\n confidence,\n violations,\n });\n this.name = 'ShrikeBlockedError';\n this.threatType = threatType;\n this.confidence = confidence;\n this.violations = violations;\n }\n}\n\n/**\n * Raised when there's a configuration error in the SDK.\n */\nexport class ShrikeConfigError extends ShrikeError {\n constructor(message: string, details: Record<string, unknown> = {}) {\n super(message, details);\n this.name = 'ShrikeConfigError';\n }\n}\n\n/**\n * Raised when the client-side rate limit is exceeded.\n */\nexport class ShrikeRateLimitError extends ShrikeError {\n /** Milliseconds until the next request will be allowed */\n public readonly retryAfterMs: number;\n\n constructor(retryAfterMs: number) {\n super(`Rate limit exceeded. Retry after ${retryAfterMs}ms`, {\n retry_after_ms: retryAfterMs,\n });\n this.name = 'ShrikeRateLimitError';\n this.retryAfterMs = retryAfterMs;\n }\n}\n","/**\n * Response sanitization for IP protection.\n *\n * Mirrors the MCP server's responseFormatter.ts to ensure SDK responses\n * do not expose internal detection methodology, layer details, or patterns.\n */\n\nimport type { ScanResult, ScanViolation } from './scanner';\n\n/**\n * Standard threat types exposed to SDK users (matches MCP ThreatType enum).\n */\nconst THREAT_TYPE_MAP: Record<string, string> = {\n // Prompt injection variants\n prompt_injection: 'prompt_injection',\n injection: 'prompt_injection',\n inject: 'prompt_injection',\n instruction_override: 'prompt_injection',\n role_hijacking: 'prompt_injection',\n context_manipulation: 'prompt_injection',\n token_manipulation: 'prompt_injection',\n indirect_injection: 'prompt_injection',\n context_poisoning: 'prompt_injection',\n function_injection: 'prompt_injection',\n memory_injection: 'prompt_injection',\n topic_mismatch: 'prompt_injection',\n // Jailbreak\n jailbreak: 'jailbreak',\n jailbreak_attempt: 'jailbreak',\n safety_bypass: 'jailbreak',\n roleplay: 'jailbreak',\n hypothetical: 'jailbreak',\n completion_baiting: 'jailbreak',\n override: 'jailbreak',\n manipulate: 'jailbreak',\n // L8 tonality drift — backend normalizes profanity + hostile as toxic_content;\n // casual stays as jailbreak (matches platform/common/models/response.go:309-313).\n tonality_drift_profanity: 'toxic_content',\n tonality_drift_hostile: 'toxic_content',\n tonality_drift_casual: 'jailbreak',\n // System prompt leak\n system_prompt_leak: 'system_prompt_leak',\n system_prompt_extraction: 'system_prompt_leak',\n // Data exfiltration\n data_exfiltration: 'data_exfiltration',\n exfiltration: 'data_exfiltration',\n exfiltrate: 'data_exfiltration',\n extract: 'data_exfiltration',\n data_leak: 'data_exfiltration',\n information_disclosure: 'data_exfiltration',\n credential_extraction: 'data_exfiltration',\n // SQL injection\n sql_injection: 'sql_injection',\n sqli: 'sql_injection',\n tautology: 'sql_injection',\n tautology_or: 'sql_injection',\n tautology_and: 'sql_injection',\n union_injection: 'sql_injection',\n stacked_query: 'sql_injection',\n // Path traversal\n path_traversal: 'path_traversal',\n directory_traversal: 'path_traversal',\n path_violation: 'path_traversal',\n file_access: 'path_traversal',\n sensitive_path: 'path_traversal',\n sensitive_extension: 'path_traversal',\n blocked_extension: 'path_traversal',\n // Secrets\n secrets_exposure: 'secrets_exposure',\n secrets: 'secrets_exposure',\n api_key: 'secrets_exposure',\n credential: 'secrets_exposure',\n sensitive_file: 'secrets_exposure',\n content_violation: 'secrets_exposure',\n sensitive_content: 'secrets_exposure',\n secret_key: 'secrets_exposure',\n aws_key: 'secrets_exposure',\n private_key: 'secrets_exposure',\n // PII\n pii_exposure: 'pii_exposure',\n pii: 'pii_exposure',\n pii_leak: 'pii_exposure',\n personal_data: 'pii_exposure',\n pii_in_search: 'pii_exposure',\n pii_extraction: 'pii_exposure',\n ssn: 'pii_exposure',\n credit_card: 'pii_exposure',\n email_exposure: 'pii_exposure',\n phone_number: 'pii_exposure',\n unexpected_pii_leakage: 'pii_exposure',\n // Domain blocking\n blocked_domain: 'blocked_domain',\n suspicious_tld: 'blocked_domain',\n suspicious_domain: 'blocked_domain',\n malicious_url: 'blocked_domain',\n // Toxic content (canonical name as of the L7 rewrite). `toxicity` kept as a\n // legacy alias so older backend builds + customer code don't break.\n toxic_content: 'toxic_content',\n toxicity: 'toxic_content',\n harmful_content: 'toxic_content',\n // Malicious code\n malicious_content: 'malicious_code',\n malicious_code: 'malicious_code',\n reverse_shell: 'malicious_code',\n web_shell: 'malicious_code',\n fork_bomb: 'malicious_code',\n crypto_miner: 'malicious_code',\n persistence: 'malicious_code',\n shell_injection: 'malicious_code',\n // Harmful intent\n harmful_intent: 'harmful_intent',\n dangerous_request: 'harmful_intent',\n // Social engineering\n social_engineering: 'social_engineering',\n emotional: 'social_engineering',\n authority_claim: 'social_engineering',\n // Privilege escalation\n privilege_escalation: 'privilege_escalation',\n // Destructive operation\n destructive_operation: 'destructive_operation',\n // L9 multi-turn correlation — the pseudo-category emitted when any\n // multi_turn_* pattern fires (crescendo, blocked_retry, trust-building,\n // memory_poisoning, etc.). The normalizeThreatType prefix check below\n // handles the specific pattern names; this entry catches the pseudo\n // value if the backend pre-normalizes.\n multi_turn_attack: 'multi_turn_attack',\n // Errors\n scan_error: 'scan_error',\n size_limit_exceeded: 'size_limit_exceeded',\n size_limit: 'size_limit_exceeded',\n timeout: 'scan_error',\n};\n\n/**\n * User-friendly guidance (matches MCP THREAT_GUIDANCE).\n */\nconst THREAT_GUIDANCE: Record<string, string> = {\n prompt_injection:\n 'This prompt contains patterns consistent with instruction override attempts.',\n jailbreak:\n 'This prompt attempts to bypass safety guidelines. The request has been blocked.',\n system_prompt_leak:\n 'The response contains system prompt disclosure. The response has been blocked.',\n data_exfiltration:\n 'This prompt may attempt to extract sensitive information.',\n sql_injection: 'This query contains potentially dangerous SQL patterns.',\n path_traversal:\n 'This file path attempts to access directories outside the allowed scope.',\n secrets_exposure:\n 'This content contains patterns matching API keys, tokens, or credentials.',\n pii_exposure: 'This content contains personally identifiable information.',\n blocked_domain: 'This web search targets a restricted domain.',\n toxic_content:\n 'This content contains potentially harmful or inappropriate language.',\n toxicity:\n 'This content contains potentially harmful or inappropriate language.',\n multi_turn_attack:\n 'A pattern was detected across multiple turns of this session that suggests a coordinated attempt to bypass safety controls.',\n malicious_code:\n 'This content contains patterns associated with malicious code.',\n harmful_intent:\n 'This request contains content associated with harmful intent.',\n social_engineering:\n 'This prompt contains social engineering patterns.',\n privilege_escalation:\n 'This query attempts to escalate privileges or gain unauthorized access.',\n destructive_operation:\n 'This query contains destructive operations. Review carefully.',\n scan_error:\n 'The security scan could not be completed. Blocked as precaution.',\n size_limit_exceeded: 'The content exceeds the maximum allowed size.',\n unknown: 'A security concern was detected. Please review the content.',\n};\n\n/**\n * Default severity per normalized threat type (matches MCP severity semantics).\n * critical > high > medium > low\n */\nconst THREAT_SEVERITY: Record<string, string> = {\n prompt_injection: 'high',\n jailbreak: 'high',\n system_prompt_leak: 'high',\n data_exfiltration: 'high',\n sql_injection: 'critical',\n path_traversal: 'high',\n secrets_exposure: 'critical',\n pii_exposure: 'high',\n blocked_domain: 'medium',\n toxic_content: 'medium',\n toxicity: 'medium',\n multi_turn_attack: 'high',\n malicious_code: 'critical',\n harmful_intent: 'high',\n social_engineering: 'medium',\n privilege_escalation: 'critical',\n destructive_operation: 'critical',\n scan_error: 'medium',\n size_limit_exceeded: 'low',\n unknown: 'medium',\n};\n\n/**\n * Fields that expose internal detection methodology - must be stripped.\n */\nconst INTERNAL_FIELDS = new Set([\n 'detected_by',\n 'policy_id',\n 'policy_name',\n 'matched_pattern',\n 'matched_text',\n 'pattern',\n 'scan_stage',\n 'ai_reasoning',\n 'llm_analysis',\n 'performance_metrics',\n 'performance',\n]);\n\n/**\n * Normalize internal threat type to standard enum.\n */\nexport function normalizeThreatType(rawType?: string): string {\n if (!rawType) {\n return 'unknown';\n }\n const normalized = rawType.toLowerCase().replace(/-/g, '_');\n if (THREAT_TYPE_MAP[normalized]) {\n return THREAT_TYPE_MAP[normalized];\n }\n // L9 multi-turn correlation patterns flow as `multi_turn_<pattern>`\n // (crescendo, blocked_retry, topic_pivot, threat_diversity, safe_then_unsafe,\n // tool_sequence_anomaly, coded_language_setup, context_overflow,\n // memory_poisoning, velocity_burst). Backend collapses these to\n // multi_turn_attack — mirror the prefix logic from\n // platform/common/models/response.go:380.\n if (normalized.startsWith('multi_turn_')) {\n return 'multi_turn_attack';\n }\n return 'unknown';\n}\n\n/**\n * Derive severity from threat type or pass through raw severity.\n *\n * If the backend provides a severity, validate and use it.\n * Otherwise, derive from the normalized threat type.\n */\nexport function deriveSeverity(\n threatType: string,\n rawSeverity?: string\n): string {\n const validSeverities = new Set(['critical', 'high', 'medium', 'low']);\n if (rawSeverity && validSeverities.has(rawSeverity.toLowerCase())) {\n return rawSeverity.toLowerCase();\n }\n return THREAT_SEVERITY[threatType] || 'medium';\n}\n\n/**\n * Convert raw confidence score to bucketed level.\n *\n * Protects IP by not exposing exact detection thresholds.\n */\nexport function bucketConfidence(score?: number): string {\n if (score === undefined || score === null) {\n return 'medium';\n }\n if (score >= 0.9) {\n return 'high';\n }\n if (score >= 0.7) {\n return 'medium';\n }\n return 'low';\n}\n\n/**\n * Governance-outcome fields the sanitizer preserves verbatim on BOTH safe\n * and unsafe branches. These are OUTCOME state (customer sees outcomes;\n * timings/attribution stay provider-only) — the contract-symmetry surface\n * (safe/refuse_tier/recovery/session_state on every response).\n */\nconst PRESERVED_GOVERNANCE_FIELDS = [\n 'action',\n 'refuse_tier',\n 'recovery',\n 'session_state',\n 'content_type',\n // Provenance: human_prompt / agent_output / agent_action / third_party.\n // An OUTCOME field, not detection attribution — it says who is answerable\n // for the scanned content, which the caller needs in order to know who a\n // refusal is even addressed to. It shipped on the backend in 2cfaa5f and was\n // silently dropped here, because this is an allow-list and nobody added it:\n // a new backend field reaches no caller until it appears in this array.\n 'content_origin',\n // The persisted record of this scan, which reportOutcome names. An\n // identifier, not attribution.\n 'scan_id',\n 'approval_info',\n 'client_session_rotation',\n] as const;\n\n/**\n * Sanitize one entry from the backend violations[] array.\n * Preserves customer-visible outcome fields (severity, action, threat_type,\n * owasp_category, user_message, suggested_action); drops attribution\n * fields (policy_id, policy_name, matched_pattern, ai_reasoning, etc.).\n */\nexport function sanitizeViolation(raw: unknown): ScanViolation | null {\n if (!raw || typeof raw !== 'object') {\n return null;\n }\n const source = raw as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(source)) {\n if (!INTERNAL_FIELDS.has(k)) {\n out[k] = v;\n }\n }\n return out as ScanViolation;\n}\n\n/**\n * Sanitize a raw backend scan response for IP protection.\n *\n * Strips internal detection attribution (layer timings, per-detector\n * confidences, pattern names, policy IDs) — NOT outcome state.\n * Preserves the four-state governance surface (`action`, `refuse_tier`,\n * `recovery`, `session_state`) so callers can distinguish\n * allow/warn/require_approval/block. Contract symmetry — safe and refuse\n * verdicts carry the same governance fields.\n */\nexport function sanitizeScanResponse(raw: ScanResult): ScanResult {\n const safe = raw.safe !== false;\n const result: ScanResult = { safe };\n\n // Governance-outcome fields pass through on BOTH branches. This is the\n // symmetric contract — a `warn` verdict (safe=true, action=\"warn\") must\n // carry recovery/refuse_tier the same as a `block` (safe=false).\n for (const field of PRESERVED_GOVERNANCE_FIELDS) {\n const value = (raw as Record<string, unknown>)[field];\n if (value !== undefined && value !== null) {\n (result as Record<string, unknown>)[field] = value;\n }\n }\n\n // violations[] is a mixed shape — pass each entry through per-item\n // sanitization to drop policy_id / policy_name / matched_pattern while\n // keeping customer-visible severity, threat_type, owasp_category,\n // user_message, suggested_action. Empty array is dropped (noise).\n const rawViolations = raw.violations;\n if (Array.isArray(rawViolations) && rawViolations.length > 0) {\n const cleaned = rawViolations\n .map((v) => sanitizeViolation(v))\n .filter((v): v is ScanViolation => v !== null);\n if (cleaned.length > 0) {\n result.violations = cleaned;\n }\n }\n\n if (safe) {\n // Safe branch: reason may carry advisory copy for warn tier.\n result.reason = raw.reason || '';\n return result;\n }\n\n // Unsafe branch: derive threat classification for legacy callers that\n // read top-level threat_type/severity/guidance. Backend /api/scan/enforce\n // returns top-level threat_type=null and puts detail inside violations[];\n // for those responses, prefer the first violation's threat_type when\n // the raw top-level field is missing.\n let rawThreatType = raw.threat_type;\n if (!rawThreatType && Array.isArray(rawViolations) && rawViolations.length > 0) {\n const first = rawViolations[0];\n if (first && typeof first === 'object' && !Array.isArray(first)) {\n const firstThreat = (first as Record<string, unknown>).threat_type;\n if (typeof firstThreat === 'string') {\n rawThreatType = firstThreat;\n }\n }\n }\n\n const threatType = normalizeThreatType(rawThreatType);\n const confidence = bucketConfidence(\n typeof raw.confidence === 'number' ? raw.confidence : undefined\n );\n const severity = deriveSeverity(\n threatType,\n typeof raw.severity === 'string' ? raw.severity : undefined\n );\n const guidance =\n THREAT_GUIDANCE[threatType] || THREAT_GUIDANCE['unknown'];\n\n result.threat_type = threatType;\n result.severity = severity;\n result.confidence = confidence;\n result.reason = raw.reason || guidance;\n result.guidance = guidance;\n return result;\n}\n\nexport { THREAT_TYPE_MAP, THREAT_GUIDANCE, THREAT_SEVERITY, INTERNAL_FIELDS };\n","{\n \"name\": \"shrike-guard\",\n \"version\": \"4.3.0\",\n \"description\": \"Shrike SDK for AI agent action governance \\u2014 drop-in wrapper for OpenAI, Anthropic, and Gemini that governs every tool call, prompt, and response server-side. Allow / approve / block. Free tier available.\",\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./openai\": {\n \"types\": \"./dist/openai/index.d.ts\",\n \"import\": \"./dist/openai/index.mjs\",\n \"require\": \"./dist/openai/index.js\"\n },\n \"./anthropic\": {\n \"types\": \"./dist/anthropic/index.d.ts\",\n \"import\": \"./dist/anthropic/index.mjs\",\n \"require\": \"./dist/anthropic/index.js\"\n },\n \"./gemini\": {\n \"types\": \"./dist/gemini/index.d.ts\",\n \"import\": \"./dist/gemini/index.mjs\",\n \"require\": \"./dist/gemini/index.js\"\n },\n \"./api\": {\n \"types\": \"./dist/api/index.d.ts\",\n \"import\": \"./dist/api/index.mjs\",\n \"require\": \"./dist/api/index.js\"\n },\n \"./claude-agent\": {\n \"types\": \"./dist/claude-agent/index.d.ts\",\n \"import\": \"./dist/claude-agent/index.mjs\",\n \"require\": \"./dist/claude-agent/index.js\"\n },\n \"./openai-agents\": {\n \"types\": \"./dist/openai-agents/index.d.ts\",\n \"import\": \"./dist/openai-agents/index.mjs\",\n \"require\": \"./dist/openai-agents/index.js\"\n },\n \"./ai\": {\n \"types\": \"./dist/ai/index.d.ts\",\n \"import\": \"./dist/ai/index.mjs\",\n \"require\": \"./dist/ai/index.js\"\n },\n \"./langchain\": {\n \"types\": \"./dist/langchain/index.d.ts\",\n \"import\": \"./dist/langchain/index.mjs\",\n \"require\": \"./dist/langchain/index.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"test\": \"jest\",\n \"test:coverage\": \"jest --coverage\",\n \"lint\": \"eslint src --ext .ts\",\n \"typecheck\": \"tsc --noEmit\",\n \"prepublishOnly\": \"npm run build\"\n },\n \"keywords\": [\n \"action-governance\",\n \"ai-agents\",\n \"agent-security\",\n \"agent-governance\",\n \"tool-call-gating\",\n \"ai-governance\",\n \"openai\",\n \"anthropic\",\n \"claude\",\n \"gemini\",\n \"llm\",\n \"prompt-injection\",\n \"pii\",\n \"jailbreak\",\n \"ai-safety\",\n \"claude-agent-sdk\",\n \"openai-agents\",\n \"vercel-ai-sdk\",\n \"langchain\",\n \"langgraph\",\n \"agent-framework\"\n ],\n \"author\": \"Shrike Security <security@shrikesecurity.com>\",\n \"license\": \"Apache-2.0\",\n \"homepage\": \"https://shrikesecurity.com\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Shrike-Security/shrike-guard-js\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/Shrike-Security/shrike-guard-js/issues\"\n },\n \"peerDependencies\": {\n \"@anthropic-ai/sdk\": \">=0.18.0\",\n \"@google/genai\": \">=1.0.0\",\n \"openai\": \">=4.0.0\",\n \"@anthropic-ai/claude-agent-sdk\": \">=0.1.0\",\n \"@openai/agents\": \">=0.1.0\",\n \"ai\": \">=5.0.0\",\n \"langchain\": \">=1.0.0\",\n \"@langchain/core\": \">=1.0.0\",\n \"zod\": \">=3.23.0\"\n },\n \"peerDependenciesMeta\": {\n \"openai\": {\n \"optional\": true\n },\n \"@anthropic-ai/sdk\": {\n \"optional\": true\n },\n \"@google/genai\": {\n \"optional\": true\n },\n \"@anthropic-ai/claude-agent-sdk\": {\n \"optional\": true\n },\n \"@openai/agents\": {\n \"optional\": true\n },\n \"ai\": {\n \"optional\": true\n },\n \"langchain\": {\n \"optional\": true\n },\n \"@langchain/core\": {\n \"optional\": true\n },\n \"zod\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@anthropic-ai/sdk\": \"^0.126.0\",\n \"@google/genai\": \"^2.12.0\",\n \"@types/jest\": \"^29.5.12\",\n \"@types/node\": \"^20.11.0\",\n \"jest\": \"^29.7.0\",\n \"openai\": \"^7.15.0\",\n \"ts-jest\": \"^29.1.2\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^5.3.3\",\n \"@anthropic-ai/claude-agent-sdk\": \"^0.3.273\",\n \"@openai/agents\": \"^0.18.0\",\n \"ai\": \"^7.0.102\",\n \"langchain\": \"^1.5.11\",\n \"@langchain/core\": \"^1.1.0\",\n \"zod\": \"^4.0.0\"\n },\n \"engines\": {\n \"node\": \">=18.0.0\"\n }\n}\n","// SDK version — derived from package.json at build time (tsup/esbuild inlines\n// the JSON literal, resolveJsonModule is on), so it can never drift from the\n// published version. It was previously a hardcoded '1.5.0' that fell out of\n// sync with package.json (4.0.2) and mis-stamped X-Shrike-SDK-Version on every\n// scan. Guarded by tests/unit/version.test.ts. Mirrors the Python SDK, which\n// derives __version__ from installed distribution metadata.\nimport { version } from '../package.json';\n\nexport const VERSION: string = version;\n","/**\n * HTTP client for the Shrike scan API.\n * Includes retry with exponential backoff, session correlation,\n * approval workflow, 401 key refresh, and rate limiting.\n */\n\nimport {\n DEFAULT_ENDPOINT,\n DEFAULT_SCAN_TIMEOUT,\n DEFAULT_RATE_LIMIT_PER_MINUTE,\n SDK_NAME,\n RETRY_CONFIG,\n getSessionId,\n getAgentId,\n warnOnceAboutTheProcessSession,\n} from './config';\nimport type { ApprovalInfo } from './config';\nimport { AUTO_CHUNK_THRESHOLD, aggregateChunkResults, chunkContent } from './chunker';\nimport { ShrikeRateLimitError } from './errors';\nimport { RateLimiter } from './rateLimiter';\nimport { sanitizeScanResponse } from './sanitizer';\nimport type { ActionOutcome, HostOutcomeReport } from './govern';\nimport { VERSION } from './version';\n\n/**\n * Phase 8b: Client-side size limits to fail fast before network round-trip.\n * These limits match the backend limits for consistency.\n */\nconst MAX_CONTENT_SIZE = 100 * 1024; // 100KB - matches backend MaxRequestBodySize\n\n/**\n * Result from a scan operation.\n */\n/**\n * Correlation pattern from L9 session-aware correlation engine.\n */\nexport interface CorrelationPattern {\n pattern_id: string;\n pattern_name: string;\n category: string;\n confidence: number;\n description: string;\n}\n\n/**\n * Governance action per Cooperative Governance 4-state contract (allow / warn / require_approval / block).\n * The `action` field is the server-authoritative signal for proceed-vs-refuse;\n * `isBlocked()` in this module consumes it directly.\n */\nexport type ScanAction = 'allow' | 'warn' | 'require_approval' | 'block';\n\n/**\n * Recovery guidance emitted by the backend on refuse verdicts.\n * `available_tools` names the MCP tools the caller can still invoke while\n * under quarantine (typically read-only surfaces like session_status).\n */\nexport interface ScanRecovery {\n instruction?: string;\n available_tools?: string[];\n patterns_triggered?: string[];\n [key: string]: unknown;\n}\n\n/**\n * Session-level state carried on every scan response (contract symmetry).\n * Fields are outcome state per the dashboard IP boundary — customer-visible.\n */\nexport interface ScanSessionState {\n session_risk_score?: number;\n session_turn_number?: number;\n session_patterns?: string[];\n session_locked?: boolean;\n [key: string]: unknown;\n}\n\n/**\n * A single policy violation as emitted by the /api/scan/enforce endpoint.\n * The sanitizer strips policy_id / policy_name / matched_pattern / other\n * attribution before this reaches callers; what remains here is the\n * customer-visible outcome surface.\n */\nexport interface ScanViolation {\n threat_type?: string;\n owasp_category?: string;\n severity?: string;\n action?: string;\n user_message?: string;\n suggested_action?: string;\n [key: string]: unknown;\n}\n\n/**\n * Provenance of scanned content. See {@link ScanResult.content_origin}.\n */\nexport type ContentOrigin =\n | 'human_prompt'\n | 'agent_output'\n | 'agent_action'\n | 'third_party';\n\n/**\n * True when the operator is answerable for this content — i.e. a person\n * typed it. Everything else was produced by the agent or arrived from\n * outside, and a refusal on it is not something the operator did.\n *\n * Use it to decide who a refusal message is addressed to: telling a user\n * \"your request was blocked\" when the agent poisoned its own context is\n * both wrong and unhelpful.\n */\nexport function attributableToOperator(origin?: ContentOrigin): boolean {\n return origin === 'human_prompt';\n}\n\nexport interface ScanResult {\n safe: boolean;\n reason?: string;\n threat_type?: string;\n severity?: string;\n confidence?: number | string;\n violations?: ScanViolation[];\n guidance?: string;\n approval_info?: ApprovalInfo;\n /** Cooperative Governance action — server-authoritative proceed/refuse signal */\n action?: ScanAction;\n /** Refuse tier: allow / warn / require_approval / block */\n refuse_tier?: ScanAction;\n /**\n * The persisted record of this scan, so the integration can report what\n * became of the action with `reportOutcome`. Absent when the backend kept\n * no row (anonymous, degraded).\n */\n scan_id?: string;\n /** Recovery guidance, present on refuse verdicts */\n recovery?: ScanRecovery;\n /** L9 session correlation state — outcome, customer-visible */\n session_state?: ScanSessionState;\n /** Specialized scan input type, e.g. 'sql' / 'file_path' / 'a2a_message' */\n content_type?: string;\n /**\n * Where the scanned content came from — who is answerable for it.\n *\n * - `human_prompt` the operator typed it\n * - `agent_output` the model generated it\n * - `agent_action` the agent is about to do it (every act-plane surface)\n * - `third_party` it arrived from outside: a tool result, a retrieved\n * document, a peer agent\n *\n * This is the field that answers \"was that my prompt, or the agent acting\n * on its own?\" — the question you cannot reconstruct after the fact from a\n * verdict alone. Unknown content types resolve to `agent_action`, never to\n * `human_prompt`: attributing an unattributable action to the operator is\n * the one error that is never safe to make.\n */\n content_origin?: ContentOrigin;\n /**\n * True when the agent's declared scope held this action (expired, tool\n * outside the scope, or action ceiling reached). The hold never skips the\n * content scan: a content block wins and stands as the verdict; otherwise\n * the verdict is require_approval and content_verdict carries what the\n * content scan said.\n */\n held_by_scope?: boolean;\n /** The content scan's own answer on a held action; absent when the content was blocked */\n content_verdict?: {\n safe: boolean;\n refuse_tier: ScanAction;\n threat_type?: string;\n severity?: string;\n };\n /** Client-side session rotation guidance */\n client_session_rotation?: unknown;\n /** L9 session risk score (0.0-1.0), present when session correlation is active */\n session_risk_score?: number;\n /** L9 matched correlation patterns, present when multi-turn patterns are detected */\n correlation_patterns?: CorrelationPattern[];\n /**\n * True when this is a fail-open verdict returned without a completed scan —\n * i.e. `failMode: 'open'` was set and the backend was unreachable, so\n * enforcement was skipped. Lets a caller distinguish \"scanned and clean\"\n * from \"not scanned, allowed anyway.\"\n */\n degraded?: boolean;\n [key: string]: unknown;\n}\n\n/**\n * Build a fail-open ALLOW verdict. Returned only when `failMode === 'open'` is\n * explicitly set and the scan could not complete (timeout, backend error).\n * Marked `degraded: true` — centralize all fail-open returns through this so the\n * marker can never be forgotten on a new wrapper.\n */\nexport function failOpenResult(reason: string): ScanResult {\n return { safe: true, reason, degraded: true };\n}\n\n/**\n * Result of ScanClient.declareScope — the persisted scope row plus derived\n * active_until + expired fields. Mirrors the shape returned by\n * POST /api/v1/agent/scope/declare on the backend.\n */\nexport interface DeclareScopeResult {\n scope_id?: string;\n agent_id?: string;\n purpose?: string;\n allowed_tools?: string[];\n forbidden_tools?: string[];\n max_duration_seconds?: number;\n expires_at?: string;\n active_until?: string;\n expired?: boolean;\n /** Renewal window: until when this key may refresh the scope itself.\n Absent means no ceiling; ceiling_reached means only an operator can now. */\n renewable_seconds?: number;\n renewable_until?: string;\n ceiling_reached?: boolean;\n created_at?: string;\n updated_at?: string;\n}\n\n/**\n * Options for the ScanClient.\n */\nexport interface ScanClientOptions {\n /** Shrike API key for authentication */\n apiKey: string;\n /** Shrike API endpoint URL */\n endpoint?: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n /** Rate limit in requests per minute (default: 100) */\n rateLimitPerMinute?: number;\n /** Optional callback to refresh API key on 401. Return new key or null. */\n onKeyRefresh?: () => Promise<string | null>;\n /**\n * The session this client scans under. Session identity is the key the\n * backend accumulates multi-turn risk against, so it should mean one unit\n * of work: one agent run, one conversation, one user's request. Defaults to\n * a process-wide id, which suits a CLI or a worker but not a server serving\n * many end users, where each user needs its own session. Prefer\n * `client.forSession(id)` per request.\n */\n sessionId?: string;\n /**\n * The agent this client scans as. Defaults to the process-wide id\n * (`SHRIKE_AGENT_ID` when set). Set it when one process drives several\n * distinct agents, so scope enforcement and agent attribution land on the\n * right one.\n */\n agentId?: string;\n}\n\n/**\n * Generate a UUID v4 string.\n */\nfunction generateUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Generate headers for scan API requests.\n */\nexport function getScanHeaders(\n shrikeApiKey: string,\n requestId?: string\n): Record<string, string> {\n return {\n Authorization: `Bearer ${shrikeApiKey}`,\n 'Content-Type': 'application/json',\n 'X-Shrike-SDK': SDK_NAME,\n 'X-Shrike-SDK-Version': VERSION,\n 'X-Shrike-Request-ID': requestId || generateUUID(),\n };\n}\n\n/** Check if an error is transient and retryable. */\nfunction isRetryableError(error: unknown): boolean {\n if (error instanceof Error) {\n const cause = 'cause' in error ? String((error as { cause?: unknown }).cause) : '';\n const errorStr = `${error.name} ${error.message} ${cause}`;\n return RETRY_CONFIG.retryableErrors.some((e) => errorStr.includes(e));\n }\n return false;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Fetch with retry for cold-start and transient failure resilience.\n * Matches MCP server's fetchWithRetry pattern.\n */\nasync function fetchWithRetry(\n url: string,\n options: RequestInit,\n timeoutMs: number,\n apiKey: string,\n onKeyRefresh?: () => Promise<string | null>\n): Promise<Response> {\n let lastError: Error | null = null;\n let delay = RETRY_CONFIG.initialDelayMs;\n\n for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n ...options,\n signal: controller.signal,\n });\n clearTimeout(timeoutId);\n\n // On 401, try key refresh and retry once\n if (response.status === 401 && onKeyRefresh && attempt === 0) {\n const newKey = await onKeyRefresh();\n if (newKey && newKey !== apiKey) {\n const retryController = new AbortController();\n const retryTimeout = setTimeout(() => retryController.abort(), timeoutMs);\n try {\n const headers = {\n ...(options.headers as Record<string, string>),\n Authorization: `Bearer ${newKey}`,\n };\n const retryResponse = await fetch(url, {\n ...options,\n headers,\n signal: retryController.signal,\n });\n clearTimeout(retryTimeout);\n return retryResponse;\n } catch {\n clearTimeout(retryTimeout);\n // Fall through to return original 401\n }\n }\n }\n\n return response;\n } catch (error) {\n clearTimeout(timeoutId);\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry on abort (timeout) or non-retryable errors\n if (lastError.name === 'AbortError' || !isRetryableError(error)) {\n throw lastError;\n }\n\n if (attempt < RETRY_CONFIG.maxRetries) {\n await sleep(delay);\n delay = Math.min(delay * RETRY_CONFIG.backoffMultiplier, RETRY_CONFIG.maxDelayMs);\n }\n }\n }\n\n throw lastError || new Error('Fetch failed after retries');\n}\n\n/**\n * Build the session context object for backend requests.\n *\n * `sessionId` / `agentId` override the process-wide defaults. When no session\n * id is supplied we warn once — see warnOnceAboutTheProcessSession for why the\n * default stays rather than being removed.\n */\nfunction buildSessionContext(\n extraContext?: Record<string, unknown>,\n sessionId?: string,\n agentId?: string\n): Record<string, unknown> {\n if (!sessionId) warnOnceAboutTheProcessSession();\n\n return {\n ...extraContext,\n session_id: sessionId || getSessionId(),\n agent_id: agentId || getAgentId(),\n source_application: 'shrike-guard-ts',\n };\n}\n\n/**\n * HTTP client for the Shrike scan API.\n */\n/**\n * Decide whether a scan verdict should be enforced as a block.\n *\n * Prefers the server-authoritative `action` field emitted by\n * /api/scan/enforce. Falls back to the legacy\n * `safe` boolean for verdicts that predate the enforce endpoint (older\n * backends, circuit-breaker fail-open synthetic verdicts, cached responses).\n *\n * Behavior:\n * - action = \"block\" → true\n * - action = \"require_approval\" → true (held actions are refused at the\n * tool-call boundary; caller can re-issue\n * after approval lands)\n * - action = \"allow\" → false\n * - action = \"warn\" → false (advisory; caller should surface\n * via block-feedback but not refuse)\n * - action absent → fall back to `!safe`\n *\n * This helper is the ONLY place the SDK converts a verdict into a\n * proceed-vs-refuse decision. Every LLM wrapper (OpenAI, Anthropic, Gemini)\n * routes through it so behavior stays uniform.\n *\n * Contract-symmetry pin: mirrors Python `_is_blocked` in shrike_guard.scanner.\n * If either SDK diverges, that is a bug in one of the two.\n */\nexport function isBlocked(verdict: ScanResult | Record<string, unknown>): boolean {\n const action = (verdict as Record<string, unknown>).action;\n if (typeof action === 'string' && action.length > 0) {\n // Known non-blocking tiers proceed.\n if (action === 'allow' || action === 'warn') return false;\n // Known blocking tiers refuse.\n if (action === 'block' || action === 'require_approval') return true;\n // Unknown/future tier (e.g. a new \"quarantine\" the backend adds): do NOT\n // fail open on an unrecognized name — fall through to `safe`. A new\n // blocking tier also sets safe:false, so this fails closed until the\n // client learns the tier explicitly. Forward-compat contract, see\n // tests/unit/contract-forward-compat.test.ts.\n }\n const safe = (verdict as Record<string, unknown>).safe;\n if (typeof safe === 'boolean') {\n return !safe;\n }\n return false;\n}\n\n/**\n * When running without an API key, appends a signup hint to scan results\n * so agents/users know they can upgrade from regex-only to full scanning.\n */\nexport function maybeAddSignupHint(result: ScanResult, apiKey: string): ScanResult {\n if (apiKey) return result;\n // Don't override if backend already provided upgrade_hint\n if ((result as any).upgrade_hint) return result;\n return {\n ...result,\n _note: 'Running without API key (L1-L5 only). Register free for cognitive threat detection: npx shrike-mcp --signup',\n };\n}\n\nexport class ScanClient {\n private apiKey: string;\n private readonly endpoint: string;\n private readonly timeout: number;\n private readonly rateLimiter: RateLimiter;\n private readonly onKeyRefresh?: () => Promise<string | null>;\n private readonly sessionId?: string;\n private readonly agentId?: string;\n\n constructor(options: ScanClientOptions) {\n this.apiKey = options.apiKey;\n this.endpoint = (options.endpoint || DEFAULT_ENDPOINT).replace(/\\/$/, '');\n this.timeout = options.timeout || DEFAULT_SCAN_TIMEOUT;\n this.rateLimiter = new RateLimiter(options.rateLimitPerMinute || DEFAULT_RATE_LIMIT_PER_MINUTE);\n this.onKeyRefresh = options.onKeyRefresh;\n this.sessionId = options.sessionId;\n this.agentId = options.agentId;\n\n if (!this.apiKey) {\n console.warn('[shrike-guard] No API key provided — running in free tier (regex-only).');\n console.warn('[shrike-guard] For full scanning (LLM analysis, session correlation): npx shrike-mcp --signup');\n }\n }\n\n /**\n * Return a view of this client that scans under `sessionId`.\n *\n * The returned client SHARES this one's rate limiter, so deriving one per\n * request is cheap and cannot multiply your rate budget. Build one ScanClient\n * at startup and derive a per-request view from it:\n *\n * ```ts\n * const guard = new ScanClient({ apiKey: KEY }); // once, at startup\n *\n * app.post('/act', async (req, res) => { // per request\n * const scoped = guard.forSession(req.session.id);\n * const verdict = await scoped.scanCommand(req.body.command);\n * });\n * ```\n *\n * Without this, every end user shares one session id and therefore one risk\n * score, and one user's refusal counts against the next user's action.\n */\n forSession(sessionId: string, agentId?: string): ScanClient {\n const view: ScanClient = Object.create(ScanClient.prototype);\n // Written through a mutable alias because the fields are readonly on the\n // class: this is a copy constructor, not a mutation of an existing client.\n const w = view as unknown as Record<string, unknown>;\n w.apiKey = this.apiKey;\n w.endpoint = this.endpoint;\n w.timeout = this.timeout;\n // Shared deliberately — a per-request client with its own limiter would\n // give every request a full rate budget and defeat the limit entirely.\n w.rateLimiter = this.rateLimiter;\n w.onKeyRefresh = this.onKeyRefresh;\n w.sessionId = sessionId;\n w.agentId = agentId !== undefined ? agentId : this.agentId;\n return view;\n }\n\n /** Session identity for this client, with any per-call extras merged. */\n private sessionContext(extraContext?: Record<string, unknown>): Record<string, unknown> {\n return buildSessionContext(extraContext, this.sessionId, this.agentId);\n }\n\n /** Check rate limit before each request. */\n private checkRateLimit(): void {\n const result = this.rateLimiter.consume();\n if (!result.allowed) {\n throw new ShrikeRateLimitError(result.retryAfterMs!);\n }\n }\n\n /**\n * Scan a prompt for security threats.\n *\n * @param prompt - The user prompt to scan.\n * @param context - Optional conversation context for better analysis.\n * @returns Scan result with 'safe' boolean and additional details.\n * @throws Error if the request fails or times out.\n */\n async scan(\n prompt: string,\n context?: string,\n options?: { plane?: string }\n ): Promise<ScanResult> {\n // Client-side size validation to fail fast\n const totalSize = prompt.length + (context?.length || 0);\n if (totalSize > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `Content too large (${Math.round(totalSize / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n // Auto-chunk large prompts. Below the threshold we keep the single-shot\n // path unchanged; above it we split on natural boundaries, scan each\n // chunk sequentially, and stop on the first block verdict. The\n // conversation_history context is NOT chunked — it rides on every scan\n // so downstream layers see the same session shape.\n if (prompt.length > AUTO_CHUNK_THRESHOLD) {\n return this.scanChunked(prompt, context, options);\n }\n\n this.checkRateLimit();\n\n const payload: Record<string, unknown> = {\n prompt,\n scan_type: 'full',\n context: this.sessionContext(options?.plane ? { plane: options.plane } : undefined),\n };\n if (context) {\n payload.conversation_history = context;\n }\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`Scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Chunk-and-scan path for prompts above AUTO_CHUNK_THRESHOLD. Splits on\n * natural boundaries, scans sequentially, aggregates. Stops on the first\n * block verdict (fail-fast) so a compromised chunk doesn't pay for the\n * remaining scans.\n *\n * Known limit: the same session id rides every chunk, so the session turn\n * count grows by the number of chunks.\n */\n private async scanChunked(\n prompt: string,\n context?: string,\n options?: { plane?: string }\n ): Promise<ScanResult> {\n const chunks = chunkContent(prompt);\n const results: ScanResult[] = [];\n for (const chunk of chunks) {\n const chunkResult = await this.scan(chunk, context, options);\n results.push(chunkResult);\n const action = chunkResult.action || chunkResult.refuse_tier;\n if (action === 'block') {\n break;\n }\n }\n return maybeAddSignupHint(aggregateChunkResults(results), this.apiKey);\n }\n\n /**\n * Scan a SQL query for injection attacks.\n *\n * @param query - The SQL query to scan.\n * @param database - Optional database name for context.\n * @param allowDestructive - If true, allows DROP/TRUNCATE operations.\n * @returns Scan result with 'safe' boolean and additional details.\n */\n async scanSql(\n query: string,\n database?: string,\n allowDestructive = false\n ): Promise<ScanResult> {\n if (query.length > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `SQL query too large (${Math.round(query.length / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Query exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const toolContext: Record<string, unknown> = {};\n if (database) {\n toolContext.database = database;\n }\n if (allowDestructive) {\n toolContext.allow_destructive = 'true';\n }\n\n const payload = {\n content: query,\n content_type: 'sql',\n context: this.sessionContext(toolContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`SQL scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan a file path for security risks.\n *\n * @param path - The file path to validate.\n * @param content - Optional file content to scan for secrets/PII.\n * @returns Scan result with 'safe' boolean and additional details.\n */\n async scanFile(path: string, content?: string): Promise<ScanResult> {\n const totalSize = path.length + (content?.length || 0);\n if (totalSize > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `File content too large (${Math.round(totalSize / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const contentType = content ? 'file_content' : 'file_path';\n const extraContext: Record<string, unknown> = {};\n if (content) {\n extraContext.file_content = content;\n }\n\n const payload = {\n content: path,\n content_type: contentType,\n context: this.sessionContext(extraContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`File scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan an A2A (Agent-to-Agent) protocol message for security threats.\n *\n * @param message - The A2A message text content to scan.\n * @param options - Optional context: sender/receiver agent IDs, task ID, role.\n * @returns Scan result with 'safe' boolean and additional details.\n */\n async scanA2AMessage(\n message: string,\n options?: {\n senderAgentId?: string;\n receiverAgentId?: string;\n taskId?: string;\n role?: 'user' | 'agent';\n }\n ): Promise<ScanResult> {\n if (message.length > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `Message too large (${Math.round(message.length / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const extraContext: Record<string, unknown> = {};\n if (options?.senderAgentId) extraContext.sender_agent_id = options.senderAgentId;\n if (options?.receiverAgentId) extraContext.receiver_agent_id = options.receiverAgentId;\n if (options?.taskId) extraContext.task_id = options.taskId;\n if (options?.role) extraContext.role = options.role;\n\n const payload = {\n content: message,\n content_type: 'a2a_message',\n context: this.sessionContext(extraContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`A2A message scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan an A2A AgentCard JSON for security threats.\n *\n * @param agentCard - The raw JSON string of the A2A AgentCard to scan.\n * @param verifySignature - Reserved for future JWS signature verification.\n * @returns Scan result with 'safe' boolean and additional details.\n */\n async scanAgentCard(\n agentCard: string,\n verifySignature = false\n ): Promise<ScanResult> {\n if (agentCard.length > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `Agent card too large (${Math.round(agentCard.length / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const extraContext: Record<string, unknown> = {};\n if (verifySignature) extraContext.verify_signature = 'true';\n\n const payload = {\n content: agentCard,\n content_type: 'agent_card',\n context: this.sessionContext(extraContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`Agent card scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan a shell command before executing it.\n *\n * The highest-volume act-plane surface, and the one the SDK could not\n * reach until 4.1.0: `shrike.scanCommand(cmd)` before you shell out is the\n * enforcement point. Catches destructive operations, fetch-and-execute\n * chains, reverse shells, credential reads, anti-forensics, and SQL\n * injection carried inside a database CLI argument (`psql -c \"...\"`),\n * which the command's own grammar cannot see.\n *\n * @param command - The command line about to be executed.\n * @param cwd - Optional working directory, for context.\n * @returns Scan result; check `safe` and `refuse_tier` before executing.\n */\n /**\n * Ask whether this agent may call a tool, without sending its arguments.\n *\n * For a tool this SDK has no reader for. The operator's declared scope\n * judges a tool by NAME, so a tool nobody can parse is still refused when\n * it is not on the allowlist, and still held when the scope has expired or\n * run out of actions.\n *\n * This answers one question. A permit says the agent was allowed to make\n * the call; it never says the arguments were inspected, because none were\n * sent. Where a tool maps to one of the scanned surfaces, scan that surface\n * instead and get both answers.\n *\n * @param toolName - The tool about to run.\n * @returns Scan result; check `safe` and `refuse_tier` before running.\n */\n async authorizeTool(toolName: string): Promise<ScanResult> {\n this.checkRateLimit();\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/authorize`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify({ tool_name: toolName, context: this.sessionContext() }),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n if (!response.ok) {\n throw new Error(`Tool authorization API returned error: ${response.status}`);\n }\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Report what became of a scanned action: executed, failed (with the\n * host's exit status when it has one) or skipped. Names the scan by the\n * `scan_id` the verdict carried. Identifiers and a status only, never\n * output or error text. Never throws: an unreported action reads as\n * unconfirmed, which is what it is.\n */\n async reportOutcome(scanId: string, outcome: ActionOutcome, options: { exitStatus?: number; source?: string } = {}): Promise<void> {\n if (!scanId) return;\n const body: Record<string, unknown> = { scan_id: scanId, outcome, ran_at: new Date().toISOString(), source: options.source || 'shrike-guard-js' };\n if (Number.isInteger(options.exitStatus)) body.exit_status = options.exitStatus;\n await this.postReport('/api/scan/outcome', body);\n }\n\n /**\n * Report the host's own decision about an action, under the host's name:\n * a framework guardrail that refused a call, a tool the host ran that\n * failed. Recorded beside Shrike's decisions, never as one of them.\n */\n async reportHostOutcome(report: HostOutcomeReport): Promise<void> {\n if (!report?.host) return;\n const body: Record<string, unknown> = { host: report.host, outcome: report.outcome };\n if (this.sessionId) body.session_id = this.sessionId;\n if (this.agentId) body.agent_id = this.agentId;\n if (report.tool) body.tool = report.tool;\n if (report.callId) body.call_id = report.callId;\n if (report.contentHash) body.content_hash = report.contentHash;\n if (report.reason) body.reason = report.reason;\n if (report.scanId) body.scan_id = report.scanId;\n await this.postReport('/api/scan/host-outcome', body);\n }\n\n private async postReport(path: string, body: Record<string, unknown>): Promise<void> {\n try {\n await fetchWithRetry(\n `${this.endpoint}${path}`,\n { method: 'POST', headers: getScanHeaders(this.apiKey), body: JSON.stringify(body) },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n } catch {\n // The record is best effort; the verdict already stood.\n }\n }\n\n async scanCommand(command: string, cwd?: string): Promise<ScanResult> {\n const toolContext: Record<string, unknown> = {};\n if (cwd) toolContext.cwd = cwd;\n return this.scanSpecialized(command, 'command', 'Command', toolContext);\n }\n\n /**\n * Scan a web search query before it reaches an external search engine.\n *\n * Catches PII and credentials leaving through a search box, credential\n * dorking, evasion tradecraft, illicit acquisition, and attack-tool\n * acquisition — while leaving ordinary defensive research alone.\n *\n * @param query - The search query about to be issued.\n * @returns Scan result; check `safe` before searching.\n */\n async scanWebSearch(query: string): Promise<ScanResult> {\n return this.scanSpecialized(query, 'web_search', 'Web search');\n }\n\n /**\n * Scan a single MCP tool definition before trusting or registering it.\n *\n * Detects tool poisoning: instructions hidden in a tool's own\n * `description`, which an agent reads as guidance and acts on without the\n * tool ever executing. Call this on every entry of a `tools/list` response\n * from a server you do not control.\n *\n * @param name - The tool name as advertised.\n * @param description - The tool description to screen.\n * @param inputSchema - Optional JSON schema; also screened.\n * @returns Scan result; do not register the tool when `safe` is false.\n */\n async scanMcpSchema(\n name: string,\n description: string,\n inputSchema?: Record<string, unknown>\n ): Promise<ScanResult> {\n this.checkRateLimit();\n\n const payload = {\n name,\n description,\n ...(inputSchema ? { input_schema: inputSchema } : {}),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/mcp_schema`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`MCP schema scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Scan retrieved context before feeding it to the model.\n *\n * RAG chunks are untrusted text from documents someone else wrote — the\n * standard carrier for indirect prompt injection. Scan them on the way in,\n * not after the model has already acted on them.\n *\n * @param chunks - The retrieved chunks, as text or an array of texts.\n * @param query - Optional user query the chunks were retrieved for.\n * @returns Scan result; check `safe` before including the context.\n */\n async scanRagContext(chunks: string | string[], query?: string): Promise<ScanResult> {\n const content = Array.isArray(chunks) ? JSON.stringify(chunks) : chunks;\n const toolContext: Record<string, unknown> = {};\n if (query) toolContext.query = query;\n return this.scanSpecialized(content, 'rag_context', 'RAG context', toolContext);\n }\n\n /**\n * Shared transport for the specialized (act-plane) surfaces.\n *\n * Every act-plane method above differs only in content type, the label used\n * in the error message, and any extra context. Before 4.1.0 each surface\n * hand-rolled this block, which is how the SDK ended up with five of the\n * eight channels missing: adding one meant copying forty lines. Now it\n * means one method.\n */\n private async scanSpecialized(\n content: string,\n contentType: string,\n label: string,\n toolContext: Record<string, unknown> = {}\n ): Promise<ScanResult> {\n if (content.length > MAX_CONTENT_SIZE) {\n return {\n safe: false,\n reason: `${label} content too large (${Math.round(content.length / 1024)}KB > ${MAX_CONTENT_SIZE / 1024}KB limit)`,\n threat_type: 'size_limit_exceeded',\n confidence: 1.0,\n violations: [\n {\n type: 'size_limit',\n description: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB`,\n },\n ],\n };\n }\n\n this.checkRateLimit();\n\n const payload = {\n content,\n content_type: contentType,\n context: this.sessionContext(toolContext),\n };\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/scan/enforce/specialized`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n throw new Error(`${label} scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this.apiKey);\n }\n\n /**\n * Declare (or refresh) the operating scope for a task-scoped agent.\n *\n * Once declared, every subsequent scan for the same agent_id is enforced\n * against the scope on the backend. Tool calls outside allowed_tools —\n * or explicitly on forbidden_tools — route to refuse_tier:\n * \"require_approval\" with threat_type: \"scope_violation\". Expired scopes\n * emit threat_type: \"scope_expired\". Absent a declaration, no scope\n * check runs; every existing SDK integration is unaffected until it\n * opts in.\n *\n * Wire contract mirrors ScanClient.declare_scope on the Python SDK and\n * scan_declare_scope on the shrike-mcp package — do not diverge without\n * updating those siblings.\n *\n * @param options.agentId - Agent identity this scope applies to.\n * @param options.allowedTools - Exact tool names permitted; [\"*\"] = any.\n * @param options.forbiddenTools - Optional. Wins over allowedTools.\n * @param options.purpose - Optional audit + dashboard label.\n * @param options.maxDurationSeconds - Optional TTL from the latest declaration.\n * @param options.expiresAt - Optional ISO-8601 absolute expiry.\n * @param options.renewableSeconds - Optional renewal window: how long, from\n * the operator's grant, this key may keep refreshing the scope. Honoured\n * on a first declaration; on a refresh the stored value always wins.\n * @returns The persisted scope row, including scope_id, active_until and\n * renewable_until.\n *\n * Refreshing: once the scope exists, calling again from the agent's own key\n * is a refresh. Any option left out is inherited from the scope on file, so\n * `{ agentId, maxDurationSeconds: 7200 }` is a complete refresh. A refresh\n * may narrow but never widen (403, reason \"widening\"), and stops working\n * once the operator's renewal window closes (403, reason \"ceiling_reached\").\n */\n async declareScope(options: {\n agentId: string;\n /** Required on a first declaration; omit on a refresh to inherit. */\n allowedTools?: string[];\n forbiddenTools?: string[];\n purpose?: string;\n maxDurationSeconds?: number;\n expiresAt?: string;\n renewableSeconds?: number;\n }): Promise<DeclareScopeResult> {\n this.checkRateLimit();\n\n const payload: Record<string, unknown> = {\n agent_id: options.agentId,\n };\n if (options.allowedTools !== undefined) payload.allowed_tools = options.allowedTools;\n if (options.purpose !== undefined) payload.purpose = options.purpose;\n if (options.forbiddenTools !== undefined) payload.forbidden_tools = options.forbiddenTools;\n if (options.maxDurationSeconds !== undefined) {\n payload.max_duration_seconds = options.maxDurationSeconds;\n }\n if (options.expiresAt !== undefined) payload.expires_at = options.expiresAt;\n if (options.renewableSeconds !== undefined) payload.renewable_seconds = options.renewableSeconds;\n\n const response = await fetchWithRetry(\n `${this.endpoint}/api/v1/agent/scope/declare`,\n {\n method: 'POST',\n headers: getScanHeaders(this.apiKey),\n body: JSON.stringify(payload),\n },\n this.timeout,\n this.apiKey,\n this.onKeyRefresh\n );\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new Error(\n `declareScope failed: ${response.status}${text ? ` — ${text.trim()}` : ''}`\n );\n }\n\n return (await response.json()) as DeclareScopeResult;\n }\n}\n","/**\n * ShrikeAnthropic - Drop-in replacement for Anthropic client with security scanning.\n */\n\nimport type Anthropic from '@anthropic-ai/sdk';\nimport type { MessageCreateParams, Message } from '@anthropic-ai/sdk/resources/messages';\n\nimport {\n FailMode,\n DEFAULT_ENDPOINT,\n DEFAULT_FAIL_MODE,\n DEFAULT_SCAN_TIMEOUT,\n getSessionId,\n getAgentId,\n} from '../config';\nimport { ShrikeBlockedError, ShrikeScanError } from '../errors';\nimport { sanitizeScanResponse } from '../sanitizer';\nimport { getScanHeaders, isBlocked, maybeAddSignupHint, ScanResult, failOpenResult } from '../scanner';\n\n/**\n * Simple logger for warnings.\n */\nfunction logWarning(message: string): void {\n console.warn(`⚠️ ${message}`);\n}\n\n/**\n * Content block type for Anthropic messages.\n */\ninterface ContentBlock {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\n/**\n * Anthropic message type.\n */\ninterface AnthropicMessage {\n role: string;\n content: string | ContentBlock[];\n}\n\n/**\n * Options for creating a ShrikeAnthropic client.\n */\nexport interface ShrikeAnthropicOptions {\n /** Anthropic API key. If not provided, uses ANTHROPIC_API_KEY env var. */\n apiKey?: string;\n /** Shrike API key for authentication with the scan service. */\n shrikeApiKey?: string;\n /** Shrike API endpoint URL. */\n shrikeEndpoint?: string;\n /** Behavior on scan failure - 'open' (allow) or 'closed' (block). */\n failMode?: FailMode | 'open' | 'closed';\n /** Timeout for scan requests in milliseconds. */\n scanTimeout?: number;\n /** Additional options passed to the Anthropic client. */\n anthropicOptions?: Record<string, unknown>;\n}\n\n/**\n * Drop-in replacement for Anthropic with Shrike security protection.\n *\n * This class wraps the official Anthropic client and automatically scans\n * all prompts before they are sent to Claude. If a prompt is detected\n * as unsafe, the request is blocked and a ShrikeBlockedError is raised.\n *\n * @example\n * ```typescript\n * import { ShrikeAnthropic } from 'shrike-guard/anthropic';\n *\n * const client = new ShrikeAnthropic({\n * apiKey: 'sk-ant-...',\n * shrikeApiKey: 'shrike-...',\n * });\n *\n * const message = await client.messages.create({\n * model: 'claude-3-opus-20240229',\n * max_tokens: 1024,\n * messages: [{ role: 'user', content: 'Hello!' }],\n * });\n * ```\n */\nexport class ShrikeAnthropic {\n private _anthropic: Anthropic;\n private _shrikeEndpoint: string;\n private _shrikeApiKey: string;\n private _failMode: FailMode;\n private _scanTimeout: number;\n\n /** Namespace for messages operations */\n public readonly messages: MessagesNamespace;\n\n constructor(options: ShrikeAnthropicOptions = {}) {\n // Dynamically import Anthropic to make it a peer dependency\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const AnthropicClient = require('@anthropic-ai/sdk').default;\n\n this._anthropic = new AnthropicClient({\n apiKey: options.apiKey,\n ...options.anthropicOptions,\n });\n this._shrikeEndpoint = (options.shrikeEndpoint || DEFAULT_ENDPOINT).replace(/\\/$/, '');\n this._shrikeApiKey = options.shrikeApiKey || '';\n this._failMode =\n typeof options.failMode === 'string'\n ? (options.failMode as FailMode)\n : options.failMode || DEFAULT_FAIL_MODE;\n this._scanTimeout = options.scanTimeout || DEFAULT_SCAN_TIMEOUT;\n\n // Note: Scan depth is set by the backend from the license tier (community = L1-L5 deterministic layers; Pro and above = full L1-L9 including LLM semantic, response intel and session correlation).\n // No local scanning - backend has full regex patterns (~50+) and normalizers\n\n if (!this._shrikeApiKey) {\n console.warn('[shrike-guard] No shrikeApiKey provided — running in free tier (regex-only).');\n console.warn('[shrike-guard] For full scanning (LLM analysis, session correlation): npx shrike-mcp --signup');\n }\n\n // Expose messages interface\n this.messages = new MessagesNamespace(this);\n }\n\n /**\n * Extract all user message content from a messages list.\n */\n _extractUserContent(messages: AnthropicMessage[]): string {\n const userContents: string[] = [];\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n if (typeof msg.content === 'string') {\n userContents.push(msg.content);\n } else if (Array.isArray(msg.content)) {\n for (const block of msg.content) {\n if (block.type === 'text' && block.text) {\n userContents.push(block.text);\n }\n }\n }\n }\n }\n\n return userContents.join('\\n');\n }\n\n /**\n * Scan user messages for security threats via backend API.\n *\n * Always calls backend - backend handles tier-based scanning:\n * - Community tier (no API key): L1-L5 (regex, unicode, malformed, encoding, token/semantic)\n * - Pro tier and above: L1-L9 (adds visual, LLM semantic, response intel, session correlation)\n */\n async _scanMessages(messages: AnthropicMessage[]): Promise<ScanResult> {\n const userContent = this._extractUserContent(messages);\n\n if (!userContent.trim()) {\n return { safe: true, reason: 'No user content to scan' };\n }\n\n // Always call backend API - tier detection happens server-side\n return this._remoteScan(userContent);\n }\n\n /**\n * Full scan via Shrike backend API.\n * Backend handles tier-based scanning automatically based on API key presence.\n */\n private async _remoteScan(prompt: string): Promise<ScanResult> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this._scanTimeout);\n\n try {\n const response = await fetch(`${this._shrikeEndpoint}/api/scan/enforce`, {\n method: 'POST',\n headers: getScanHeaders(this._shrikeApiKey),\n body: JSON.stringify({\n prompt,\n context: {\n session_id: getSessionId(),\n agent_id: getAgentId(),\n source_application: 'shrike-guard-ts',\n },\n }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n if (this._failMode === FailMode.OPEN) {\n return failOpenResult(`Scan API error: ${response.status}`);\n }\n throw new ShrikeScanError(`Scan API returned error: ${response.status}`);\n }\n\n return maybeAddSignupHint(sanitizeScanResponse((await response.json()) as ScanResult), this._shrikeApiKey);\n } catch (error) {\n if (error instanceof ShrikeScanError) {\n throw error;\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n if (errorMessage.includes('abort')) {\n if (this._failMode === FailMode.OPEN) {\n // No local fallback - just fail open\n logWarning('Scan request timed out, failing open (allowing request)');\n return failOpenResult('Scan timeout, failing open');\n }\n throw new ShrikeScanError(\"Scan request timed out and fail_mode is 'closed'\");\n }\n\n if (this._failMode === FailMode.OPEN) {\n return failOpenResult(`Scan error: ${errorMessage}`);\n }\n throw new ShrikeScanError(`Scan failed: ${errorMessage}`);\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /** Access the underlying Anthropic client */\n get anthropic(): Anthropic {\n return this._anthropic;\n }\n}\n\n/**\n * Namespace for messages operations.\n */\nclass MessagesNamespace {\n private _client: ShrikeAnthropic;\n\n constructor(client: ShrikeAnthropic) {\n this._client = client;\n }\n\n /**\n * Create a message with security scanning.\n *\n * This method scans the user messages before sending them to Anthropic.\n * If the scan detects a security threat, a ShrikeBlockedError is raised.\n */\n async create(params: MessageCreateParams): Promise<Message> {\n // 1. Scan messages for security threats\n const scanResult = await this._client._scanMessages(\n params.messages as unknown as AnthropicMessage[]\n );\n\n // 2. Block if unsafe\n if (isBlocked(scanResult)) {\n throw new ShrikeBlockedError(\n `Request blocked: ${scanResult.reason || 'Security threat detected'}`,\n scanResult.threat_type,\n scanResult.confidence,\n scanResult.violations || []\n );\n }\n\n // 3. Proxy to Anthropic (non-streaming)\n return this._client.anthropic.messages.create({\n ...params,\n stream: false,\n }) as unknown as Promise<Message>;\n }\n}\n"]}
|
package/dist/api/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { B as BaseClient, a as BaseClientOptions } from '../auth-DotJtJHE.mjs';
|
|
2
2
|
export { A as AuthClient, b as AuthResponse, L as LoginRequest, R as RegisterRequest, U as UserProfile } from '../auth-DotJtJHE.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { c as ScanResult } from '../govern-olQtsipX.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Agent API client for Shrike.
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { B as BaseClient, a as BaseClientOptions } from '../auth-DotJtJHE.js';
|
|
2
2
|
export { A as AuthClient, b as AuthResponse, L as LoginRequest, R as RegisterRequest, U as UserProfile } from '../auth-DotJtJHE.js';
|
|
3
|
-
import {
|
|
3
|
+
import { c as ScanResult } from '../govern-olQtsipX.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Agent API client for Shrike.
|