@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/README.md +279 -46
- package/dist/index.cjs +282 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -13
- package/dist/index.d.ts +37 -13
- package/dist/index.js +279 -87
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
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
|
-
|
|
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,
|
|
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
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
return
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
628
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
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
|
|
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
|