shrike-guard 1.3.3 → 4.0.2

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 ADDED
@@ -0,0 +1,70 @@
1
+ # Changelog
2
+
3
+ ## [4.0.2] - 2026-07-16
4
+
5
+ ### Security
6
+ - **Fail-open verdicts now carry `degraded: true` on every provider wrapper.** Previously the OpenAI, Anthropic, and Gemini wrappers returned a plain allow verdict when `failMode: 'open'` was set and the backend was unreachable, so a caller could not distinguish "scanned and clean" from "not scanned, enforcement skipped." All fail-open returns now route through a single `failOpenResult()` constructor, and `degraded?: boolean` is part of the exported `ScanResult` type. The default remains fail-**closed**; this affects only callers who explicitly opt into fail-open.
7
+
8
+ ### Fixed
9
+ - **Gemini wrapper now works against `@google/genai`.** It previously required `@google/genai` but instantiated the legacy `@google/generative-ai` class (`GoogleGenerativeAI`) and called `getGenerativeModel()`, so it threw at construction against the package it declared. Migrated to the current v2 SDK surface (`GoogleGenAI` + `ai.models.generateContent(...)` / `ai.chats.create(...)`); the public `getGenerativeModel().generateContent()` facade is unchanged. Dependency raised to `@google/genai >=1.0.0` (tested against 2.12). Added a real-package shape test so an SDK rename can't silently ship broken again.
10
+ - Corrected the `ShrikeScanError` docstring, which incorrectly described `fail_mode='open'` as the default. The default is `fail_mode='closed'` (secure by default across the 4.x line) — behavior unchanged; documentation-only. (Supersedes the unreleased 4.0.1.)
11
+
12
+ ## [4.0.0] - 2026-07-13
13
+
14
+ ### Version alignment (no breaking changes)
15
+ All Shrike client surfaces — MCP server (`shrike-mcp`), TypeScript SDK, and Python SDK — now share a single version line starting at 4.0.0. The major-version jump from 1.5.0 signals the alignment, not an API break: every 1.5.0 program runs unchanged on 4.0.0.
16
+
17
+ ### Added
18
+ - **Client-side auto-chunk for large inputs.** Prompts over 20KB are automatically split on natural boundaries (paragraph → line → hard cut) into ~8KB chunks and scanned sequentially with fail-fast on the first blocking verdict. Applies to `ScanClient.scan()`. New `chunker` module exports `AUTO_CHUNK_THRESHOLD`, `CHUNK_TARGET_SIZE`, `chunkContent()`, and `aggregateChunkResults()` for callers who want to drive chunking directly.
19
+ - **Aggregation contract:** worst-action wins across chunks; violations dedup by (threat_type, severity, action); `session_state` from the last scanned chunk; `recovery` from the first blocking chunk.
20
+
21
+ ### Why
22
+ Large single-shot scan inputs (agent transcripts, RAG context dumps, file contents) previously rode through the cascade as one oversized request — slower verdicts and more LLM-layer tokens burned per scan. Chunked inputs land in the backend cascade's small-content band, which allows earlier early-exit on clean content and cheaper verdicts on both sides of the wire. Fail-fast on the first blocked chunk means a threat at the top of a large document blocks without paying to scan the rest.
23
+
24
+ ## [1.5.0] - 2026-07-06
25
+
26
+ ### Added
27
+ - **Resilience primitives — `CircuitBreaker`, `CircuitOpenError`, `CircuitState`, `retryWithBackoff`** (mirrors Python SDK). Three-state breaker with configurable `failureThreshold` (5), `successThreshold` (2), `timeout` (30s), and `maxHalfOpenRequests` (3). Under a partial backend outage the breaker short-circuits repeated failures fast rather than letting `fail_mode: "closed"` produce a blocked-response storm. `retryWithBackoff` treats `CircuitOpenError` as non-retryable by default.
28
+ - **`AuthClient` re-exported at package root** (previously only reachable via `shrike-guard/api`). Matches the Python SDK's placement — `import { AuthClient } from 'shrike-guard'` now works.
29
+
30
+ ### Why
31
+ Cross-language SDK parity. The Python SDK ships `CircuitBreaker` + `retry_with_backoff`; this brings the TypeScript SDK to feature-equivalence so customers flipping between languages see identical failure semantics under real outage conditions.
32
+
33
+ ## [1.4.0] - 2026-07-02
34
+
35
+ ### Added
36
+ - **Client-side PII redaction.** `redactPII()`, `rehydratePII()`, `getRedactionSummary()`, `updatePIIPatterns()`, and `getPIIPatternCount()` exported from the root entry point. Detects and tokenizes 20+ PII types (SSN, credit card, email, phone, address, medical record, wallet, etc.) BEFORE the prompt leaves customer environment — Shrike backend never sees the raw PII.
37
+ - **`syncPIIPatterns({ endpoint, apiKey })`** — one-shot startup call fetches the canonical Presidio-derived pattern set from the Shrike backend so client-side detection stays uniform with the server-side scan. Fails safe: on any error (network / timeout / malformed) the bootstrap patterns stay in place; sync never blocks scans.
38
+ - **Backend-owned prefix contract.** The recognizer ships its client-side redaction tag (e.g. `[IP_1]`) as part of the pattern payload; the client uses it verbatim. When the backend omits the field (pre-2026-07-02 releases), the SDK derives the prefix from the threat_type. No pattern is ever silently dropped for an unmapped prefix. Adding a new backend pattern requires zero SDK changes.
39
+
40
+ ### Why
41
+ Client-side redaction removes PII before it crosses the network to Shrike's backend — defense-in-depth for HIPAA / PCI / GLBA / CMMC workloads on top of the BAA/DPA that already covers transmission. The `syncPIIPatterns` step keeps client patterns in step with backend patterns without shipping SDK updates every time Presidio adds a recognizer.
42
+
43
+ ### Fixed
44
+ - gemini-client test mock import matches the runtime `@google/genai` package (upstream renamed from `@google/generative-ai`).
45
+
46
+ ## [1.1.0] - 2026-02-19
47
+
48
+ ### Added
49
+ - "What Shrike Detects" section in README: 86+ rules across 6 compliance frameworks
50
+ - CHANGELOG.md
51
+
52
+ ### Changed
53
+ - Updated backend tier description: all tiers now get full 9-layer cascade (L1-L8)
54
+
55
+ ## [1.0.1] - 2026-01-20
56
+
57
+ ### Fixed
58
+ - Minor bug fixes and documentation updates
59
+
60
+ ## [1.0.0] - 2026-01-15
61
+
62
+ ### Added
63
+ - Initial release
64
+ - Drop-in OpenAI, Anthropic, and Gemini client wrappers
65
+ - Automatic prompt scanning via Shrike backend
66
+ - Fail-open and fail-closed modes
67
+ - Subpath imports (shrike-guard/openai, /anthropic, /gemini)
68
+ - SQL injection and file scanning
69
+ - Response sanitization (IP protection)
70
+ - API management clients (auth, policies, agents, sandbox)
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Node.js 18+](https://img.shields.io/badge/node-18+-green.svg)](https://nodejs.org/)
5
5
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
6
 
7
- **Shrike Guard** is a TypeScript SDK for the [Shrike](https://shrikesecurity.com) platform — AI governance for every AI interaction. It wraps OpenAI, Anthropic (Claude), and Google Gemini clients to automatically evaluate all prompts against policy before they reach the LLM. Whether you're governing a customer-facing chatbot, securing developer AI tools, or managing autonomous agent actions — the same multi-layered cognitive pipeline evaluates every interaction.
7
+ **Shrike Guard** is a TypeScript SDK for the [Shrike](https://shrikesecurity.com) platform — AI governance for every AI interaction. It wraps OpenAI, Anthropic (Claude), and Google Gemini clients to automatically evaluate all prompts against policy before they reach the LLM. Whether you're governing a customer-facing chatbot, securing developer AI tools, or managing autonomous agent actions — the same 9-layer cognitive pipeline evaluates every interaction.
8
8
 
9
9
  ## Features
10
10
 
@@ -15,14 +15,14 @@
15
15
  - Jailbreak attempts
16
16
  - SQL injection
17
17
  - Path traversal
18
- - **Fail-safe modes**: Choose between fail-open (default) or fail-closed behavior
18
+ - **Fail-safe modes**: Defaults to fail-closed (Zero Trust posture); opt into fail-open explicitly when availability outranks enforcement
19
19
  - **CJS + ESM**: Dual build via tsup, works everywhere
20
20
  - **Subpath imports**: `shrike-guard/openai`, `shrike-guard/anthropic`, `shrike-guard/gemini`
21
21
  - **Zero code changes**: Just replace your import
22
22
 
23
23
  ## What Shrike Detects
24
24
 
25
- Shrike's backend runs a multi-stage detection pipeline with security rules across **7 compliance frameworks**:
25
+ Shrike's 9-layer cognitive pipeline includes sensitive-data detection aligned to 5 major regulatory frameworks:
26
26
 
27
27
  | Framework | Coverage |
28
28
  |-----------|----------|
@@ -31,10 +31,8 @@ Shrike's backend runs a multi-stage detection pipeline with security rules acros
31
31
  | **ISO 27001** | Information security — passwords, tokens, certificates |
32
32
  | **SOC 2** | Secrets, credentials, API keys, cloud tokens |
33
33
  | **NIST** | AI risk management (IR 8596), cybersecurity framework (CSF 2.0) |
34
- | **PCI-DSS** | Cardholder data — PAN, CVV, expiry, track data |
35
- | **WebMCP** | MCP tool description injection, data exfiltration |
36
34
 
37
- Plus built-in detection for prompt injection, jailbreaks, social engineering, and dangerous requests.
35
+ Detection coverage is not a certification claim — see [shrikesecurity.com/compliance](https://shrikesecurity.com/compliance) for our current certification status. Plus built-in detection for prompt injection, jailbreaks, social engineering, and dangerous requests.
38
36
 
39
37
  ### Tiers
40
38
 
@@ -42,7 +40,7 @@ Detection depth depends on your tier. All tiers get the same SDK wrappers — ti
42
40
 
43
41
  | | Anonymous | Community | Pro | Enterprise |
44
42
  |---|---|---|---|---|
45
- | Detection Layers | L1-L5 | L1-L7 | L1-L8 | L1-L9 |
43
+ | Detection Layers | L1-L5 | L1-L7 | L1-L9 (full) | L1-L9 (full) |
46
44
  | API Key | Not needed | Free signup | Paid | Paid |
47
45
  | Rate Limit | — | 10/min | 100/min | 1,000/min |
48
46
  | Scans/month | — | 1,000 | 25,000 | 1,000,000 |
@@ -128,20 +126,22 @@ Choose how the SDK behaves when the security scan fails (timeout, network error,
128
126
  ```typescript
129
127
  import { ShrikeOpenAI } from 'shrike-guard/openai';
130
128
 
131
- // Fail-open (default): Allow requests if scan fails
132
- // Best for: Most applications where availability is important
129
+ // Fail-closed (default): Block requests if scan fails
130
+ // Best for: Production security workloads. If the Shrike backend is down,
131
+ // the SDK raises ShrikeScanError instead of allowing traffic through unguarded.
133
132
  const client = new ShrikeOpenAI({
134
133
  apiKey: 'sk-...',
135
134
  shrikeApiKey: 'shrike-...',
136
- failMode: 'open',
135
+ failMode: 'closed', // This is the default
137
136
  });
138
137
 
139
- // Fail-closed: Block requests if scan fails
140
- // Best for: Security-critical applications
141
- const strictClient = new ShrikeOpenAI({
138
+ // Fail-open: Allow requests if scan fails
139
+ // Best for: Non-production experiments, internal tools where availability must
140
+ // outrank enforcement. Trades the guard's enforcement promise for uptime.
141
+ const permissiveClient = new ShrikeOpenAI({
142
142
  apiKey: 'sk-...',
143
143
  shrikeApiKey: 'shrike-...',
144
- failMode: 'closed',
144
+ failMode: 'open',
145
145
  });
146
146
  ```
147
147
 
@@ -302,9 +302,9 @@ Looking for an AI security SDK? Here's how Shrike Guard compares:
302
302
  | Feature | Shrike Guard | Lakera | Prompt Armor |
303
303
  |---|---|---|---|
304
304
  | Drop-in OpenAI/Anthropic/Gemini wrapper | Yes | No | No |
305
- | Multi-layered evaluation pipeline | Yes | Limited | Limited |
305
+ | 9-layer cognitive pipeline | Yes | Limited | Limited |
306
306
  | PII detection + redaction | Yes | Partial | No |
307
- | Session correlation | Yes (Enterprise) | No | No |
307
+ | Session correlation | Yes (Pro+) | No | No |
308
308
  | Free tier (no API key) | Yes | No | No |
309
309
  | Open source client | Yes (Apache 2.0) | No | No |
310
310
 
@@ -1,6 +1,6 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { MessageCreateParams, Message } from '@anthropic-ai/sdk/resources/messages';
3
- import { F as FailMode, S as ScanResult } from '../scanner-CkCXFY3H.mjs';
3
+ import { F as FailMode, S as ScanResult } from '../scanner-cxA3LnQR.mjs';
4
4
 
5
5
  /**
6
6
  * ShrikeAnthropic - Drop-in replacement for Anthropic client with security scanning.
@@ -1,6 +1,6 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { MessageCreateParams, Message } from '@anthropic-ai/sdk/resources/messages';
3
- import { F as FailMode, S as ScanResult } from '../scanner-CkCXFY3H.js';
3
+ import { F as FailMode, S as ScanResult } from '../scanner-cxA3LnQR.js';
4
4
 
5
5
  /**
6
6
  * ShrikeAnthropic - Drop-in replacement for Anthropic client with security scanning.
@@ -74,9 +74,11 @@ var THREAT_TYPE_MAP = {
74
74
  completion_baiting: "jailbreak",
75
75
  override: "jailbreak",
76
76
  manipulate: "jailbreak",
77
- tonality_drift_profanity: "jailbreak",
77
+ // L8 tonality drift — backend normalizes profanity + hostile as toxic_content;
78
+ // casual stays as jailbreak (matches platform/common/models/response.go:309-313).
79
+ tonality_drift_profanity: "toxic_content",
80
+ tonality_drift_hostile: "toxic_content",
78
81
  tonality_drift_casual: "jailbreak",
79
- tonality_drift_hostile: "jailbreak",
80
82
  // System prompt leak
81
83
  system_prompt_leak: "system_prompt_leak",
82
84
  system_prompt_extraction: "system_prompt_leak",
@@ -132,9 +134,11 @@ var THREAT_TYPE_MAP = {
132
134
  suspicious_tld: "blocked_domain",
133
135
  suspicious_domain: "blocked_domain",
134
136
  malicious_url: "blocked_domain",
135
- // Toxicity
136
- toxicity: "toxicity",
137
- harmful_content: "toxicity",
137
+ // Toxic content (canonical name as of the L7 rewrite). `toxicity` kept as a
138
+ // legacy alias so older backend builds + customer code don't break.
139
+ toxic_content: "toxic_content",
140
+ toxicity: "toxic_content",
141
+ harmful_content: "toxic_content",
138
142
  // Malicious code
139
143
  malicious_content: "malicious_code",
140
144
  malicious_code: "malicious_code",
@@ -155,6 +159,12 @@ var THREAT_TYPE_MAP = {
155
159
  privilege_escalation: "privilege_escalation",
156
160
  // Destructive operation
157
161
  destructive_operation: "destructive_operation",
162
+ // L9 multi-turn correlation — the pseudo-category emitted when any
163
+ // multi_turn_* pattern fires (crescendo, blocked_retry, trust-building,
164
+ // memory_poisoning, etc.). The normalizeThreatType prefix check below
165
+ // handles the specific pattern names; this entry catches the pseudo
166
+ // value if the backend pre-normalizes.
167
+ multi_turn_attack: "multi_turn_attack",
158
168
  // Errors
159
169
  scan_error: "scan_error",
160
170
  size_limit_exceeded: "size_limit_exceeded",
@@ -171,7 +181,9 @@ var THREAT_GUIDANCE = {
171
181
  secrets_exposure: "This content contains patterns matching API keys, tokens, or credentials.",
172
182
  pii_exposure: "This content contains personally identifiable information.",
173
183
  blocked_domain: "This web search targets a restricted domain.",
184
+ toxic_content: "This content contains potentially harmful or inappropriate language.",
174
185
  toxicity: "This content contains potentially harmful or inappropriate language.",
186
+ multi_turn_attack: "A pattern was detected across multiple turns of this session that suggests a coordinated attempt to bypass safety controls.",
175
187
  malicious_code: "This content contains patterns associated with malicious code.",
176
188
  harmful_intent: "This request contains content associated with harmful intent.",
177
189
  social_engineering: "This prompt contains social engineering patterns.",
@@ -191,7 +203,9 @@ var THREAT_SEVERITY = {
191
203
  secrets_exposure: "critical",
192
204
  pii_exposure: "high",
193
205
  blocked_domain: "medium",
206
+ toxic_content: "medium",
194
207
  toxicity: "medium",
208
+ multi_turn_attack: "high",
195
209
  malicious_code: "critical",
196
210
  harmful_intent: "high",
197
211
  social_engineering: "medium",
@@ -201,12 +215,31 @@ var THREAT_SEVERITY = {
201
215
  size_limit_exceeded: "low",
202
216
  unknown: "medium"
203
217
  };
218
+ var INTERNAL_FIELDS = /* @__PURE__ */ new Set([
219
+ "detected_by",
220
+ "policy_id",
221
+ "policy_name",
222
+ "matched_pattern",
223
+ "matched_text",
224
+ "pattern",
225
+ "scan_stage",
226
+ "ai_reasoning",
227
+ "llm_analysis",
228
+ "performance_metrics",
229
+ "performance"
230
+ ]);
204
231
  function normalizeThreatType(rawType) {
205
232
  if (!rawType) {
206
233
  return "unknown";
207
234
  }
208
235
  const normalized = rawType.toLowerCase().replace(/-/g, "_");
209
- return THREAT_TYPE_MAP[normalized] || "unknown";
236
+ if (THREAT_TYPE_MAP[normalized]) {
237
+ return THREAT_TYPE_MAP[normalized];
238
+ }
239
+ if (normalized.startsWith("multi_turn_")) {
240
+ return "multi_turn_attack";
241
+ }
242
+ return "unknown";
210
243
  }
211
244
  function deriveSeverity(threatType, rawSeverity) {
212
245
  const validSeverities = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
@@ -227,20 +260,59 @@ function bucketConfidence(score) {
227
260
  }
228
261
  return "low";
229
262
  }
263
+ var PRESERVED_GOVERNANCE_FIELDS = [
264
+ "action",
265
+ "refuse_tier",
266
+ "recovery",
267
+ "session_state",
268
+ "content_type",
269
+ "approval_info",
270
+ "client_session_rotation"
271
+ ];
272
+ function sanitizeViolation(raw) {
273
+ if (!raw || typeof raw !== "object") {
274
+ return null;
275
+ }
276
+ const source = raw;
277
+ const out = {};
278
+ for (const [k, v] of Object.entries(source)) {
279
+ if (!INTERNAL_FIELDS.has(k)) {
280
+ out[k] = v;
281
+ }
282
+ }
283
+ return out;
284
+ }
230
285
  function sanitizeScanResponse(raw) {
231
286
  const safe = raw.safe !== false;
232
- const approvalInfo = raw.approval_info;
287
+ const result = { safe };
288
+ for (const field of PRESERVED_GOVERNANCE_FIELDS) {
289
+ const value = raw[field];
290
+ if (value !== void 0 && value !== null) {
291
+ result[field] = value;
292
+ }
293
+ }
294
+ const rawViolations = raw.violations;
295
+ if (Array.isArray(rawViolations) && rawViolations.length > 0) {
296
+ const cleaned = rawViolations.map((v) => sanitizeViolation(v)).filter((v) => v !== null);
297
+ if (cleaned.length > 0) {
298
+ result.violations = cleaned;
299
+ }
300
+ }
233
301
  if (safe) {
234
- const result2 = {
235
- safe: true,
236
- reason: raw.reason || ""
237
- };
238
- if (approvalInfo) {
239
- result2.approval_info = approvalInfo;
302
+ result.reason = raw.reason || "";
303
+ return result;
304
+ }
305
+ let rawThreatType = raw.threat_type;
306
+ if (!rawThreatType && Array.isArray(rawViolations) && rawViolations.length > 0) {
307
+ const first = rawViolations[0];
308
+ if (first && typeof first === "object" && !Array.isArray(first)) {
309
+ const firstThreat = first.threat_type;
310
+ if (typeof firstThreat === "string") {
311
+ rawThreatType = firstThreat;
312
+ }
240
313
  }
241
- return result2;
242
314
  }
243
- const threatType = normalizeThreatType(raw.threat_type);
315
+ const threatType = normalizeThreatType(rawThreatType);
244
316
  const confidence = bucketConfidence(
245
317
  typeof raw.confidence === "number" ? raw.confidence : void 0
246
318
  );
@@ -249,22 +321,19 @@ function sanitizeScanResponse(raw) {
249
321
  typeof raw.severity === "string" ? raw.severity : void 0
250
322
  );
251
323
  const guidance = THREAT_GUIDANCE[threatType] || THREAT_GUIDANCE["unknown"];
252
- const result = {
253
- safe: false,
254
- threat_type: threatType,
255
- severity,
256
- confidence,
257
- reason: raw.reason || guidance,
258
- guidance
259
- };
260
- if (approvalInfo) {
261
- result.approval_info = approvalInfo;
262
- }
324
+ result.threat_type = threatType;
325
+ result.severity = severity;
326
+ result.confidence = confidence;
327
+ result.reason = raw.reason || guidance;
328
+ result.guidance = guidance;
263
329
  return result;
264
330
  }
265
331
 
266
332
  // src/version.ts
267
- var VERSION = "1.3.1";
333
+ var VERSION = "1.5.0";
334
+ function failOpenResult(reason) {
335
+ return { safe: true, reason, degraded: true };
336
+ }
268
337
  function generateUUID() {
269
338
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
270
339
  const r = Math.random() * 16 | 0;
@@ -281,6 +350,18 @@ function getScanHeaders(shrikeApiKey, requestId) {
281
350
  "X-Shrike-Request-ID": generateUUID()
282
351
  };
283
352
  }
353
+ function isBlocked(verdict) {
354
+ const action = verdict.action;
355
+ if (typeof action === "string" && action.length > 0) {
356
+ if (action === "allow" || action === "warn") return false;
357
+ if (action === "block" || action === "require_approval") return true;
358
+ }
359
+ const safe = verdict.safe;
360
+ if (typeof safe === "boolean") {
361
+ return !safe;
362
+ }
363
+ return false;
364
+ }
284
365
  function maybeAddSignupHint(result, apiKey) {
285
366
  if (apiKey) return result;
286
367
  if (result.upgrade_hint) return result;
@@ -353,7 +434,7 @@ var ShrikeAnthropic = class {
353
434
  const controller = new AbortController();
354
435
  const timeoutId = setTimeout(() => controller.abort(), this._scanTimeout);
355
436
  try {
356
- const response = await fetch(`${this._shrikeEndpoint}/scan`, {
437
+ const response = await fetch(`${this._shrikeEndpoint}/api/scan/enforce`, {
357
438
  method: "POST",
358
439
  headers: getScanHeaders(this._shrikeApiKey),
359
440
  body: JSON.stringify({
@@ -368,7 +449,7 @@ var ShrikeAnthropic = class {
368
449
  });
369
450
  if (!response.ok) {
370
451
  if (this._failMode === "open" /* OPEN */) {
371
- return { safe: true, reason: `Scan API error: ${response.status}` };
452
+ return failOpenResult(`Scan API error: ${response.status}`);
372
453
  }
373
454
  throw new ShrikeScanError(`Scan API returned error: ${response.status}`);
374
455
  }
@@ -381,12 +462,12 @@ var ShrikeAnthropic = class {
381
462
  if (errorMessage.includes("abort")) {
382
463
  if (this._failMode === "open" /* OPEN */) {
383
464
  logWarning("Scan request timed out, failing open (allowing request)");
384
- return { safe: true, reason: "Scan timeout, failing open" };
465
+ return failOpenResult("Scan timeout, failing open");
385
466
  }
386
467
  throw new ShrikeScanError("Scan request timed out and fail_mode is 'closed'");
387
468
  }
388
469
  if (this._failMode === "open" /* OPEN */) {
389
- return { safe: true, reason: `Scan error: ${errorMessage}` };
470
+ return failOpenResult(`Scan error: ${errorMessage}`);
390
471
  }
391
472
  throw new ShrikeScanError(`Scan failed: ${errorMessage}`);
392
473
  } finally {
@@ -412,7 +493,7 @@ var MessagesNamespace = class {
412
493
  const scanResult = await this._client._scanMessages(
413
494
  params.messages
414
495
  );
415
- if (!scanResult.safe) {
496
+ if (isBlocked(scanResult)) {
416
497
  throw new ShrikeBlockedError(
417
498
  `Request blocked: ${scanResult.reason || "Security threat detected"}`,
418
499
  scanResult.threat_type,