@silmaril-security/sdk 0.1.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/LICENSE +12 -0
- package/README.md +121 -0
- package/dist/index.cjs +763 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +167 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.js +711 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/exceptions.ts
|
|
34
|
+
var MAX_PROMPT_DISPLAY_LEN, PromptBlockedException, SilmarilApiError;
|
|
35
|
+
var init_exceptions = __esm({
|
|
36
|
+
"src/exceptions.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
MAX_PROMPT_DISPLAY_LEN = 100;
|
|
39
|
+
PromptBlockedException = class _PromptBlockedException extends Error {
|
|
40
|
+
score;
|
|
41
|
+
threshold;
|
|
42
|
+
promptText;
|
|
43
|
+
runId;
|
|
44
|
+
constructor(params) {
|
|
45
|
+
super(_PromptBlockedException.formatMessage(params));
|
|
46
|
+
this.name = "PromptBlockedException";
|
|
47
|
+
this.score = params.score;
|
|
48
|
+
this.threshold = params.threshold;
|
|
49
|
+
this.promptText = params.promptText;
|
|
50
|
+
this.runId = params.runId;
|
|
51
|
+
Object.setPrototypeOf(this, _PromptBlockedException.prototype);
|
|
52
|
+
}
|
|
53
|
+
static formatMessage(params) {
|
|
54
|
+
const truncated = params.promptText.length > MAX_PROMPT_DISPLAY_LEN ? `${params.promptText.slice(0, MAX_PROMPT_DISPLAY_LEN)}...` : params.promptText;
|
|
55
|
+
return `Prompt blocked by Silmaril Firewall (score=${params.score.toFixed(4)}, threshold=${params.threshold.toFixed(4)}): '${truncated}'`;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
SilmarilApiError = class _SilmarilApiError extends Error {
|
|
59
|
+
status;
|
|
60
|
+
statusText;
|
|
61
|
+
body;
|
|
62
|
+
constructor(params) {
|
|
63
|
+
super(`Silmaril API error ${params.status} ${params.statusText}: ${params.body}`);
|
|
64
|
+
this.name = "SilmarilApiError";
|
|
65
|
+
this.status = params.status;
|
|
66
|
+
this.statusText = params.statusText;
|
|
67
|
+
this.body = params.body;
|
|
68
|
+
Object.setPrototypeOf(this, _SilmarilApiError.prototype);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// src/hooks.ts
|
|
75
|
+
function resolveHooks(hooks) {
|
|
76
|
+
if (hooks === void 0) {
|
|
77
|
+
return DEFAULT_HOOKS;
|
|
78
|
+
}
|
|
79
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
80
|
+
for (const h of hooks) {
|
|
81
|
+
if (!FIREWALL_HOOK_VALUES.has(h)) {
|
|
82
|
+
throw new Error(`Invalid FirewallHook value: ${String(h)}`);
|
|
83
|
+
}
|
|
84
|
+
resolved.add(h);
|
|
85
|
+
}
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
function prependHook(text, hook) {
|
|
89
|
+
if (hook === void 0 || hook === null) {
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
if (hook === HookLabel.UNKNOWN) {
|
|
93
|
+
return text;
|
|
94
|
+
}
|
|
95
|
+
return `[HOOK:${hook}] ${text}`;
|
|
96
|
+
}
|
|
97
|
+
function prependToolName(text, toolName) {
|
|
98
|
+
if (toolName === void 0 || toolName === null) {
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
return `[TOOL:${toolName}] ${text}`;
|
|
102
|
+
}
|
|
103
|
+
var HookLabel, FirewallHook, DEFAULT_HOOKS, INPUT_HOOKS, OUTPUT_HOOKS, ALL_HOOKS, FIREWALL_HOOK_TO_LABEL, DEFAULT_HOOK_THRESHOLDS, FIREWALL_HOOK_VALUES;
|
|
104
|
+
var init_hooks = __esm({
|
|
105
|
+
"src/hooks.ts"() {
|
|
106
|
+
"use strict";
|
|
107
|
+
HookLabel = {
|
|
108
|
+
USER_INPUT: "user_input",
|
|
109
|
+
SYSTEM_PROMPT: "system_prompt",
|
|
110
|
+
TOOL_CALL: "tool_call",
|
|
111
|
+
TOOL_RESPONSE: "tool_response",
|
|
112
|
+
LLM_OUTPUT: "llm_output",
|
|
113
|
+
UNKNOWN: "unknown"
|
|
114
|
+
};
|
|
115
|
+
FirewallHook = {
|
|
116
|
+
LLM_START: "on_llm_start",
|
|
117
|
+
CHAT_MODEL_START: "on_chat_model_start",
|
|
118
|
+
TOOL_START: "on_tool_start",
|
|
119
|
+
RETRIEVER_START: "on_retriever_start",
|
|
120
|
+
LLM_END: "on_llm_end",
|
|
121
|
+
TOOL_END: "on_tool_end",
|
|
122
|
+
RETRIEVER_END: "on_retriever_end"
|
|
123
|
+
};
|
|
124
|
+
DEFAULT_HOOKS = /* @__PURE__ */ new Set([
|
|
125
|
+
FirewallHook.LLM_START,
|
|
126
|
+
FirewallHook.CHAT_MODEL_START
|
|
127
|
+
]);
|
|
128
|
+
INPUT_HOOKS = /* @__PURE__ */ new Set([
|
|
129
|
+
FirewallHook.LLM_START,
|
|
130
|
+
FirewallHook.CHAT_MODEL_START,
|
|
131
|
+
FirewallHook.TOOL_START,
|
|
132
|
+
FirewallHook.RETRIEVER_START
|
|
133
|
+
]);
|
|
134
|
+
OUTPUT_HOOKS = /* @__PURE__ */ new Set([
|
|
135
|
+
FirewallHook.LLM_END,
|
|
136
|
+
FirewallHook.TOOL_END,
|
|
137
|
+
FirewallHook.RETRIEVER_END
|
|
138
|
+
]);
|
|
139
|
+
ALL_HOOKS = /* @__PURE__ */ new Set([
|
|
140
|
+
...INPUT_HOOKS,
|
|
141
|
+
...OUTPUT_HOOKS
|
|
142
|
+
]);
|
|
143
|
+
FIREWALL_HOOK_TO_LABEL = {
|
|
144
|
+
[FirewallHook.CHAT_MODEL_START]: HookLabel.USER_INPUT,
|
|
145
|
+
[FirewallHook.LLM_START]: HookLabel.USER_INPUT,
|
|
146
|
+
[FirewallHook.TOOL_START]: HookLabel.TOOL_CALL,
|
|
147
|
+
[FirewallHook.TOOL_END]: HookLabel.TOOL_RESPONSE,
|
|
148
|
+
[FirewallHook.RETRIEVER_START]: HookLabel.TOOL_CALL,
|
|
149
|
+
[FirewallHook.RETRIEVER_END]: HookLabel.TOOL_RESPONSE,
|
|
150
|
+
[FirewallHook.LLM_END]: HookLabel.LLM_OUTPUT
|
|
151
|
+
};
|
|
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
|
+
FIREWALL_HOOK_VALUES = new Set(Object.values(FirewallHook));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// src/utils/extract.ts
|
|
165
|
+
function extractTextFromPrompts(prompts) {
|
|
166
|
+
return prompts.map((p) => p.trim()).filter((p) => p.length > 0).join("\n");
|
|
167
|
+
}
|
|
168
|
+
function extractTextFromToolInput(inputStr) {
|
|
169
|
+
return inputStr.trim();
|
|
170
|
+
}
|
|
171
|
+
function extractTextFromLLMResult(response) {
|
|
172
|
+
const parts = [];
|
|
173
|
+
for (const genList of response.generations ?? []) {
|
|
174
|
+
for (const gen of genList) {
|
|
175
|
+
const text = (gen.text ?? "").trim();
|
|
176
|
+
if (text) {
|
|
177
|
+
parts.push(text);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return parts.join("\n");
|
|
182
|
+
}
|
|
183
|
+
function extractTextFromDocuments(documents) {
|
|
184
|
+
const parts = [];
|
|
185
|
+
for (const doc of documents) {
|
|
186
|
+
const text = (doc.pageContent ?? "").trim();
|
|
187
|
+
if (text) {
|
|
188
|
+
parts.push(text);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return parts.join("\n");
|
|
192
|
+
}
|
|
193
|
+
var init_extract = __esm({
|
|
194
|
+
"src/utils/extract.ts"() {
|
|
195
|
+
"use strict";
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/adapters/langchain.ts
|
|
200
|
+
var langchain_exports = {};
|
|
201
|
+
__export(langchain_exports, {
|
|
202
|
+
createLangChainHandler: () => createLangChainHandler
|
|
203
|
+
});
|
|
204
|
+
function getMessageRole(message) {
|
|
205
|
+
if (typeof message.role === "string") {
|
|
206
|
+
return message.role.toLowerCase();
|
|
207
|
+
}
|
|
208
|
+
if (typeof message.type === "string") {
|
|
209
|
+
return message.type.toLowerCase();
|
|
210
|
+
}
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
function extractMessageText(content) {
|
|
214
|
+
if (content === void 0) {
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
if (typeof content === "string") {
|
|
218
|
+
return content;
|
|
219
|
+
}
|
|
220
|
+
const parts = [];
|
|
221
|
+
for (const block of content) {
|
|
222
|
+
if (typeof block === "string") {
|
|
223
|
+
parts.push(block);
|
|
224
|
+
} else if (block && block.type === "text") {
|
|
225
|
+
parts.push(block.text ?? "");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return parts.join(" ");
|
|
229
|
+
}
|
|
230
|
+
function findLastUserMessage2(messages) {
|
|
231
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
232
|
+
const msg = messages[i];
|
|
233
|
+
if (USER_ROLES.has(getMessageRole(msg))) {
|
|
234
|
+
return msg;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
async function createLangChainHandler(firewall, options = {}) {
|
|
240
|
+
const { BaseCallbackHandler } = await import("@langchain/core/callbacks/base");
|
|
241
|
+
const enabledHooks = resolveHooks(options.hooks);
|
|
242
|
+
const failOpen = options.failOpen ?? true;
|
|
243
|
+
const logger = options.logger ?? ((message, error) => {
|
|
244
|
+
console.warn(`silmaril.firewall: ${message}`, error);
|
|
245
|
+
});
|
|
246
|
+
const overrideThreshold = options.threshold;
|
|
247
|
+
const overrideHookThresholds = options.hookThresholds ?? {};
|
|
248
|
+
const effectiveThreshold = (label) => {
|
|
249
|
+
if (overrideHookThresholds[label] !== void 0) {
|
|
250
|
+
return overrideHookThresholds[label];
|
|
251
|
+
}
|
|
252
|
+
if (firewall.hookThresholds[label] !== void 0) {
|
|
253
|
+
return firewall.hookThresholds[label];
|
|
254
|
+
}
|
|
255
|
+
return overrideThreshold ?? firewall.threshold;
|
|
256
|
+
};
|
|
257
|
+
const classify = async (text, hookLabel, runId, toolName) => {
|
|
258
|
+
try {
|
|
259
|
+
const result = await firewall.classify(text, {
|
|
260
|
+
hook: hookLabel,
|
|
261
|
+
...toolName !== void 0 ? { toolName } : {}
|
|
262
|
+
});
|
|
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
|
+
} catch (err) {
|
|
273
|
+
if (err instanceof PromptBlockedException) {
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
if (!failOpen) {
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
logger("classification failed, allowing prompt through", err);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
class SilmarilFirewallHandler extends BaseCallbackHandler {
|
|
283
|
+
name = "silmaril_firewall_handler";
|
|
284
|
+
raiseError = true;
|
|
285
|
+
awaitHandlers = true;
|
|
286
|
+
async handleChatModelStart(_llm, messages, runId) {
|
|
287
|
+
if (!enabledHooks.has(FirewallHook.CHAT_MODEL_START)) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const batches = messages;
|
|
291
|
+
const flat = [];
|
|
292
|
+
for (const batch of batches) {
|
|
293
|
+
for (const m of batch) {
|
|
294
|
+
flat.push(m);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const lastUser = findLastUserMessage2(flat);
|
|
298
|
+
if (!lastUser) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const text = extractMessageText(lastUser.content).trim();
|
|
302
|
+
if (!text) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.CHAT_MODEL_START], runId);
|
|
306
|
+
}
|
|
307
|
+
async handleLLMStart(_llm, prompts, runId) {
|
|
308
|
+
if (!enabledHooks.has(FirewallHook.LLM_START)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const text = extractTextFromPrompts(prompts);
|
|
312
|
+
if (!text) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_START], runId);
|
|
316
|
+
}
|
|
317
|
+
async handleToolStart(tool, inputStr, runId) {
|
|
318
|
+
if (!enabledHooks.has(FirewallHook.TOOL_START)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const text = extractTextFromToolInput(inputStr);
|
|
322
|
+
if (!text) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const toolName = tool?.name;
|
|
326
|
+
const prefixed = prependToolName(text, toolName);
|
|
327
|
+
await classify(prefixed, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
|
|
328
|
+
}
|
|
329
|
+
async handleRetrieverStart(_retriever, query, runId) {
|
|
330
|
+
if (!enabledHooks.has(FirewallHook.RETRIEVER_START)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const text = query.trim();
|
|
334
|
+
if (!text) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_START], runId);
|
|
338
|
+
}
|
|
339
|
+
async handleLLMEnd(output, runId) {
|
|
340
|
+
if (!enabledHooks.has(FirewallHook.LLM_END)) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const text = extractTextFromLLMResult(output);
|
|
344
|
+
if (!text) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_END], runId);
|
|
348
|
+
}
|
|
349
|
+
async handleToolEnd(output, runId, _parentRunId, _tags, _kwargs) {
|
|
350
|
+
if (!enabledHooks.has(FirewallHook.TOOL_END)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const text = String(output).trim();
|
|
354
|
+
if (!text) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const toolName = _kwargs?.name;
|
|
358
|
+
const prefixed = prependToolName(text, toolName);
|
|
359
|
+
await classify(prefixed, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
|
|
360
|
+
}
|
|
361
|
+
async handleRetrieverEnd(documents, runId) {
|
|
362
|
+
if (!enabledHooks.has(FirewallHook.RETRIEVER_END)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const text = extractTextFromDocuments(documents);
|
|
366
|
+
if (!text) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_END], runId);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return new SilmarilFirewallHandler();
|
|
373
|
+
}
|
|
374
|
+
var USER_ROLES;
|
|
375
|
+
var init_langchain = __esm({
|
|
376
|
+
"src/adapters/langchain.ts"() {
|
|
377
|
+
"use strict";
|
|
378
|
+
init_exceptions();
|
|
379
|
+
init_hooks();
|
|
380
|
+
init_extract();
|
|
381
|
+
USER_ROLES = /* @__PURE__ */ new Set(["human", "user"]);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// src/index.ts
|
|
386
|
+
var index_exports = {};
|
|
387
|
+
__export(index_exports, {
|
|
388
|
+
ALL_HOOKS: () => ALL_HOOKS,
|
|
389
|
+
CHUNK_OVERLAP: () => CHUNK_OVERLAP,
|
|
390
|
+
CHUNK_OVERLAP_CHARS: () => CHUNK_OVERLAP_CHARS,
|
|
391
|
+
CHUNK_WINDOW: () => CHUNK_WINDOW,
|
|
392
|
+
CHUNK_WINDOW_CHARS: () => CHUNK_WINDOW_CHARS,
|
|
393
|
+
DEFAULT_HOOKS: () => DEFAULT_HOOKS,
|
|
394
|
+
DEFAULT_HOOK_THRESHOLDS: () => DEFAULT_HOOK_THRESHOLDS,
|
|
395
|
+
DEFAULT_THRESHOLD: () => DEFAULT_THRESHOLD,
|
|
396
|
+
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
397
|
+
FIREWALL_HOOK_TO_LABEL: () => FIREWALL_HOOK_TO_LABEL,
|
|
398
|
+
Firewall: () => Firewall,
|
|
399
|
+
FirewallHook: () => FirewallHook,
|
|
400
|
+
HookLabel: () => HookLabel,
|
|
401
|
+
INPUT_HOOKS: () => INPUT_HOOKS,
|
|
402
|
+
MAX_INPUT_CHARS: () => MAX_INPUT_CHARS,
|
|
403
|
+
MAX_INPUT_TOKENS: () => MAX_INPUT_TOKENS,
|
|
404
|
+
OUTPUT_HOOKS: () => OUTPUT_HOOKS,
|
|
405
|
+
PromptBlockedException: () => PromptBlockedException,
|
|
406
|
+
SilmarilApiError: () => SilmarilApiError,
|
|
407
|
+
chunkText: () => chunkText,
|
|
408
|
+
prependHook: () => prependHook,
|
|
409
|
+
prependToolName: () => prependToolName,
|
|
410
|
+
resolveHooks: () => resolveHooks
|
|
411
|
+
});
|
|
412
|
+
module.exports = __toCommonJS(index_exports);
|
|
413
|
+
|
|
414
|
+
// src/adapters/vercel.ts
|
|
415
|
+
init_exceptions();
|
|
416
|
+
init_hooks();
|
|
417
|
+
function stringifyToolValue(value) {
|
|
418
|
+
if (value === null || value === void 0) {
|
|
419
|
+
return "";
|
|
420
|
+
}
|
|
421
|
+
if (typeof value === "string") {
|
|
422
|
+
return value;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
return JSON.stringify(value);
|
|
426
|
+
} catch {
|
|
427
|
+
return String(value);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function extractContentText(content) {
|
|
431
|
+
if (typeof content === "string") {
|
|
432
|
+
return content;
|
|
433
|
+
}
|
|
434
|
+
const parts = [];
|
|
435
|
+
for (const block of content) {
|
|
436
|
+
if (typeof block === "string") {
|
|
437
|
+
parts.push(block);
|
|
438
|
+
} else if (block && block.type === "text") {
|
|
439
|
+
parts.push(block.text ?? "");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return parts.join(" ");
|
|
443
|
+
}
|
|
444
|
+
function iterateToolResultParts(message) {
|
|
445
|
+
if (typeof message.content === "string") {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
const out = [];
|
|
449
|
+
for (const part of message.content) {
|
|
450
|
+
if (typeof part === "string") {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (part.type === "tool-result") {
|
|
454
|
+
const text = stringifyToolValue(part.result);
|
|
455
|
+
if (text.trim()) {
|
|
456
|
+
out.push({ text, toolName: part.toolName });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
function findLastUserMessage(messages) {
|
|
463
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
464
|
+
const msg = messages[i];
|
|
465
|
+
if (msg.role.toLowerCase() === "user") {
|
|
466
|
+
return msg;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return void 0;
|
|
470
|
+
}
|
|
471
|
+
function createMiddleware(firewall, options = {}) {
|
|
472
|
+
const scanInput = options.scanInput ?? true;
|
|
473
|
+
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
|
+
};
|
|
485
|
+
const classifyOrBlock = async (text, hook, toolName) => {
|
|
486
|
+
if (!text.trim()) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const result = await firewall.classify(
|
|
490
|
+
text,
|
|
491
|
+
toolName !== void 0 ? { hook, toolName } : { hook }
|
|
492
|
+
);
|
|
493
|
+
const threshold = effectiveThreshold(hook);
|
|
494
|
+
const blocked = result.score >= threshold;
|
|
495
|
+
options.onClassify?.({
|
|
496
|
+
hook,
|
|
497
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
498
|
+
text,
|
|
499
|
+
result,
|
|
500
|
+
blocked
|
|
501
|
+
});
|
|
502
|
+
if (blocked) {
|
|
503
|
+
const err = new PromptBlockedException({
|
|
504
|
+
score: result.score,
|
|
505
|
+
threshold,
|
|
506
|
+
promptText: text
|
|
507
|
+
});
|
|
508
|
+
options.onBlocked?.(err);
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
const scanPrompt = async (prompt) => {
|
|
513
|
+
if (prompt.length === 0) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const last = prompt[prompt.length - 1];
|
|
517
|
+
const role = last.role.toLowerCase();
|
|
518
|
+
if (role === "tool") {
|
|
519
|
+
for (const { text: text2, toolName } of iterateToolResultParts(last)) {
|
|
520
|
+
await classifyOrBlock(text2, HookLabel.TOOL_RESPONSE, toolName);
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const lastUser = findLastUserMessage(prompt);
|
|
525
|
+
if (!lastUser) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const text = extractContentText(lastUser.content).trim();
|
|
529
|
+
if (!text) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
await classifyOrBlock(text, HookLabel.USER_INPUT);
|
|
533
|
+
};
|
|
534
|
+
const scanGenerateResult = async (result) => {
|
|
535
|
+
if (scanOutput && typeof result.text === "string" && result.text.length > 0) {
|
|
536
|
+
await classifyOrBlock(result.text, HookLabel.LLM_OUTPUT);
|
|
537
|
+
}
|
|
538
|
+
if (options.scanToolCalls && Array.isArray(result.toolCalls)) {
|
|
539
|
+
for (const call of result.toolCalls) {
|
|
540
|
+
if (!call || typeof call !== "object") {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const args = typeof call.args === "string" ? call.args : stringifyToolValue(call.args);
|
|
544
|
+
const toolName = typeof call.toolName === "string" ? call.toolName : void 0;
|
|
545
|
+
if (args.trim()) {
|
|
546
|
+
await classifyOrBlock(args, HookLabel.TOOL_CALL, toolName);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
return {
|
|
552
|
+
async wrapGenerate({ params, doGenerate }) {
|
|
553
|
+
if (scanInput) {
|
|
554
|
+
await scanPrompt(params.prompt);
|
|
555
|
+
}
|
|
556
|
+
const result = await doGenerate();
|
|
557
|
+
await scanGenerateResult(result);
|
|
558
|
+
return result;
|
|
559
|
+
},
|
|
560
|
+
async wrapStream({ params, doStream }) {
|
|
561
|
+
if (scanInput) {
|
|
562
|
+
await scanPrompt(params.prompt);
|
|
563
|
+
}
|
|
564
|
+
const { stream, ...rest } = await doStream();
|
|
565
|
+
if (!scanOutput) {
|
|
566
|
+
return { stream, ...rest };
|
|
567
|
+
}
|
|
568
|
+
let buffered = "";
|
|
569
|
+
const transformed = stream.pipeThrough(
|
|
570
|
+
new TransformStream({
|
|
571
|
+
transform(chunk, controller) {
|
|
572
|
+
const part = chunk;
|
|
573
|
+
if (part && part.type === "text-delta") {
|
|
574
|
+
const delta = part.textDelta ?? part.delta ?? "";
|
|
575
|
+
buffered += delta;
|
|
576
|
+
}
|
|
577
|
+
controller.enqueue(chunk);
|
|
578
|
+
},
|
|
579
|
+
async flush(controller) {
|
|
580
|
+
if (!buffered.trim()) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
await classifyOrBlock(buffered, HookLabel.LLM_OUTPUT);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
if (err instanceof PromptBlockedException) {
|
|
587
|
+
controller.enqueue({ type: "error", error: err });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
throw err;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
return { stream: transformed, ...rest };
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/chunking.ts
|
|
601
|
+
var CHARS_PER_TOKEN = 4;
|
|
602
|
+
var MAX_INPUT_TOKENS = 10240;
|
|
603
|
+
var CHUNK_WINDOW = 400;
|
|
604
|
+
var CHUNK_OVERLAP = 64;
|
|
605
|
+
var MAX_INPUT_CHARS = MAX_INPUT_TOKENS * CHARS_PER_TOKEN;
|
|
606
|
+
var CHUNK_WINDOW_CHARS = CHUNK_WINDOW * CHARS_PER_TOKEN;
|
|
607
|
+
var CHUNK_OVERLAP_CHARS = CHUNK_OVERLAP * CHARS_PER_TOKEN;
|
|
608
|
+
var CHUNK_STRIDE_CHARS = CHUNK_WINDOW_CHARS - CHUNK_OVERLAP_CHARS;
|
|
609
|
+
function chunkText(text) {
|
|
610
|
+
const n = text.length;
|
|
611
|
+
if (n > MAX_INPUT_CHARS) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Input has ~${Math.floor(n / CHARS_PER_TOKEN)} tokens (${n} chars); max is ${MAX_INPUT_TOKENS} tokens (${MAX_INPUT_CHARS} chars)`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (n <= CHUNK_WINDOW_CHARS) {
|
|
617
|
+
return [text];
|
|
618
|
+
}
|
|
619
|
+
const chunks = [];
|
|
620
|
+
for (let start = 0; start < n; start += CHUNK_STRIDE_CHARS) {
|
|
621
|
+
const end = start + CHUNK_WINDOW_CHARS;
|
|
622
|
+
chunks.push(text.slice(start, end));
|
|
623
|
+
if (end >= n) {
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return chunks;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/firewall.ts
|
|
631
|
+
init_exceptions();
|
|
632
|
+
var DEFAULT_THRESHOLD = 0.5;
|
|
633
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
634
|
+
var DEFAULT_MAX_RETRIES = 5;
|
|
635
|
+
var MAX_BACKOFF_SECONDS = 30;
|
|
636
|
+
var Firewall = class {
|
|
637
|
+
apiKey;
|
|
638
|
+
apiUrl;
|
|
639
|
+
threshold;
|
|
640
|
+
timeoutMs;
|
|
641
|
+
hookThresholds;
|
|
642
|
+
headers;
|
|
643
|
+
constructor(options) {
|
|
644
|
+
if (!options.apiKey) {
|
|
645
|
+
throw new Error("Firewall: apiKey is required");
|
|
646
|
+
}
|
|
647
|
+
if (!options.apiUrl) {
|
|
648
|
+
throw new Error("Firewall: apiUrl is required");
|
|
649
|
+
}
|
|
650
|
+
this.apiKey = options.apiKey;
|
|
651
|
+
this.apiUrl = options.apiUrl;
|
|
652
|
+
this.threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
653
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
654
|
+
this.hookThresholds = options.hookThresholds ?? {};
|
|
655
|
+
this.headers = Object.freeze({
|
|
656
|
+
"x-api-key": this.apiKey,
|
|
657
|
+
"content-type": "application/json"
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
async classify(text, options = {}) {
|
|
661
|
+
const chunks = chunkText(text);
|
|
662
|
+
if (chunks.length === 1) {
|
|
663
|
+
const payload = { text: chunks[0] };
|
|
664
|
+
if (options.hook !== void 0) {
|
|
665
|
+
payload.hook = options.hook;
|
|
666
|
+
}
|
|
667
|
+
if (options.toolName !== void 0) {
|
|
668
|
+
payload.tool_name = options.toolName;
|
|
669
|
+
}
|
|
670
|
+
const data = await this.postWithRetry(payload);
|
|
671
|
+
return Object.freeze({ prediction: data.prediction, score: Number(data.score) });
|
|
672
|
+
}
|
|
673
|
+
const batchOpts = {};
|
|
674
|
+
if (options.hook !== void 0) {
|
|
675
|
+
batchOpts.hooks = chunks.map(() => options.hook);
|
|
676
|
+
}
|
|
677
|
+
if (options.toolName !== void 0) {
|
|
678
|
+
batchOpts.toolNames = chunks.map(() => options.toolName);
|
|
679
|
+
}
|
|
680
|
+
const results = await this.classifyBatch(chunks, batchOpts);
|
|
681
|
+
return results.reduce((best, r) => r.score > best.score ? r : best);
|
|
682
|
+
}
|
|
683
|
+
async classifyBatch(texts, options = {}) {
|
|
684
|
+
const payload = { texts };
|
|
685
|
+
if (options.hooks && options.hooks.length > 0) {
|
|
686
|
+
payload.hooks = options.hooks.map((h) => String(h));
|
|
687
|
+
}
|
|
688
|
+
if (options.toolNames && options.toolNames.length > 0) {
|
|
689
|
+
payload.tool_names = options.toolNames.map((t) => t === void 0 ? null : t);
|
|
690
|
+
}
|
|
691
|
+
const data = await this.postWithRetry(payload);
|
|
692
|
+
return data.predictions.map(
|
|
693
|
+
(p) => Object.freeze({ prediction: p.prediction, score: Number(p.score) })
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
asLangChainHandler(options = {}) {
|
|
697
|
+
return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then((m) => m.createLangChainHandler(this, options));
|
|
698
|
+
}
|
|
699
|
+
asMiddleware(options = {}) {
|
|
700
|
+
return createMiddleware(this, options);
|
|
701
|
+
}
|
|
702
|
+
effectiveThreshold(hook) {
|
|
703
|
+
if (hook === void 0) {
|
|
704
|
+
return this.threshold;
|
|
705
|
+
}
|
|
706
|
+
return this.hookThresholds[hook] ?? this.threshold;
|
|
707
|
+
}
|
|
708
|
+
async postWithRetry(payload, maxRetries = DEFAULT_MAX_RETRIES) {
|
|
709
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
710
|
+
const response = await fetch(this.apiUrl, {
|
|
711
|
+
method: "POST",
|
|
712
|
+
headers: this.headers,
|
|
713
|
+
body: JSON.stringify(payload),
|
|
714
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
715
|
+
});
|
|
716
|
+
if (response.status !== 429 || attempt === maxRetries) {
|
|
717
|
+
if (!response.ok) {
|
|
718
|
+
const body = await response.text().catch(() => "");
|
|
719
|
+
throw new SilmarilApiError({
|
|
720
|
+
status: response.status,
|
|
721
|
+
statusText: response.statusText,
|
|
722
|
+
body
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return await response.json();
|
|
726
|
+
}
|
|
727
|
+
const waitSeconds = Math.min(2 ** attempt, MAX_BACKOFF_SECONDS);
|
|
728
|
+
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1e3));
|
|
729
|
+
}
|
|
730
|
+
throw new Error("Firewall: exhausted retries (unreachable)");
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// src/index.ts
|
|
735
|
+
init_exceptions();
|
|
736
|
+
init_hooks();
|
|
737
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
738
|
+
0 && (module.exports = {
|
|
739
|
+
ALL_HOOKS,
|
|
740
|
+
CHUNK_OVERLAP,
|
|
741
|
+
CHUNK_OVERLAP_CHARS,
|
|
742
|
+
CHUNK_WINDOW,
|
|
743
|
+
CHUNK_WINDOW_CHARS,
|
|
744
|
+
DEFAULT_HOOKS,
|
|
745
|
+
DEFAULT_HOOK_THRESHOLDS,
|
|
746
|
+
DEFAULT_THRESHOLD,
|
|
747
|
+
DEFAULT_TIMEOUT_MS,
|
|
748
|
+
FIREWALL_HOOK_TO_LABEL,
|
|
749
|
+
Firewall,
|
|
750
|
+
FirewallHook,
|
|
751
|
+
HookLabel,
|
|
752
|
+
INPUT_HOOKS,
|
|
753
|
+
MAX_INPUT_CHARS,
|
|
754
|
+
MAX_INPUT_TOKENS,
|
|
755
|
+
OUTPUT_HOOKS,
|
|
756
|
+
PromptBlockedException,
|
|
757
|
+
SilmarilApiError,
|
|
758
|
+
chunkText,
|
|
759
|
+
prependHook,
|
|
760
|
+
prependToolName,
|
|
761
|
+
resolveHooks
|
|
762
|
+
});
|
|
763
|
+
//# sourceMappingURL=index.cjs.map
|