@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.js
CHANGED
|
@@ -9,6 +9,22 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/exceptions.ts
|
|
12
|
+
function parseErrorBody(body) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(body);
|
|
15
|
+
if (!parsed || typeof parsed !== "object") {
|
|
16
|
+
return { details: void 0, error: void 0, apiMessage: void 0 };
|
|
17
|
+
}
|
|
18
|
+
const data = parsed;
|
|
19
|
+
return {
|
|
20
|
+
details: data.details && typeof data.details === "object" ? data.details : void 0,
|
|
21
|
+
error: typeof data.error === "string" ? data.error : void 0,
|
|
22
|
+
apiMessage: typeof data.message === "string" ? data.message : void 0
|
|
23
|
+
};
|
|
24
|
+
} catch {
|
|
25
|
+
return { details: void 0, error: void 0, apiMessage: void 0 };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
12
28
|
var MAX_PROMPT_DISPLAY_LEN, PromptBlockedException, SilmarilApiError;
|
|
13
29
|
var init_exceptions = __esm({
|
|
14
30
|
"src/exceptions.ts"() {
|
|
@@ -37,12 +53,20 @@ var init_exceptions = __esm({
|
|
|
37
53
|
status;
|
|
38
54
|
statusText;
|
|
39
55
|
body;
|
|
56
|
+
details;
|
|
57
|
+
error;
|
|
58
|
+
apiMessage;
|
|
40
59
|
constructor(params) {
|
|
41
|
-
|
|
60
|
+
const statusText = params.statusText ? ` ${params.statusText}` : "";
|
|
61
|
+
super(`Silmaril API error ${params.status}${statusText}`);
|
|
42
62
|
this.name = "SilmarilApiError";
|
|
43
63
|
this.status = params.status;
|
|
44
64
|
this.statusText = params.statusText;
|
|
45
65
|
this.body = params.body;
|
|
66
|
+
const parsed = parseErrorBody(params.body);
|
|
67
|
+
this.details = params.details ?? parsed.details;
|
|
68
|
+
this.error = params.error ?? parsed.error;
|
|
69
|
+
this.apiMessage = params.apiMessage ?? parsed.apiMessage;
|
|
46
70
|
Object.setPrototypeOf(this, _SilmarilApiError.prototype);
|
|
47
71
|
}
|
|
48
72
|
};
|
|
@@ -78,7 +102,7 @@ function prependToolName(text, toolName) {
|
|
|
78
102
|
}
|
|
79
103
|
return `[TOOL:${toolName}] ${text}`;
|
|
80
104
|
}
|
|
81
|
-
var HookLabel, FirewallHook, DEFAULT_HOOKS, INPUT_HOOKS, OUTPUT_HOOKS, ALL_HOOKS, FIREWALL_HOOK_TO_LABEL,
|
|
105
|
+
var HookLabel, FirewallHook, DEFAULT_HOOKS, INPUT_HOOKS, OUTPUT_HOOKS, ALL_HOOKS, FIREWALL_HOOK_TO_LABEL, FIREWALL_HOOK_VALUES;
|
|
82
106
|
var init_hooks = __esm({
|
|
83
107
|
"src/hooks.ts"() {
|
|
84
108
|
"use strict";
|
|
@@ -127,14 +151,6 @@ var init_hooks = __esm({
|
|
|
127
151
|
[FirewallHook.RETRIEVER_END]: HookLabel.TOOL_RESPONSE,
|
|
128
152
|
[FirewallHook.LLM_END]: HookLabel.LLM_OUTPUT
|
|
129
153
|
};
|
|
130
|
-
DEFAULT_HOOK_THRESHOLDS = {
|
|
131
|
-
[HookLabel.USER_INPUT]: 0.5,
|
|
132
|
-
[HookLabel.SYSTEM_PROMPT]: 0.3,
|
|
133
|
-
[HookLabel.TOOL_CALL]: 0.4,
|
|
134
|
-
[HookLabel.TOOL_RESPONSE]: 0.3,
|
|
135
|
-
[HookLabel.LLM_OUTPUT]: 0.4,
|
|
136
|
-
[HookLabel.UNKNOWN]: 0.5
|
|
137
|
-
};
|
|
138
154
|
FIREWALL_HOOK_VALUES = new Set(Object.values(FirewallHook));
|
|
139
155
|
}
|
|
140
156
|
});
|
|
@@ -221,40 +237,49 @@ async function createLangChainHandler(firewall, options = {}) {
|
|
|
221
237
|
const logger = options.logger ?? ((message, error) => {
|
|
222
238
|
console.warn(`silmaril.firewall: ${message}`, error);
|
|
223
239
|
});
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
return
|
|
240
|
+
const shadowMode = options.shadowMode ?? firewall.shadowMode;
|
|
241
|
+
const onClassify = options.onClassify;
|
|
242
|
+
const fireOnClassify = (hookLabel, text, result, blocked, toolName) => {
|
|
243
|
+
if (!onClassify) {
|
|
244
|
+
return;
|
|
229
245
|
}
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
try {
|
|
247
|
+
onClassify({
|
|
248
|
+
hook: hookLabel,
|
|
249
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
250
|
+
text,
|
|
251
|
+
result,
|
|
252
|
+
blocked,
|
|
253
|
+
shadowMode
|
|
254
|
+
});
|
|
255
|
+
} catch (err) {
|
|
256
|
+
logger("onClassify callback threw", err);
|
|
232
257
|
}
|
|
233
|
-
return overrideThreshold ?? firewall.threshold;
|
|
234
258
|
};
|
|
235
259
|
const classify = async (text, hookLabel, runId, toolName) => {
|
|
260
|
+
let result;
|
|
236
261
|
try {
|
|
237
|
-
|
|
262
|
+
result = await firewall.classify(text, {
|
|
238
263
|
hook: hookLabel,
|
|
239
264
|
...toolName !== void 0 ? { toolName } : {}
|
|
240
265
|
});
|
|
241
|
-
const threshold = effectiveThreshold(hookLabel);
|
|
242
|
-
if (result.score >= threshold) {
|
|
243
|
-
throw new PromptBlockedException({
|
|
244
|
-
score: result.score,
|
|
245
|
-
threshold,
|
|
246
|
-
promptText: text,
|
|
247
|
-
runId
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
266
|
} catch (err) {
|
|
251
|
-
if (err instanceof PromptBlockedException) {
|
|
252
|
-
throw err;
|
|
253
|
-
}
|
|
254
267
|
if (!failOpen) {
|
|
255
268
|
throw err;
|
|
256
269
|
}
|
|
257
270
|
logger("classification failed, allowing prompt through", err);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const threshold = result.threshold;
|
|
274
|
+
const blocked = result.score >= threshold;
|
|
275
|
+
fireOnClassify(hookLabel, text, result, blocked, toolName);
|
|
276
|
+
if (blocked && !shadowMode) {
|
|
277
|
+
throw new PromptBlockedException({
|
|
278
|
+
score: result.score,
|
|
279
|
+
threshold,
|
|
280
|
+
promptText: text,
|
|
281
|
+
runId
|
|
282
|
+
});
|
|
258
283
|
}
|
|
259
284
|
};
|
|
260
285
|
class SilmarilFirewallHandler extends BaseCallbackHandler {
|
|
@@ -301,8 +326,7 @@ async function createLangChainHandler(firewall, options = {}) {
|
|
|
301
326
|
return;
|
|
302
327
|
}
|
|
303
328
|
const toolName = tool?.name;
|
|
304
|
-
|
|
305
|
-
await classify(prefixed, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
|
|
329
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
|
|
306
330
|
}
|
|
307
331
|
async handleRetrieverStart(_retriever, query, runId) {
|
|
308
332
|
if (!enabledHooks.has(FirewallHook.RETRIEVER_START)) {
|
|
@@ -333,8 +357,7 @@ async function createLangChainHandler(firewall, options = {}) {
|
|
|
333
357
|
return;
|
|
334
358
|
}
|
|
335
359
|
const toolName = _kwargs?.name;
|
|
336
|
-
|
|
337
|
-
await classify(prefixed, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
|
|
360
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
|
|
338
361
|
}
|
|
339
362
|
async handleRetrieverEnd(documents, runId) {
|
|
340
363
|
if (!enabledHooks.has(FirewallHook.RETRIEVER_END)) {
|
|
@@ -420,17 +443,7 @@ function findLastUserMessage(messages) {
|
|
|
420
443
|
function createMiddleware(firewall, options = {}) {
|
|
421
444
|
const scanInput = options.scanInput ?? true;
|
|
422
445
|
const scanOutput = options.scanOutput ?? false;
|
|
423
|
-
const
|
|
424
|
-
const overrideHookThresholds = options.hookThresholds ?? {};
|
|
425
|
-
const effectiveThreshold = (label) => {
|
|
426
|
-
if (overrideHookThresholds[label] !== void 0) {
|
|
427
|
-
return overrideHookThresholds[label];
|
|
428
|
-
}
|
|
429
|
-
if (firewall.hookThresholds[label] !== void 0) {
|
|
430
|
-
return firewall.hookThresholds[label];
|
|
431
|
-
}
|
|
432
|
-
return overrideThreshold ?? firewall.threshold;
|
|
433
|
-
};
|
|
446
|
+
const shadowMode = options.shadowMode ?? firewall.shadowMode;
|
|
434
447
|
const classifyOrBlock = async (text, hook, toolName) => {
|
|
435
448
|
if (!text.trim()) {
|
|
436
449
|
return;
|
|
@@ -439,16 +452,17 @@ function createMiddleware(firewall, options = {}) {
|
|
|
439
452
|
text,
|
|
440
453
|
toolName !== void 0 ? { hook, toolName } : { hook }
|
|
441
454
|
);
|
|
442
|
-
const threshold =
|
|
455
|
+
const threshold = result.threshold;
|
|
443
456
|
const blocked = result.score >= threshold;
|
|
444
457
|
options.onClassify?.({
|
|
445
458
|
hook,
|
|
446
459
|
...toolName !== void 0 ? { toolName } : {},
|
|
447
460
|
text,
|
|
448
461
|
result,
|
|
449
|
-
blocked
|
|
462
|
+
blocked,
|
|
463
|
+
shadowMode
|
|
450
464
|
});
|
|
451
|
-
if (blocked) {
|
|
465
|
+
if (blocked && !shadowMode) {
|
|
452
466
|
const err = new PromptBlockedException({
|
|
453
467
|
score: result.score,
|
|
454
468
|
threshold,
|
|
@@ -561,7 +575,45 @@ var MAX_INPUT_CHARS = MAX_INPUT_TOKENS * CHARS_PER_TOKEN;
|
|
|
561
575
|
var CHUNK_WINDOW_CHARS = CHUNK_WINDOW * CHARS_PER_TOKEN;
|
|
562
576
|
var CHUNK_OVERLAP_CHARS = CHUNK_OVERLAP * CHARS_PER_TOKEN;
|
|
563
577
|
var CHUNK_STRIDE_CHARS = CHUNK_WINDOW_CHARS - CHUNK_OVERLAP_CHARS;
|
|
578
|
+
function isHighSurrogate(code) {
|
|
579
|
+
return code >= 55296 && code <= 56319;
|
|
580
|
+
}
|
|
581
|
+
function isLowSurrogate(code) {
|
|
582
|
+
return code >= 56320 && code <= 57343;
|
|
583
|
+
}
|
|
584
|
+
function sanitizeText(text) {
|
|
585
|
+
let out = "";
|
|
586
|
+
for (let i = 0; i < text.length; i++) {
|
|
587
|
+
const code = text.charCodeAt(i);
|
|
588
|
+
if (isHighSurrogate(code)) {
|
|
589
|
+
if (i + 1 < text.length && isLowSurrogate(text.charCodeAt(i + 1))) {
|
|
590
|
+
out += text[i];
|
|
591
|
+
out += text[i + 1];
|
|
592
|
+
i++;
|
|
593
|
+
}
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (isLowSurrogate(code)) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
out += text[i];
|
|
600
|
+
}
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
function safeChunkStart(text, start) {
|
|
604
|
+
if (start > 0 && start < text.length && isLowSurrogate(text.charCodeAt(start)) && isHighSurrogate(text.charCodeAt(start - 1))) {
|
|
605
|
+
return start + 1;
|
|
606
|
+
}
|
|
607
|
+
return start;
|
|
608
|
+
}
|
|
609
|
+
function safeChunkEnd(text, end) {
|
|
610
|
+
if (end > 0 && end < text.length && isHighSurrogate(text.charCodeAt(end - 1)) && isLowSurrogate(text.charCodeAt(end))) {
|
|
611
|
+
return end - 1;
|
|
612
|
+
}
|
|
613
|
+
return end;
|
|
614
|
+
}
|
|
564
615
|
function chunkText(text) {
|
|
616
|
+
text = sanitizeText(text);
|
|
565
617
|
const n = text.length;
|
|
566
618
|
if (n > MAX_INPUT_CHARS) {
|
|
567
619
|
throw new Error(
|
|
@@ -573,8 +625,12 @@ function chunkText(text) {
|
|
|
573
625
|
}
|
|
574
626
|
const chunks = [];
|
|
575
627
|
for (let start = 0; start < n; start += CHUNK_STRIDE_CHARS) {
|
|
576
|
-
const
|
|
577
|
-
|
|
628
|
+
const safeStart = safeChunkStart(text, start);
|
|
629
|
+
if (safeStart >= n) {
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
const end = safeChunkEnd(text, safeStart + CHUNK_WINDOW_CHARS);
|
|
633
|
+
chunks.push(text.slice(safeStart, end));
|
|
578
634
|
if (end >= n) {
|
|
579
635
|
break;
|
|
580
636
|
}
|
|
@@ -584,16 +640,122 @@ function chunkText(text) {
|
|
|
584
640
|
|
|
585
641
|
// src/firewall.ts
|
|
586
642
|
init_exceptions();
|
|
587
|
-
var
|
|
643
|
+
var BASE_THRESHOLD = 0.5;
|
|
644
|
+
var TARGET_SEQUENCE_FPR = 0.01;
|
|
645
|
+
var MAX_ADAPTIVE_THRESHOLD = 0.9;
|
|
588
646
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
647
|
+
var DEFAULT_CHUNK_CONCURRENCY = 8;
|
|
589
648
|
var DEFAULT_MAX_RETRIES = 5;
|
|
590
649
|
var MAX_BACKOFF_SECONDS = 30;
|
|
650
|
+
var MAX_ERROR_BODY_BYTES = 1 << 16;
|
|
651
|
+
function adaptiveThreshold(scoringOpportunityCount) {
|
|
652
|
+
if (!Number.isInteger(scoringOpportunityCount) || scoringOpportunityCount < 1) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`Firewall: scoringOpportunityCount must be an integer >= 1, got ${scoringOpportunityCount}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (scoringOpportunityCount === 1) {
|
|
658
|
+
return BASE_THRESHOLD;
|
|
659
|
+
}
|
|
660
|
+
const targetChunkFpr = 1 - Math.pow(1 - TARGET_SEQUENCE_FPR, 1 / scoringOpportunityCount);
|
|
661
|
+
const oddsRatio = TARGET_SEQUENCE_FPR / targetChunkFpr;
|
|
662
|
+
const rawThreshold = oddsRatio / (1 + oddsRatio);
|
|
663
|
+
return Math.min(rawThreshold, MAX_ADAPTIVE_THRESHOLD);
|
|
664
|
+
}
|
|
665
|
+
function blockResultFromResponse(data, threshold) {
|
|
666
|
+
const result = {
|
|
667
|
+
prediction: data.prediction,
|
|
668
|
+
score: Number(data.score),
|
|
669
|
+
threshold
|
|
670
|
+
};
|
|
671
|
+
if (data.primary_outcome !== void 0) {
|
|
672
|
+
result.primaryOutcome = data.primary_outcome;
|
|
673
|
+
}
|
|
674
|
+
if (data.outcome_scores !== void 0) {
|
|
675
|
+
result.outcomeScores = Object.freeze(
|
|
676
|
+
Object.fromEntries(Object.entries(data.outcome_scores).map(([k, v]) => [k, Number(v)]))
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (data.detector_scores !== void 0) {
|
|
680
|
+
result.detectorScores = Object.freeze(
|
|
681
|
+
Object.fromEntries(Object.entries(data.detector_scores).map(([k, v]) => [k, Number(v)]))
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
if (data.detector_counts !== void 0) {
|
|
685
|
+
result.detectorCounts = Object.freeze(
|
|
686
|
+
Object.fromEntries(Object.entries(data.detector_counts).map(([k, v]) => [k, Number(v)]))
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
return Object.freeze(result);
|
|
690
|
+
}
|
|
691
|
+
async function readCappedErrorBody(response) {
|
|
692
|
+
if (!response.body) {
|
|
693
|
+
return response.text().then((body2) => body2.slice(0, MAX_ERROR_BODY_BYTES)).catch(() => "");
|
|
694
|
+
}
|
|
695
|
+
const reader = response.body.getReader();
|
|
696
|
+
const chunks = [];
|
|
697
|
+
let remaining = MAX_ERROR_BODY_BYTES;
|
|
698
|
+
try {
|
|
699
|
+
while (remaining > 0) {
|
|
700
|
+
const { done, value } = await reader.read();
|
|
701
|
+
if (done) {
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
if (!value) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const chunk = value.byteLength > remaining ? value.slice(0, remaining) : value;
|
|
708
|
+
chunks.push(chunk);
|
|
709
|
+
remaining -= chunk.byteLength;
|
|
710
|
+
if (chunk.byteLength < value.byteLength) {
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
return "";
|
|
716
|
+
} finally {
|
|
717
|
+
await reader.cancel().catch(() => void 0);
|
|
718
|
+
}
|
|
719
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
720
|
+
const body = new Uint8Array(total);
|
|
721
|
+
let offset = 0;
|
|
722
|
+
for (const chunk of chunks) {
|
|
723
|
+
body.set(chunk, offset);
|
|
724
|
+
offset += chunk.byteLength;
|
|
725
|
+
}
|
|
726
|
+
return new TextDecoder().decode(body);
|
|
727
|
+
}
|
|
728
|
+
async function mapWithConcurrency(items, concurrency, task) {
|
|
729
|
+
const results = new Array(items.length);
|
|
730
|
+
let nextIndex = 0;
|
|
731
|
+
let firstError;
|
|
732
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
733
|
+
async function worker() {
|
|
734
|
+
for (; ; ) {
|
|
735
|
+
const index = nextIndex;
|
|
736
|
+
nextIndex += 1;
|
|
737
|
+
if (index >= items.length) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
results[index] = await task(items[index], index);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
firstError ??= err;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
748
|
+
if (firstError !== void 0) {
|
|
749
|
+
throw firstError;
|
|
750
|
+
}
|
|
751
|
+
return results;
|
|
752
|
+
}
|
|
591
753
|
var Firewall = class {
|
|
592
754
|
apiKey;
|
|
593
755
|
apiUrl;
|
|
594
|
-
threshold;
|
|
595
756
|
timeoutMs;
|
|
596
|
-
|
|
757
|
+
chunkConcurrency;
|
|
758
|
+
shadowMode;
|
|
597
759
|
headers;
|
|
598
760
|
constructor(options) {
|
|
599
761
|
if (!options.apiKey) {
|
|
@@ -604,9 +766,17 @@ var Firewall = class {
|
|
|
604
766
|
}
|
|
605
767
|
this.apiKey = options.apiKey;
|
|
606
768
|
this.apiUrl = options.apiUrl;
|
|
607
|
-
this.threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
608
769
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
609
|
-
this.
|
|
770
|
+
if (typeof this.timeoutMs !== "number" || !Number.isFinite(this.timeoutMs) || this.timeoutMs < 0) {
|
|
771
|
+
throw new Error(`Firewall: timeoutMs must be a finite non-negative number, got ${this.timeoutMs}`);
|
|
772
|
+
}
|
|
773
|
+
this.chunkConcurrency = options.chunkConcurrency ?? DEFAULT_CHUNK_CONCURRENCY;
|
|
774
|
+
if (!Number.isInteger(this.chunkConcurrency) || this.chunkConcurrency < 1) {
|
|
775
|
+
throw new Error(
|
|
776
|
+
`Firewall: chunkConcurrency must be an integer >= 1, got ${this.chunkConcurrency}`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
this.shadowMode = options.shadowMode ?? false;
|
|
610
780
|
this.headers = Object.freeze({
|
|
611
781
|
"x-api-key": this.apiKey,
|
|
612
782
|
"content-type": "application/json"
|
|
@@ -614,39 +784,52 @@ var Firewall = class {
|
|
|
614
784
|
}
|
|
615
785
|
async classify(text, options = {}) {
|
|
616
786
|
const chunks = chunkText(text);
|
|
787
|
+
const threshold = adaptiveThreshold(chunks.length);
|
|
617
788
|
if (chunks.length === 1) {
|
|
618
|
-
|
|
619
|
-
if (options.hook !== void 0) {
|
|
620
|
-
payload.hook = options.hook;
|
|
621
|
-
}
|
|
622
|
-
if (options.toolName !== void 0) {
|
|
623
|
-
payload.tool_name = options.toolName;
|
|
624
|
-
}
|
|
625
|
-
const data = await this.postWithRetry(payload);
|
|
626
|
-
return Object.freeze({ prediction: data.prediction, score: Number(data.score) });
|
|
627
|
-
}
|
|
628
|
-
const batchOpts = {};
|
|
629
|
-
if (options.hook !== void 0) {
|
|
630
|
-
batchOpts.hooks = chunks.map(() => options.hook);
|
|
789
|
+
return this.classifySingleChunk(chunks[0], options, threshold);
|
|
631
790
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
791
|
+
const results = await mapWithConcurrency(
|
|
792
|
+
chunks,
|
|
793
|
+
this.chunkConcurrency,
|
|
794
|
+
(chunk) => this.classifySingleChunk(chunk, options, threshold)
|
|
795
|
+
);
|
|
636
796
|
return results.reduce((best, r) => r.score > best.score ? r : best);
|
|
637
797
|
}
|
|
638
798
|
async classifyBatch(texts, options = {}) {
|
|
639
|
-
|
|
799
|
+
if (texts.length === 0) {
|
|
800
|
+
throw new Error("Firewall: texts must not be empty");
|
|
801
|
+
}
|
|
802
|
+
if (options.hooks !== void 0 && options.hooks.length !== texts.length) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`Firewall: hooks length ${options.hooks.length} does not match texts length ${texts.length}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
if (options.toolNames !== void 0 && options.toolNames.length !== texts.length) {
|
|
808
|
+
throw new Error(
|
|
809
|
+
`Firewall: toolNames length ${options.toolNames.length} does not match texts length ${texts.length}`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (options.metadata !== void 0 && options.metadata.length !== texts.length) {
|
|
813
|
+
throw new Error(
|
|
814
|
+
`Firewall: metadata length ${options.metadata.length} does not match texts length ${texts.length}`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const threshold = adaptiveThreshold(texts.length);
|
|
818
|
+
const payload = {
|
|
819
|
+
texts: texts.map((text) => sanitizeText(text)),
|
|
820
|
+
threshold
|
|
821
|
+
};
|
|
640
822
|
if (options.hooks && options.hooks.length > 0) {
|
|
641
823
|
payload.hooks = options.hooks.map((h) => String(h));
|
|
642
824
|
}
|
|
643
825
|
if (options.toolNames && options.toolNames.length > 0) {
|
|
644
826
|
payload.tool_names = options.toolNames.map((t) => t === void 0 ? null : t);
|
|
645
827
|
}
|
|
828
|
+
if (options.metadata && options.metadata.length > 0) {
|
|
829
|
+
payload.metadata = options.metadata.map((m) => m === void 0 ? null : m);
|
|
830
|
+
}
|
|
646
831
|
const data = await this.postWithRetry(payload);
|
|
647
|
-
return data.predictions.map(
|
|
648
|
-
(p) => Object.freeze({ prediction: p.prediction, score: Number(p.score) })
|
|
649
|
-
);
|
|
832
|
+
return data.predictions.map((p) => blockResultFromResponse(p, threshold));
|
|
650
833
|
}
|
|
651
834
|
asLangChainHandler(options = {}) {
|
|
652
835
|
return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then((m) => m.createLangChainHandler(this, options));
|
|
@@ -654,23 +837,18 @@ var Firewall = class {
|
|
|
654
837
|
asMiddleware(options = {}) {
|
|
655
838
|
return createMiddleware(this, options);
|
|
656
839
|
}
|
|
657
|
-
effectiveThreshold(hook) {
|
|
658
|
-
if (hook === void 0) {
|
|
659
|
-
return this.threshold;
|
|
660
|
-
}
|
|
661
|
-
return this.hookThresholds[hook] ?? this.threshold;
|
|
662
|
-
}
|
|
663
840
|
async postWithRetry(payload, maxRetries = DEFAULT_MAX_RETRIES) {
|
|
664
841
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
665
842
|
const response = await fetch(this.apiUrl, {
|
|
666
843
|
method: "POST",
|
|
667
844
|
headers: this.headers,
|
|
668
845
|
body: JSON.stringify(payload),
|
|
846
|
+
redirect: "error",
|
|
669
847
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
670
848
|
});
|
|
671
849
|
if (response.status !== 429 || attempt === maxRetries) {
|
|
672
850
|
if (!response.ok) {
|
|
673
|
-
const body = await response
|
|
851
|
+
const body = await readCappedErrorBody(response);
|
|
674
852
|
throw new SilmarilApiError({
|
|
675
853
|
status: response.status,
|
|
676
854
|
statusText: response.statusText,
|
|
@@ -684,6 +862,20 @@ var Firewall = class {
|
|
|
684
862
|
}
|
|
685
863
|
throw new Error("Firewall: exhausted retries (unreachable)");
|
|
686
864
|
}
|
|
865
|
+
async classifySingleChunk(text, options, threshold) {
|
|
866
|
+
const payload = { text, threshold };
|
|
867
|
+
if (options.hook !== void 0) {
|
|
868
|
+
payload.hook = options.hook;
|
|
869
|
+
}
|
|
870
|
+
if (options.toolName !== void 0) {
|
|
871
|
+
payload.tool_name = options.toolName;
|
|
872
|
+
}
|
|
873
|
+
if (options.metadata !== void 0) {
|
|
874
|
+
payload.metadata = options.metadata;
|
|
875
|
+
}
|
|
876
|
+
const data = await this.postWithRetry(payload);
|
|
877
|
+
return blockResultFromResponse(data, threshold);
|
|
878
|
+
}
|
|
687
879
|
};
|
|
688
880
|
|
|
689
881
|
// src/index.ts
|
|
@@ -695,9 +887,8 @@ export {
|
|
|
695
887
|
CHUNK_OVERLAP_CHARS,
|
|
696
888
|
CHUNK_WINDOW,
|
|
697
889
|
CHUNK_WINDOW_CHARS,
|
|
890
|
+
DEFAULT_CHUNK_CONCURRENCY,
|
|
698
891
|
DEFAULT_HOOKS,
|
|
699
|
-
DEFAULT_HOOK_THRESHOLDS,
|
|
700
|
-
DEFAULT_THRESHOLD,
|
|
701
892
|
DEFAULT_TIMEOUT_MS,
|
|
702
893
|
FIREWALL_HOOK_TO_LABEL,
|
|
703
894
|
Firewall,
|
|
@@ -712,6 +903,7 @@ export {
|
|
|
712
903
|
chunkText,
|
|
713
904
|
prependHook,
|
|
714
905
|
prependToolName,
|
|
715
|
-
resolveHooks
|
|
906
|
+
resolveHooks,
|
|
907
|
+
sanitizeText
|
|
716
908
|
};
|
|
717
909
|
//# sourceMappingURL=index.js.map
|