@silmaril-security/sdk 0.1.2 → 0.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/dist/index.cjs CHANGED
@@ -31,6 +31,22 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/exceptions.ts
34
+ function parseErrorBody(body) {
35
+ try {
36
+ const parsed = JSON.parse(body);
37
+ if (!parsed || typeof parsed !== "object") {
38
+ return { details: void 0, error: void 0, apiMessage: void 0 };
39
+ }
40
+ const data = parsed;
41
+ return {
42
+ details: data.details && typeof data.details === "object" ? data.details : void 0,
43
+ error: typeof data.error === "string" ? data.error : void 0,
44
+ apiMessage: typeof data.message === "string" ? data.message : void 0
45
+ };
46
+ } catch {
47
+ return { details: void 0, error: void 0, apiMessage: void 0 };
48
+ }
49
+ }
34
50
  var MAX_PROMPT_DISPLAY_LEN, PromptBlockedException, SilmarilApiError;
35
51
  var init_exceptions = __esm({
36
52
  "src/exceptions.ts"() {
@@ -59,12 +75,20 @@ var init_exceptions = __esm({
59
75
  status;
60
76
  statusText;
61
77
  body;
78
+ details;
79
+ error;
80
+ apiMessage;
62
81
  constructor(params) {
63
- super(`Silmaril API error ${params.status} ${params.statusText}: ${params.body}`);
82
+ const statusText = params.statusText ? ` ${params.statusText}` : "";
83
+ super(`Silmaril API error ${params.status}${statusText}`);
64
84
  this.name = "SilmarilApiError";
65
85
  this.status = params.status;
66
86
  this.statusText = params.statusText;
67
87
  this.body = params.body;
88
+ const parsed = parseErrorBody(params.body);
89
+ this.details = params.details ?? parsed.details;
90
+ this.error = params.error ?? parsed.error;
91
+ this.apiMessage = params.apiMessage ?? parsed.apiMessage;
68
92
  Object.setPrototypeOf(this, _SilmarilApiError.prototype);
69
93
  }
70
94
  };
@@ -100,7 +124,7 @@ function prependToolName(text, toolName) {
100
124
  }
101
125
  return `[TOOL:${toolName}] ${text}`;
102
126
  }
103
- var HookLabel, FirewallHook, DEFAULT_HOOKS, INPUT_HOOKS, OUTPUT_HOOKS, ALL_HOOKS, FIREWALL_HOOK_TO_LABEL, DEFAULT_HOOK_THRESHOLDS, FIREWALL_HOOK_VALUES;
127
+ var HookLabel, FirewallHook, DEFAULT_HOOKS, INPUT_HOOKS, OUTPUT_HOOKS, ALL_HOOKS, FIREWALL_HOOK_TO_LABEL, FIREWALL_HOOK_VALUES;
104
128
  var init_hooks = __esm({
105
129
  "src/hooks.ts"() {
106
130
  "use strict";
@@ -149,14 +173,6 @@ var init_hooks = __esm({
149
173
  [FirewallHook.RETRIEVER_END]: HookLabel.TOOL_RESPONSE,
150
174
  [FirewallHook.LLM_END]: HookLabel.LLM_OUTPUT
151
175
  };
152
- DEFAULT_HOOK_THRESHOLDS = {
153
- [HookLabel.USER_INPUT]: 0.5,
154
- [HookLabel.SYSTEM_PROMPT]: 0.3,
155
- [HookLabel.TOOL_CALL]: 0.4,
156
- [HookLabel.TOOL_RESPONSE]: 0.3,
157
- [HookLabel.LLM_OUTPUT]: 0.4,
158
- [HookLabel.UNKNOWN]: 0.5
159
- };
160
176
  FIREWALL_HOOK_VALUES = new Set(Object.values(FirewallHook));
161
177
  }
162
178
  });
@@ -243,40 +259,49 @@ async function createLangChainHandler(firewall, options = {}) {
243
259
  const logger = options.logger ?? ((message, error) => {
244
260
  console.warn(`silmaril.firewall: ${message}`, error);
245
261
  });
246
- const overrideThreshold = options.threshold;
247
- const overrideHookThresholds = options.hookThresholds ?? {};
248
- const effectiveThreshold = (label) => {
249
- if (overrideHookThresholds[label] !== void 0) {
250
- return overrideHookThresholds[label];
262
+ const shadowMode = options.shadowMode ?? firewall.shadowMode;
263
+ const onClassify = options.onClassify;
264
+ const fireOnClassify = (hookLabel, text, result, blocked, toolName) => {
265
+ if (!onClassify) {
266
+ return;
251
267
  }
252
- if (firewall.hookThresholds[label] !== void 0) {
253
- return firewall.hookThresholds[label];
268
+ try {
269
+ onClassify({
270
+ hook: hookLabel,
271
+ ...toolName !== void 0 ? { toolName } : {},
272
+ text,
273
+ result,
274
+ blocked,
275
+ shadowMode
276
+ });
277
+ } catch (err) {
278
+ logger("onClassify callback threw", err);
254
279
  }
255
- return overrideThreshold ?? firewall.threshold;
256
280
  };
257
281
  const classify = async (text, hookLabel, runId, toolName) => {
282
+ let result;
258
283
  try {
259
- const result = await firewall.classify(text, {
284
+ result = await firewall.classify(text, {
260
285
  hook: hookLabel,
261
286
  ...toolName !== void 0 ? { toolName } : {}
262
287
  });
263
- const threshold = effectiveThreshold(hookLabel);
264
- if (result.score >= threshold) {
265
- throw new PromptBlockedException({
266
- score: result.score,
267
- threshold,
268
- promptText: text,
269
- runId
270
- });
271
- }
272
288
  } catch (err) {
273
- if (err instanceof PromptBlockedException) {
274
- throw err;
275
- }
276
289
  if (!failOpen) {
277
290
  throw err;
278
291
  }
279
292
  logger("classification failed, allowing prompt through", err);
293
+ return;
294
+ }
295
+ const threshold = result.threshold;
296
+ const blocked = result.score >= threshold;
297
+ fireOnClassify(hookLabel, text, result, blocked, toolName);
298
+ if (blocked && !shadowMode) {
299
+ throw new PromptBlockedException({
300
+ score: result.score,
301
+ threshold,
302
+ promptText: text,
303
+ runId
304
+ });
280
305
  }
281
306
  };
282
307
  class SilmarilFirewallHandler extends BaseCallbackHandler {
@@ -323,8 +348,7 @@ async function createLangChainHandler(firewall, options = {}) {
323
348
  return;
324
349
  }
325
350
  const toolName = tool?.name;
326
- const prefixed = prependToolName(text, toolName);
327
- await classify(prefixed, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
351
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
328
352
  }
329
353
  async handleRetrieverStart(_retriever, query, runId) {
330
354
  if (!enabledHooks.has(FirewallHook.RETRIEVER_START)) {
@@ -355,8 +379,7 @@ async function createLangChainHandler(firewall, options = {}) {
355
379
  return;
356
380
  }
357
381
  const toolName = _kwargs?.name;
358
- const prefixed = prependToolName(text, toolName);
359
- await classify(prefixed, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
382
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
360
383
  }
361
384
  async handleRetrieverEnd(documents, runId) {
362
385
  if (!enabledHooks.has(FirewallHook.RETRIEVER_END)) {
@@ -390,9 +413,8 @@ __export(index_exports, {
390
413
  CHUNK_OVERLAP_CHARS: () => CHUNK_OVERLAP_CHARS,
391
414
  CHUNK_WINDOW: () => CHUNK_WINDOW,
392
415
  CHUNK_WINDOW_CHARS: () => CHUNK_WINDOW_CHARS,
416
+ DEFAULT_CHUNK_CONCURRENCY: () => DEFAULT_CHUNK_CONCURRENCY,
393
417
  DEFAULT_HOOKS: () => DEFAULT_HOOKS,
394
- DEFAULT_HOOK_THRESHOLDS: () => DEFAULT_HOOK_THRESHOLDS,
395
- DEFAULT_THRESHOLD: () => DEFAULT_THRESHOLD,
396
418
  DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
397
419
  FIREWALL_HOOK_TO_LABEL: () => FIREWALL_HOOK_TO_LABEL,
398
420
  Firewall: () => Firewall,
@@ -407,7 +429,8 @@ __export(index_exports, {
407
429
  chunkText: () => chunkText,
408
430
  prependHook: () => prependHook,
409
431
  prependToolName: () => prependToolName,
410
- resolveHooks: () => resolveHooks
432
+ resolveHooks: () => resolveHooks,
433
+ sanitizeText: () => sanitizeText
411
434
  });
412
435
  module.exports = __toCommonJS(index_exports);
413
436
 
@@ -471,17 +494,7 @@ function findLastUserMessage(messages) {
471
494
  function createMiddleware(firewall, options = {}) {
472
495
  const scanInput = options.scanInput ?? true;
473
496
  const scanOutput = options.scanOutput ?? false;
474
- const overrideThreshold = options.threshold;
475
- const overrideHookThresholds = options.hookThresholds ?? {};
476
- const effectiveThreshold = (label) => {
477
- if (overrideHookThresholds[label] !== void 0) {
478
- return overrideHookThresholds[label];
479
- }
480
- if (firewall.hookThresholds[label] !== void 0) {
481
- return firewall.hookThresholds[label];
482
- }
483
- return overrideThreshold ?? firewall.threshold;
484
- };
497
+ const shadowMode = options.shadowMode ?? firewall.shadowMode;
485
498
  const classifyOrBlock = async (text, hook, toolName) => {
486
499
  if (!text.trim()) {
487
500
  return;
@@ -490,16 +503,17 @@ function createMiddleware(firewall, options = {}) {
490
503
  text,
491
504
  toolName !== void 0 ? { hook, toolName } : { hook }
492
505
  );
493
- const threshold = effectiveThreshold(hook);
506
+ const threshold = result.threshold;
494
507
  const blocked = result.score >= threshold;
495
508
  options.onClassify?.({
496
509
  hook,
497
510
  ...toolName !== void 0 ? { toolName } : {},
498
511
  text,
499
512
  result,
500
- blocked
513
+ blocked,
514
+ shadowMode
501
515
  });
502
- if (blocked) {
516
+ if (blocked && !shadowMode) {
503
517
  const err = new PromptBlockedException({
504
518
  score: result.score,
505
519
  threshold,
@@ -612,7 +626,45 @@ var MAX_INPUT_CHARS = MAX_INPUT_TOKENS * CHARS_PER_TOKEN;
612
626
  var CHUNK_WINDOW_CHARS = CHUNK_WINDOW * CHARS_PER_TOKEN;
613
627
  var CHUNK_OVERLAP_CHARS = CHUNK_OVERLAP * CHARS_PER_TOKEN;
614
628
  var CHUNK_STRIDE_CHARS = CHUNK_WINDOW_CHARS - CHUNK_OVERLAP_CHARS;
629
+ function isHighSurrogate(code) {
630
+ return code >= 55296 && code <= 56319;
631
+ }
632
+ function isLowSurrogate(code) {
633
+ return code >= 56320 && code <= 57343;
634
+ }
635
+ function sanitizeText(text) {
636
+ let out = "";
637
+ for (let i = 0; i < text.length; i++) {
638
+ const code = text.charCodeAt(i);
639
+ if (isHighSurrogate(code)) {
640
+ if (i + 1 < text.length && isLowSurrogate(text.charCodeAt(i + 1))) {
641
+ out += text[i];
642
+ out += text[i + 1];
643
+ i++;
644
+ }
645
+ continue;
646
+ }
647
+ if (isLowSurrogate(code)) {
648
+ continue;
649
+ }
650
+ out += text[i];
651
+ }
652
+ return out;
653
+ }
654
+ function safeChunkStart(text, start) {
655
+ if (start > 0 && start < text.length && isLowSurrogate(text.charCodeAt(start)) && isHighSurrogate(text.charCodeAt(start - 1))) {
656
+ return start + 1;
657
+ }
658
+ return start;
659
+ }
660
+ function safeChunkEnd(text, end) {
661
+ if (end > 0 && end < text.length && isHighSurrogate(text.charCodeAt(end - 1)) && isLowSurrogate(text.charCodeAt(end))) {
662
+ return end - 1;
663
+ }
664
+ return end;
665
+ }
615
666
  function chunkText(text) {
667
+ text = sanitizeText(text);
616
668
  const n = text.length;
617
669
  if (n > MAX_INPUT_CHARS) {
618
670
  throw new Error(
@@ -624,8 +676,12 @@ function chunkText(text) {
624
676
  }
625
677
  const chunks = [];
626
678
  for (let start = 0; start < n; start += CHUNK_STRIDE_CHARS) {
627
- const end = start + CHUNK_WINDOW_CHARS;
628
- chunks.push(text.slice(start, end));
679
+ const safeStart = safeChunkStart(text, start);
680
+ if (safeStart >= n) {
681
+ break;
682
+ }
683
+ const end = safeChunkEnd(text, safeStart + CHUNK_WINDOW_CHARS);
684
+ chunks.push(text.slice(safeStart, end));
629
685
  if (end >= n) {
630
686
  break;
631
687
  }
@@ -635,16 +691,122 @@ function chunkText(text) {
635
691
 
636
692
  // src/firewall.ts
637
693
  init_exceptions();
638
- var DEFAULT_THRESHOLD = 0.5;
694
+ var BASE_THRESHOLD = 0.5;
695
+ var TARGET_SEQUENCE_FPR = 0.01;
696
+ var MAX_ADAPTIVE_THRESHOLD = 0.9;
639
697
  var DEFAULT_TIMEOUT_MS = 1e4;
698
+ var DEFAULT_CHUNK_CONCURRENCY = 8;
640
699
  var DEFAULT_MAX_RETRIES = 5;
641
700
  var MAX_BACKOFF_SECONDS = 30;
701
+ var MAX_ERROR_BODY_BYTES = 1 << 16;
702
+ function adaptiveThreshold(scoringOpportunityCount) {
703
+ if (!Number.isInteger(scoringOpportunityCount) || scoringOpportunityCount < 1) {
704
+ throw new Error(
705
+ `Firewall: scoringOpportunityCount must be an integer >= 1, got ${scoringOpportunityCount}`
706
+ );
707
+ }
708
+ if (scoringOpportunityCount === 1) {
709
+ return BASE_THRESHOLD;
710
+ }
711
+ const targetChunkFpr = 1 - Math.pow(1 - TARGET_SEQUENCE_FPR, 1 / scoringOpportunityCount);
712
+ const oddsRatio = TARGET_SEQUENCE_FPR / targetChunkFpr;
713
+ const rawThreshold = oddsRatio / (1 + oddsRatio);
714
+ return Math.min(rawThreshold, MAX_ADAPTIVE_THRESHOLD);
715
+ }
716
+ function blockResultFromResponse(data, threshold) {
717
+ const result = {
718
+ prediction: data.prediction,
719
+ score: Number(data.score),
720
+ threshold
721
+ };
722
+ if (data.primary_outcome !== void 0) {
723
+ result.primaryOutcome = data.primary_outcome;
724
+ }
725
+ if (data.outcome_scores !== void 0) {
726
+ result.outcomeScores = Object.freeze(
727
+ Object.fromEntries(Object.entries(data.outcome_scores).map(([k, v]) => [k, Number(v)]))
728
+ );
729
+ }
730
+ if (data.detector_scores !== void 0) {
731
+ result.detectorScores = Object.freeze(
732
+ Object.fromEntries(Object.entries(data.detector_scores).map(([k, v]) => [k, Number(v)]))
733
+ );
734
+ }
735
+ if (data.detector_counts !== void 0) {
736
+ result.detectorCounts = Object.freeze(
737
+ Object.fromEntries(Object.entries(data.detector_counts).map(([k, v]) => [k, Number(v)]))
738
+ );
739
+ }
740
+ return Object.freeze(result);
741
+ }
742
+ async function readCappedErrorBody(response) {
743
+ if (!response.body) {
744
+ return response.text().then((body2) => body2.slice(0, MAX_ERROR_BODY_BYTES)).catch(() => "");
745
+ }
746
+ const reader = response.body.getReader();
747
+ const chunks = [];
748
+ let remaining = MAX_ERROR_BODY_BYTES;
749
+ try {
750
+ while (remaining > 0) {
751
+ const { done, value } = await reader.read();
752
+ if (done) {
753
+ break;
754
+ }
755
+ if (!value) {
756
+ continue;
757
+ }
758
+ const chunk = value.byteLength > remaining ? value.slice(0, remaining) : value;
759
+ chunks.push(chunk);
760
+ remaining -= chunk.byteLength;
761
+ if (chunk.byteLength < value.byteLength) {
762
+ break;
763
+ }
764
+ }
765
+ } catch {
766
+ return "";
767
+ } finally {
768
+ await reader.cancel().catch(() => void 0);
769
+ }
770
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
771
+ const body = new Uint8Array(total);
772
+ let offset = 0;
773
+ for (const chunk of chunks) {
774
+ body.set(chunk, offset);
775
+ offset += chunk.byteLength;
776
+ }
777
+ return new TextDecoder().decode(body);
778
+ }
779
+ async function mapWithConcurrency(items, concurrency, task) {
780
+ const results = new Array(items.length);
781
+ let nextIndex = 0;
782
+ let firstError;
783
+ const workerCount = Math.min(concurrency, items.length);
784
+ async function worker() {
785
+ for (; ; ) {
786
+ const index = nextIndex;
787
+ nextIndex += 1;
788
+ if (index >= items.length) {
789
+ return;
790
+ }
791
+ try {
792
+ results[index] = await task(items[index], index);
793
+ } catch (err) {
794
+ firstError ??= err;
795
+ }
796
+ }
797
+ }
798
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
799
+ if (firstError !== void 0) {
800
+ throw firstError;
801
+ }
802
+ return results;
803
+ }
642
804
  var Firewall = class {
643
805
  apiKey;
644
806
  apiUrl;
645
- threshold;
646
807
  timeoutMs;
647
- hookThresholds;
808
+ chunkConcurrency;
809
+ shadowMode;
648
810
  headers;
649
811
  constructor(options) {
650
812
  if (!options.apiKey) {
@@ -655,9 +817,17 @@ var Firewall = class {
655
817
  }
656
818
  this.apiKey = options.apiKey;
657
819
  this.apiUrl = options.apiUrl;
658
- this.threshold = options.threshold ?? DEFAULT_THRESHOLD;
659
820
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
660
- this.hookThresholds = options.hookThresholds ?? {};
821
+ if (typeof this.timeoutMs !== "number" || !Number.isFinite(this.timeoutMs) || this.timeoutMs < 0) {
822
+ throw new Error(`Firewall: timeoutMs must be a finite non-negative number, got ${this.timeoutMs}`);
823
+ }
824
+ this.chunkConcurrency = options.chunkConcurrency ?? DEFAULT_CHUNK_CONCURRENCY;
825
+ if (!Number.isInteger(this.chunkConcurrency) || this.chunkConcurrency < 1) {
826
+ throw new Error(
827
+ `Firewall: chunkConcurrency must be an integer >= 1, got ${this.chunkConcurrency}`
828
+ );
829
+ }
830
+ this.shadowMode = options.shadowMode ?? false;
661
831
  this.headers = Object.freeze({
662
832
  "x-api-key": this.apiKey,
663
833
  "content-type": "application/json"
@@ -665,39 +835,52 @@ var Firewall = class {
665
835
  }
666
836
  async classify(text, options = {}) {
667
837
  const chunks = chunkText(text);
838
+ const threshold = adaptiveThreshold(chunks.length);
668
839
  if (chunks.length === 1) {
669
- const payload = { text: chunks[0] };
670
- if (options.hook !== void 0) {
671
- payload.hook = options.hook;
672
- }
673
- if (options.toolName !== void 0) {
674
- payload.tool_name = options.toolName;
675
- }
676
- const data = await this.postWithRetry(payload);
677
- return Object.freeze({ prediction: data.prediction, score: Number(data.score) });
678
- }
679
- const batchOpts = {};
680
- if (options.hook !== void 0) {
681
- batchOpts.hooks = chunks.map(() => options.hook);
840
+ return this.classifySingleChunk(chunks[0], options, threshold);
682
841
  }
683
- if (options.toolName !== void 0) {
684
- batchOpts.toolNames = chunks.map(() => options.toolName);
685
- }
686
- const results = await this.classifyBatch(chunks, batchOpts);
842
+ const results = await mapWithConcurrency(
843
+ chunks,
844
+ this.chunkConcurrency,
845
+ (chunk) => this.classifySingleChunk(chunk, options, threshold)
846
+ );
687
847
  return results.reduce((best, r) => r.score > best.score ? r : best);
688
848
  }
689
849
  async classifyBatch(texts, options = {}) {
690
- const payload = { texts };
850
+ if (texts.length === 0) {
851
+ throw new Error("Firewall: texts must not be empty");
852
+ }
853
+ if (options.hooks !== void 0 && options.hooks.length !== texts.length) {
854
+ throw new Error(
855
+ `Firewall: hooks length ${options.hooks.length} does not match texts length ${texts.length}`
856
+ );
857
+ }
858
+ if (options.toolNames !== void 0 && options.toolNames.length !== texts.length) {
859
+ throw new Error(
860
+ `Firewall: toolNames length ${options.toolNames.length} does not match texts length ${texts.length}`
861
+ );
862
+ }
863
+ if (options.metadata !== void 0 && options.metadata.length !== texts.length) {
864
+ throw new Error(
865
+ `Firewall: metadata length ${options.metadata.length} does not match texts length ${texts.length}`
866
+ );
867
+ }
868
+ const threshold = adaptiveThreshold(texts.length);
869
+ const payload = {
870
+ texts: texts.map((text) => sanitizeText(text)),
871
+ threshold
872
+ };
691
873
  if (options.hooks && options.hooks.length > 0) {
692
874
  payload.hooks = options.hooks.map((h) => String(h));
693
875
  }
694
876
  if (options.toolNames && options.toolNames.length > 0) {
695
877
  payload.tool_names = options.toolNames.map((t) => t === void 0 ? null : t);
696
878
  }
879
+ if (options.metadata && options.metadata.length > 0) {
880
+ payload.metadata = options.metadata.map((m) => m === void 0 ? null : m);
881
+ }
697
882
  const data = await this.postWithRetry(payload);
698
- return data.predictions.map(
699
- (p) => Object.freeze({ prediction: p.prediction, score: Number(p.score) })
700
- );
883
+ return data.predictions.map((p) => blockResultFromResponse(p, threshold));
701
884
  }
702
885
  asLangChainHandler(options = {}) {
703
886
  return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then((m) => m.createLangChainHandler(this, options));
@@ -705,23 +888,18 @@ var Firewall = class {
705
888
  asMiddleware(options = {}) {
706
889
  return createMiddleware(this, options);
707
890
  }
708
- effectiveThreshold(hook) {
709
- if (hook === void 0) {
710
- return this.threshold;
711
- }
712
- return this.hookThresholds[hook] ?? this.threshold;
713
- }
714
891
  async postWithRetry(payload, maxRetries = DEFAULT_MAX_RETRIES) {
715
892
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
716
893
  const response = await fetch(this.apiUrl, {
717
894
  method: "POST",
718
895
  headers: this.headers,
719
896
  body: JSON.stringify(payload),
897
+ redirect: "error",
720
898
  signal: AbortSignal.timeout(this.timeoutMs)
721
899
  });
722
900
  if (response.status !== 429 || attempt === maxRetries) {
723
901
  if (!response.ok) {
724
- const body = await response.text().catch(() => "");
902
+ const body = await readCappedErrorBody(response);
725
903
  throw new SilmarilApiError({
726
904
  status: response.status,
727
905
  statusText: response.statusText,
@@ -735,6 +913,20 @@ var Firewall = class {
735
913
  }
736
914
  throw new Error("Firewall: exhausted retries (unreachable)");
737
915
  }
916
+ async classifySingleChunk(text, options, threshold) {
917
+ const payload = { text, threshold };
918
+ if (options.hook !== void 0) {
919
+ payload.hook = options.hook;
920
+ }
921
+ if (options.toolName !== void 0) {
922
+ payload.tool_name = options.toolName;
923
+ }
924
+ if (options.metadata !== void 0) {
925
+ payload.metadata = options.metadata;
926
+ }
927
+ const data = await this.postWithRetry(payload);
928
+ return blockResultFromResponse(data, threshold);
929
+ }
738
930
  };
739
931
 
740
932
  // src/index.ts
@@ -747,9 +939,8 @@ init_hooks();
747
939
  CHUNK_OVERLAP_CHARS,
748
940
  CHUNK_WINDOW,
749
941
  CHUNK_WINDOW_CHARS,
942
+ DEFAULT_CHUNK_CONCURRENCY,
750
943
  DEFAULT_HOOKS,
751
- DEFAULT_HOOK_THRESHOLDS,
752
- DEFAULT_THRESHOLD,
753
944
  DEFAULT_TIMEOUT_MS,
754
945
  FIREWALL_HOOK_TO_LABEL,
755
946
  Firewall,
@@ -764,6 +955,7 @@ init_hooks();
764
955
  chunkText,
765
956
  prependHook,
766
957
  prependToolName,
767
- resolveHooks
958
+ resolveHooks,
959
+ sanitizeText
768
960
  });
769
961
  //# sourceMappingURL=index.cjs.map