@silmaril-security/sdk 0.3.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Silmaril Firewall TypeScript SDK are documented here.
4
+
5
+ ## 0.4.2 - 2026-06-02
6
+
7
+ - Add typed firewall outcome constants, ordered outcome lists, descriptions,
8
+ type guards, and response normalizers.
9
+ - Type `BlockResult.primaryOutcome`, `outcomeScores`, `detectorScores`, and
10
+ `detectorCounts` around the canonical outcome taxonomy.
11
+ - Document simple outcome routing examples for direct `classify()` results.
12
+
13
+ ## 0.4.1 - 2026-05-24
14
+
15
+ - Recover the `0.4.x` npm release by moving to `0.4.1`. The `v0.4.0` Git tag
16
+ exists, but `@silmaril-security/sdk@0.4.0` was never published to npm.
17
+ - Support Vercel AI SDK v5 and v6 with the `ai` peer range
18
+ `^5.0.0 || ^6.0.0`.
19
+ - Keep optional LangChain.js types out of the root package declarations so core
20
+ consumers can typecheck without installing `@langchain/core`.
21
+ - Add typed ESM and CommonJS export conditions, plus explicit adapter subpath
22
+ exports for `@silmaril-security/sdk/adapters/vercel` and
23
+ `@silmaril-security/sdk/adapters/langchain`.
24
+ - Add public repository hygiene docs for contribution and vulnerability
25
+ reporting.
26
+
27
+ ## 0.4.0 - Skipped on npm
28
+
29
+ - Do not install this version. The Git tag exists, but the npm package was not
30
+ published.
package/README.md CHANGED
@@ -18,9 +18,10 @@ This SDK provides the low-level TypeScript interface for that workflow:
18
18
  - Classify user input, tool calls, tool responses, model output, or system
19
19
  prompt content.
20
20
  - Preserve hook and tool-name context for more accurate decisions.
21
- - Enforce automatic adaptive thresholds in adapters, with shadow mode for
21
+ - Enforce backend-owned adaptive thresholds in adapters, with shadow mode for
22
22
  observation-only rollout.
23
23
  - Chunk long inputs consistently before they reach the API.
24
+ - Send SDK metadata that lets the Firewall reconstruct chunked payloads.
24
25
  - Retry API rate-limit responses.
25
26
  - Optionally attach the firewall to Vercel AI SDK middleware and LangChain.js
26
27
  callback flows.
@@ -36,16 +37,21 @@ npm install @silmaril-security/sdk
36
37
  For reproducible installs, pin a tagged release:
37
38
 
38
39
  ```sh
39
- npm install @silmaril-security/sdk@0.3.1
40
+ npm install @silmaril-security/sdk@0.4.2
40
41
  ```
41
42
 
42
43
  Requires Node 18 or later.
43
44
 
44
45
  The package name and SDK import path are both `@silmaril-security/sdk`, so call
45
- sites use `Firewall`, `HookLabel`, and `PromptBlockedException` from that
46
- package.
46
+ sites use `Firewall`, `HookLabel`, and `FirewallBlockedException` from that
47
+ package. `PromptBlockedException` is a deprecated alias.
47
48
 
48
- Optional Vercel AI SDK middleware support:
49
+ This repository is public and source-available for integration transparency,
50
+ but the SDK is not permissive open source. See [LICENSE](LICENSE) for the
51
+ governing terms.
52
+
53
+ The core client does not require optional framework peer dependencies. Optional
54
+ Vercel AI SDK middleware support is compatible with Vercel AI SDK v5 and v6:
49
55
 
50
56
  ```sh
51
57
  npm install ai @ai-sdk/openai
@@ -109,7 +115,53 @@ console.log(`tool output: ${toolResult.prediction} ${toolResult.score.toFixed(4)
109
115
  `classify()` and `classifyBatch()` return the server's prediction, score, and
110
116
  internally applied threshold. Direct calls do not throw on malicious verdicts.
111
117
  The Vercel AI SDK and LangChain.js adapters use `result.threshold` and throw
112
- `PromptBlockedException` when enforcement is enabled.
118
+ `FirewallBlockedException` when enforcement is enabled.
119
+
120
+ ## Handle Outcomes
121
+
122
+ Direct calls expose typed outcome labels so applications can choose different
123
+ responses for different firewall decisions:
124
+
125
+ ```ts
126
+ import { Firewall, HookLabel, Outcome } from "@silmaril-security/sdk";
127
+
128
+ const result = await fw.classify(userInput, {
129
+ hook: HookLabel.USER_INPUT,
130
+ });
131
+
132
+ if (result.score < result.threshold) {
133
+ continueNormally();
134
+ } else {
135
+ switch (result.primaryOutcome) {
136
+ case Outcome.SecretExposure:
137
+ redactAndSuppress(result);
138
+ break;
139
+ case Outcome.InformationDisclosure:
140
+ requireReview(result);
141
+ break;
142
+ case Outcome.ControlAbuse:
143
+ denyAndAskForConfirmation(result);
144
+ break;
145
+ case Outcome.SystemCompromise:
146
+ blockAndEscalate(result);
147
+ break;
148
+ case Outcome.ServiceDisruption:
149
+ blockDisruptiveAction(result);
150
+ break;
151
+ default:
152
+ blockByDefault(result);
153
+ }
154
+ }
155
+ ```
156
+
157
+ Outcome taxonomy:
158
+
159
+ - `benign`: no harmful firewall outcome detected.
160
+ - `information_disclosure`: private data, documents, internal context, logs, traces, customer data, SQL rows, topology, or similar non-secret sensitive information.
161
+ - `secret_exposure`: credentials, tokens, API keys, cookies, passwords, signing keys, OAuth secrets, session material, or webhook secrets.
162
+ - `control_abuse`: misuse of authorized tools or user privileges to send, change, approve, delete, operate, or bypass policy/RBAC without a stronger outcome.
163
+ - `system_compromise`: privilege escalation, account takeover, hostile integration/plugin takeover, persistence, lateral movement, attacker webhook registration, or code/plugin execution.
164
+ - `service_disruption`: downtime, lockout, degradation, alert suppression, destructive loops, resource exhaustion, cost spikes, or hidden outage evidence.
113
165
 
114
166
  ## Options
115
167
 
@@ -126,24 +178,26 @@ interface FirewallOptions {
126
178
  The SDK uses native `fetch`, `AbortSignal.timeout`, and JSON request bodies with
127
179
  `x-api-key` and `content-type` headers.
128
180
 
129
- ## Automatic Thresholding
181
+ ## Backend Thresholding
130
182
 
131
- Customers do not tune score thresholds. Short inputs use the base threshold
132
- `0.5`, which corresponds to the SDK's default single-chunk operating point.
133
- When a call creates more scoring opportunities, the SDK raises the internal
134
- threshold before sending requests to `/classify`: 2 chunks use about `0.6661`,
135
- 5 chunks use about `0.8328`, and 10 or more opportunities are capped at `0.9`.
183
+ Customers do not tune score thresholds in the SDK. Tenant Firewall config owns
184
+ the adaptive threshold schedule. The default backend config is
185
+ `base_threshold=0.5`, `target_sequence_fpr=0.01`, and
186
+ `max_adaptive_threshold=0.9`, which keeps the current schedule: 1 scoring
187
+ opportunity uses `0.5`, 2 use about `0.6661`, 5 use about `0.8328`, and 10 or
188
+ more are capped at `0.9`.
136
189
 
137
- For `classify()`, the scoring-opportunity count is the number of generated
138
- chunks. For `classifyBatch()`, it is the number of texts in the batch. The
139
- applied value remains available on `BlockResult.threshold` and
140
- `PromptBlockedException.threshold` as diagnostic metadata.
190
+ The SDK no longer sends `threshold` in request payloads. It sends chunk
191
+ metadata instead, and the backend combines tenant config, active batch size,
192
+ and chunk count to decide the threshold. The applied value remains available on
193
+ `BlockResult.threshold` and `FirewallBlockedException.threshold` as diagnostic
194
+ metadata.
141
195
 
142
196
  ## Shadow Mode
143
197
 
144
198
  The Vercel AI SDK and LangChain.js adapters enforce thresholds by default.
145
199
  Shadow mode keeps the same classification and threshold logic but suppresses
146
- `PromptBlockedException`, so live traffic can continue while telemetry records
200
+ `FirewallBlockedException`, so live traffic can continue while telemetry records
147
201
  what would have blocked:
148
202
 
149
203
  ```ts
@@ -175,8 +229,8 @@ const model = wrapLanguageModel({
175
229
  await generateText({ model, prompt: "Hello" });
176
230
  ```
177
231
 
178
- Per-adapter overrides let you enforce or shadow one surface without changing the
179
- client default:
232
+ Per-adapter `shadowMode` options let you enforce or shadow one surface without
233
+ changing the client default:
180
234
 
181
235
  ```ts
182
236
  fw.asMiddleware({
@@ -188,10 +242,10 @@ await fw.asLangChainHandler({
188
242
  });
189
243
  ```
190
244
 
191
- `ClassifyEvent` includes `hook`, `toolName`, `text`, `result`, `blocked`, and
192
- `shadowMode`. `blocked` is computed from `result.score >= result.threshold`.
193
- Direct `classify()` and `classifyBatch()` calls never throw on verdicts and are
194
- unaffected by shadow mode.
245
+ `ClassifyEvent` includes `hook`, `toolName`, `toolCallId`, `runId`, `text`,
246
+ `result`, `blocked`, and `shadowMode`. `blocked` is computed from
247
+ `result.score >= result.threshold`. Direct `classify()` and `classifyBatch()`
248
+ calls never throw on verdicts and are unaffected by shadow mode.
195
249
 
196
250
  ## Hook Labels
197
251
 
@@ -209,10 +263,39 @@ text-prefix integrations. `classify()` and `classifyBatch()` send hook and tool
209
263
  metadata as structured JSON fields, so normal callers should use the `hook`,
210
264
  `toolName`, `hooks`, and `toolNames` options.
211
265
 
266
+ ## Request Metadata
267
+
268
+ Use `metadata` to forward application or integration identifiers to the
269
+ classification API without embedding them in the classified text:
270
+
271
+ ```ts
272
+ await fw.classify(text, {
273
+ hook: HookLabel.USER_INPUT,
274
+ metadata: {
275
+ langgraph: {
276
+ thread_id: "customer-thread-123",
277
+ run_id: "langgraph-run-456",
278
+ message_id: "message-789",
279
+ },
280
+ },
281
+ });
282
+ ```
283
+
284
+ The SDK preserves caller metadata and adds a reserved `metadata.silmaril`
285
+ namespace to every request. SDK-controlled fields are `sdk_language`,
286
+ `sdk_version`, `request_id`, `input_index`, `chunk_index`, and `chunk_count`.
287
+ Single unchunked requests use `input_index=0`, `chunk_index=0`, and
288
+ `chunk_count=1`; batches use one metadata object per input; chunked requests
289
+ reuse a single request id across all chunks. If callers provide
290
+ `metadata.silmaril`, it must be an object and SDK-reserved keys are overwritten
291
+ by the SDK.
292
+
212
293
  ## Errors
213
294
 
214
295
  - `SilmarilApiError`: thrown when the firewall API responds with a non-2xx or redirect status. Carries `status`, `statusText`, a 64 KiB-capped `body`, and any parsed malformed-input diagnostics. The default error message omits the body to keep logs clean.
215
- - `PromptBlockedException`: thrown by the Vercel AI SDK and LangChain.js adapters in enforcement mode when the score meets or exceeds the effective threshold. Carries `score`, `threshold`, `promptText`, and optional `runId`.
296
+ - `FirewallBlockedException`: thrown by the Vercel AI SDK and LangChain.js adapters in enforcement mode when the backend blocks the request. Carries `score`, `threshold`, `promptText`, and optional `runId`, `hook`, `toolName`, `toolCallId`, and `result`.
297
+
298
+ `PromptBlockedException` remains as a deprecated alias for one release.
216
299
 
217
300
  All SDK exception types extend `Error` and work with `instanceof`.
218
301
 
@@ -252,14 +335,19 @@ console.log(`classified ${results.length} items`);
252
335
 
253
336
  Batch requests preserve result order and can carry per-item hooks, tool names,
254
337
  and metadata. Hook, tool-name, and metadata arrays must match the number of
255
- texts. Each batch carries one internal threshold based on batch size.
338
+ texts. Each batch carries SDK metadata per item so the backend can apply
339
+ tenant-owned thresholding.
256
340
 
257
341
  ## Migration Notes
258
342
 
259
- Version `0.3.0` removes customer-facing `threshold` and `hookThresholds`
260
- configuration from the client, Vercel middleware, and LangChain adapter.
261
- Existing hook metadata, shadow mode, result threshold diagnostics, and typed
262
- blocking exceptions remain available.
343
+ Version `0.4.1` is the public npm recovery release for `0.4.x`: the `v0.4.0`
344
+ Git tag exists, but `@silmaril-security/sdk@0.4.0` was not published to npm.
345
+ Use `0.4.1` or later. This release moves all threshold decisions to Firewall
346
+ tenant/backend config, adds SDK reconstruction metadata, renames blocking
347
+ exceptions to `FirewallBlockedException`, keeps optional LangChain.js types out
348
+ of the root package declarations, adds typed CJS/ESM export conditions, supports
349
+ Vercel AI SDK v5 and v6. The deprecated
350
+ `PromptBlockedException` alias remains available for one release.
263
351
 
264
352
  ## Vercel AI SDK Middleware
265
353
 
@@ -316,11 +404,22 @@ await model.invoke("Hello");
316
404
 
317
405
  The LangChain handler is fail-open by default: infrastructure errors are logged
318
406
  and the LLM call proceeds. Set `failOpen: false` to make API errors bubble up.
319
- Blocking decisions still throw `PromptBlockedException` unless shadow mode is
320
- enabled.
407
+ Blocking decisions still throw `FirewallBlockedException` unless shadow mode is
408
+ enabled. `PromptBlockedException` continues to work as a deprecated alias for
409
+ one release.
321
410
 
322
411
  `asLangChainHandler()` is async because it lazy-loads `@langchain/core` so core
323
- users do not pay for it.
412
+ users do not pay for it. The root package intentionally exposes a structural
413
+ handler type so projects that do not install LangChain.js can still typecheck
414
+ with `skipLibCheck: false`. If your project needs the exact LangChain
415
+ `BaseCallbackHandler` return type, import the adapter helper from the optional
416
+ subpath:
417
+
418
+ ```ts
419
+ import { createLangChainHandler } from "@silmaril-security/sdk/adapters/langchain";
420
+
421
+ const handler = await createLangChainHandler(fw);
422
+ ```
324
423
 
325
424
  ## Retries
326
425
 
@@ -0,0 +1,349 @@
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 __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/adapters/langchain.ts
31
+ var langchain_exports = {};
32
+ __export(langchain_exports, {
33
+ createLangChainHandler: () => createLangChainHandler
34
+ });
35
+ module.exports = __toCommonJS(langchain_exports);
36
+
37
+ // src/exceptions.ts
38
+ var MAX_PROMPT_DISPLAY_LEN = 100;
39
+ var FirewallBlockedException = class _FirewallBlockedException extends Error {
40
+ score;
41
+ threshold;
42
+ promptText;
43
+ runId;
44
+ hook;
45
+ toolName;
46
+ toolCallId;
47
+ result;
48
+ constructor(params) {
49
+ super(_FirewallBlockedException.formatMessage(params));
50
+ this.name = "FirewallBlockedException";
51
+ this.score = params.score;
52
+ this.threshold = params.threshold;
53
+ this.promptText = params.promptText;
54
+ this.runId = params.runId;
55
+ this.hook = params.hook;
56
+ this.toolName = params.toolName;
57
+ this.toolCallId = params.toolCallId;
58
+ this.result = params.result;
59
+ Object.setPrototypeOf(this, _FirewallBlockedException.prototype);
60
+ }
61
+ static formatMessage(params) {
62
+ const truncated = params.promptText.length > MAX_PROMPT_DISPLAY_LEN ? `${params.promptText.slice(0, MAX_PROMPT_DISPLAY_LEN)}...` : params.promptText;
63
+ return `Request blocked by Silmaril Firewall (score=${params.score.toFixed(4)}, threshold=${params.threshold.toFixed(4)}): '${truncated}'`;
64
+ }
65
+ };
66
+
67
+ // src/hooks.ts
68
+ var HookLabel = {
69
+ USER_INPUT: "user_input",
70
+ SYSTEM_PROMPT: "system_prompt",
71
+ TOOL_CALL: "tool_call",
72
+ TOOL_RESPONSE: "tool_response",
73
+ LLM_OUTPUT: "llm_output",
74
+ UNKNOWN: "unknown"
75
+ };
76
+ var FirewallHook = {
77
+ LLM_START: "on_llm_start",
78
+ CHAT_MODEL_START: "on_chat_model_start",
79
+ TOOL_START: "on_tool_start",
80
+ RETRIEVER_START: "on_retriever_start",
81
+ LLM_END: "on_llm_end",
82
+ TOOL_END: "on_tool_end",
83
+ RETRIEVER_END: "on_retriever_end"
84
+ };
85
+ var DEFAULT_HOOKS = /* @__PURE__ */ new Set([
86
+ FirewallHook.LLM_START,
87
+ FirewallHook.CHAT_MODEL_START
88
+ ]);
89
+ var INPUT_HOOKS = /* @__PURE__ */ new Set([
90
+ FirewallHook.LLM_START,
91
+ FirewallHook.CHAT_MODEL_START,
92
+ FirewallHook.TOOL_START,
93
+ FirewallHook.RETRIEVER_START
94
+ ]);
95
+ var OUTPUT_HOOKS = /* @__PURE__ */ new Set([
96
+ FirewallHook.LLM_END,
97
+ FirewallHook.TOOL_END,
98
+ FirewallHook.RETRIEVER_END
99
+ ]);
100
+ var ALL_HOOKS = /* @__PURE__ */ new Set([
101
+ ...INPUT_HOOKS,
102
+ ...OUTPUT_HOOKS
103
+ ]);
104
+ var FIREWALL_HOOK_TO_LABEL = {
105
+ [FirewallHook.CHAT_MODEL_START]: HookLabel.USER_INPUT,
106
+ [FirewallHook.LLM_START]: HookLabel.USER_INPUT,
107
+ [FirewallHook.TOOL_START]: HookLabel.TOOL_CALL,
108
+ [FirewallHook.TOOL_END]: HookLabel.TOOL_RESPONSE,
109
+ [FirewallHook.RETRIEVER_START]: HookLabel.TOOL_CALL,
110
+ [FirewallHook.RETRIEVER_END]: HookLabel.TOOL_RESPONSE,
111
+ [FirewallHook.LLM_END]: HookLabel.LLM_OUTPUT
112
+ };
113
+ var FIREWALL_HOOK_VALUES = new Set(Object.values(FirewallHook));
114
+ function resolveHooks(hooks) {
115
+ if (hooks === void 0) {
116
+ return DEFAULT_HOOKS;
117
+ }
118
+ const resolved = /* @__PURE__ */ new Set();
119
+ for (const h of hooks) {
120
+ if (!FIREWALL_HOOK_VALUES.has(h)) {
121
+ throw new Error(`Invalid FirewallHook value: ${String(h)}`);
122
+ }
123
+ resolved.add(h);
124
+ }
125
+ return resolved;
126
+ }
127
+
128
+ // src/utils/extract.ts
129
+ function extractTextFromPrompts(prompts) {
130
+ return prompts.map((p) => p.trim()).filter((p) => p.length > 0).join("\n");
131
+ }
132
+ function extractTextFromToolInput(inputStr) {
133
+ return inputStr.trim();
134
+ }
135
+ function extractTextFromLLMResult(response) {
136
+ const parts = [];
137
+ for (const genList of response.generations ?? []) {
138
+ for (const gen of genList) {
139
+ const text = (gen.text ?? "").trim();
140
+ if (text) {
141
+ parts.push(text);
142
+ }
143
+ }
144
+ }
145
+ return parts.join("\n");
146
+ }
147
+ function extractTextFromDocuments(documents) {
148
+ const parts = [];
149
+ for (const doc of documents) {
150
+ const text = (doc.pageContent ?? "").trim();
151
+ if (text) {
152
+ parts.push(text);
153
+ }
154
+ }
155
+ return parts.join("\n");
156
+ }
157
+
158
+ // src/adapters/langchain.ts
159
+ var USER_ROLES = /* @__PURE__ */ new Set(["human", "user"]);
160
+ function getMessageRole(message) {
161
+ if (typeof message.role === "string") {
162
+ return message.role.toLowerCase();
163
+ }
164
+ if (typeof message.type === "string") {
165
+ return message.type.toLowerCase();
166
+ }
167
+ return "";
168
+ }
169
+ function extractMessageText(content) {
170
+ if (content === void 0) {
171
+ return "";
172
+ }
173
+ if (typeof content === "string") {
174
+ return content;
175
+ }
176
+ const parts = [];
177
+ for (const block of content) {
178
+ if (typeof block === "string") {
179
+ parts.push(block);
180
+ } else if (block && block.type === "text") {
181
+ parts.push(block.text ?? "");
182
+ }
183
+ }
184
+ return parts.join(" ");
185
+ }
186
+ function findLastUserMessage(messages) {
187
+ for (let i = messages.length - 1; i >= 0; i--) {
188
+ const msg = messages[i];
189
+ if (USER_ROLES.has(getMessageRole(msg))) {
190
+ return msg;
191
+ }
192
+ }
193
+ return void 0;
194
+ }
195
+ async function createLangChainHandler(firewall, options = {}) {
196
+ const { BaseCallbackHandler } = await import("@langchain/core/callbacks/base");
197
+ const enabledHooks = resolveHooks(options.hooks);
198
+ const failOpen = options.failOpen ?? true;
199
+ const logger = options.logger ?? ((message, error) => {
200
+ console.warn(`silmaril.firewall: ${message}`, error);
201
+ });
202
+ const shadowMode = options.shadowMode ?? firewall.shadowMode;
203
+ const onClassify = options.onClassify;
204
+ const fireOnClassify = (event) => {
205
+ if (!onClassify) {
206
+ return;
207
+ }
208
+ try {
209
+ onClassify(event);
210
+ } catch (err) {
211
+ logger("onClassify callback threw", err);
212
+ }
213
+ };
214
+ const classify = async (text, hookLabel, runId, toolName) => {
215
+ let result;
216
+ try {
217
+ result = await firewall.classify(text, {
218
+ hook: hookLabel,
219
+ ...toolName !== void 0 ? { toolName } : {}
220
+ });
221
+ } catch (err) {
222
+ if (!failOpen) {
223
+ throw err;
224
+ }
225
+ logger("classification failed, allowing prompt through", err);
226
+ return;
227
+ }
228
+ const threshold = result.threshold;
229
+ const blocked = result.score >= threshold;
230
+ const commonEventFields = {
231
+ hook: hookLabel,
232
+ ...toolName !== void 0 ? { toolName } : {},
233
+ runId,
234
+ text,
235
+ result
236
+ };
237
+ fireOnClassify({
238
+ ...commonEventFields,
239
+ blocked,
240
+ shadowMode
241
+ });
242
+ if (!blocked || shadowMode) {
243
+ return;
244
+ }
245
+ throw new FirewallBlockedException({
246
+ score: result.score,
247
+ threshold,
248
+ promptText: text,
249
+ runId,
250
+ hook: hookLabel,
251
+ ...toolName !== void 0 ? { toolName } : {},
252
+ result
253
+ });
254
+ };
255
+ class SilmarilFirewallHandler extends BaseCallbackHandler {
256
+ name = "silmaril_firewall_handler";
257
+ raiseError = true;
258
+ awaitHandlers = true;
259
+ async handleChatModelStart(_llm, messages, runId) {
260
+ if (!enabledHooks.has(FirewallHook.CHAT_MODEL_START)) {
261
+ return;
262
+ }
263
+ const batches = messages;
264
+ const flat = [];
265
+ for (const batch of batches) {
266
+ for (const m of batch) {
267
+ flat.push(m);
268
+ }
269
+ }
270
+ const lastUser = findLastUserMessage(flat);
271
+ if (!lastUser) {
272
+ return;
273
+ }
274
+ const text = extractMessageText(lastUser.content).trim();
275
+ if (!text) {
276
+ return;
277
+ }
278
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.CHAT_MODEL_START], runId);
279
+ }
280
+ async handleLLMStart(_llm, prompts, runId) {
281
+ if (!enabledHooks.has(FirewallHook.LLM_START)) {
282
+ return;
283
+ }
284
+ const text = extractTextFromPrompts(prompts);
285
+ if (!text) {
286
+ return;
287
+ }
288
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_START], runId);
289
+ }
290
+ async handleToolStart(tool, inputStr, runId) {
291
+ if (!enabledHooks.has(FirewallHook.TOOL_START)) {
292
+ return;
293
+ }
294
+ const text = extractTextFromToolInput(inputStr);
295
+ if (!text) {
296
+ return;
297
+ }
298
+ const toolName = tool?.name;
299
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
300
+ }
301
+ async handleRetrieverStart(_retriever, query, runId) {
302
+ if (!enabledHooks.has(FirewallHook.RETRIEVER_START)) {
303
+ return;
304
+ }
305
+ const text = query.trim();
306
+ if (!text) {
307
+ return;
308
+ }
309
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_START], runId);
310
+ }
311
+ async handleLLMEnd(output, runId) {
312
+ if (!enabledHooks.has(FirewallHook.LLM_END)) {
313
+ return;
314
+ }
315
+ const text = extractTextFromLLMResult(output);
316
+ if (!text) {
317
+ return;
318
+ }
319
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_END], runId);
320
+ }
321
+ async handleToolEnd(output, runId, _parentRunId, _tags, _kwargs) {
322
+ if (!enabledHooks.has(FirewallHook.TOOL_END)) {
323
+ return;
324
+ }
325
+ const text = String(output).trim();
326
+ if (!text) {
327
+ return;
328
+ }
329
+ const toolName = _kwargs?.name;
330
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
331
+ }
332
+ async handleRetrieverEnd(documents, runId) {
333
+ if (!enabledHooks.has(FirewallHook.RETRIEVER_END)) {
334
+ return;
335
+ }
336
+ const text = extractTextFromDocuments(documents);
337
+ if (!text) {
338
+ return;
339
+ }
340
+ await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_END], runId);
341
+ }
342
+ }
343
+ return new SilmarilFirewallHandler();
344
+ }
345
+ // Annotate the CommonJS export names for ESM import in node:
346
+ 0 && (module.exports = {
347
+ createLangChainHandler
348
+ });
349
+ //# sourceMappingURL=langchain.cjs.map