ai-shield-openai 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/package.json +28 -0
- package/src/index.ts +52 -0
- package/src/wrapper.ts +422 -0
- package/tsconfig.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-shield-openai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI Shield wrapper for OpenAI SDK — automatic input/output scanning",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"openai": ">=4.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"ai-shield-core": "0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"openai": "^4.77.0",
|
|
26
|
+
"typescript": "^5.7.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// @ai-shield/openai — Public API
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
ShieldedOpenAI,
|
|
7
|
+
ShieldedChatStream,
|
|
8
|
+
ShieldBlockError,
|
|
9
|
+
ShieldBudgetError,
|
|
10
|
+
type ShieldedOpenAIConfig,
|
|
11
|
+
type ChatCompletionChunk,
|
|
12
|
+
} from "./wrapper.js";
|
|
13
|
+
|
|
14
|
+
// Re-export core types for convenience
|
|
15
|
+
export type {
|
|
16
|
+
ShieldConfig,
|
|
17
|
+
ScanResult,
|
|
18
|
+
ScanContext,
|
|
19
|
+
} from "ai-shield-core";
|
|
20
|
+
|
|
21
|
+
// --- Convenience factory ---
|
|
22
|
+
|
|
23
|
+
import type { ShieldedOpenAIConfig } from "./wrapper.js";
|
|
24
|
+
import { ShieldedOpenAI } from "./wrapper.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wrap an OpenAI client with AI Shield protection.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import OpenAI from "openai";
|
|
32
|
+
* import { createShield } from "@ai-shield/openai";
|
|
33
|
+
*
|
|
34
|
+
* const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
35
|
+
* const shielded = createShield(openai, {
|
|
36
|
+
* agentId: "chatbot",
|
|
37
|
+
* shield: { pii: { action: "mask", locale: "de-DE" } },
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* // Every call is automatically scanned
|
|
41
|
+
* const response = await shielded.createChatCompletion({
|
|
42
|
+
* model: "gpt-4o",
|
|
43
|
+
* messages: [{ role: "user", content: userInput }],
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function createShield(
|
|
48
|
+
client: ConstructorParameters<typeof ShieldedOpenAI>[0],
|
|
49
|
+
config?: ShieldedOpenAIConfig,
|
|
50
|
+
): ShieldedOpenAI {
|
|
51
|
+
return new ShieldedOpenAI(client, config);
|
|
52
|
+
}
|
package/src/wrapper.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import type { AIShield, ShieldConfig, ScanContext, ScanResult } from "ai-shield-core";
|
|
2
|
+
|
|
3
|
+
// ============================================================
|
|
4
|
+
// OpenAI Shield Wrapper — Drop-in replacement
|
|
5
|
+
// Wraps OpenAI SDK, scans input before & output after LLM call
|
|
6
|
+
// Supports both non-streaming and streaming modes
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
export interface ShieldedOpenAIConfig {
|
|
10
|
+
/** AI Shield config (or pass existing AIShield instance) */
|
|
11
|
+
shield?: ShieldConfig;
|
|
12
|
+
/** Pre-created AIShield instance (takes precedence over shield config) */
|
|
13
|
+
shieldInstance?: AIShield;
|
|
14
|
+
/** Agent ID for tool policy / cost tracking */
|
|
15
|
+
agentId?: string;
|
|
16
|
+
/** Custom scan context factory */
|
|
17
|
+
contextFactory?: (messages: ChatMessage[]) => ScanContext;
|
|
18
|
+
/** Whether to scan output (response) too — default: false */
|
|
19
|
+
scanOutput?: boolean;
|
|
20
|
+
/** Callback when input is blocked */
|
|
21
|
+
onBlocked?: (result: ScanResult, messages: ChatMessage[]) => void;
|
|
22
|
+
/** Callback when input has warnings */
|
|
23
|
+
onWarning?: (result: ScanResult, messages: ChatMessage[]) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ChatMessage {
|
|
27
|
+
role: string;
|
|
28
|
+
content: string | null | Array<{ type: string; text?: string }>;
|
|
29
|
+
tool_calls?: Array<{ function: { name: string; arguments: string } }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ChatCompletionParams {
|
|
33
|
+
model: string;
|
|
34
|
+
messages: ChatMessage[];
|
|
35
|
+
tools?: Array<{ function: { name: string } }>;
|
|
36
|
+
stream?: boolean;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ChatCompletion {
|
|
41
|
+
choices: Array<{
|
|
42
|
+
message: {
|
|
43
|
+
content: string | null;
|
|
44
|
+
tool_calls?: Array<{ function: { name: string; arguments: string } }>;
|
|
45
|
+
};
|
|
46
|
+
}>;
|
|
47
|
+
usage?: {
|
|
48
|
+
prompt_tokens: number;
|
|
49
|
+
completion_tokens: number;
|
|
50
|
+
total_tokens: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ChatCompletionChunk {
|
|
55
|
+
choices: Array<{
|
|
56
|
+
delta: {
|
|
57
|
+
content?: string | null;
|
|
58
|
+
role?: string;
|
|
59
|
+
tool_calls?: Array<{ function: { name: string; arguments: string } }>;
|
|
60
|
+
};
|
|
61
|
+
index: number;
|
|
62
|
+
finish_reason: string | null;
|
|
63
|
+
}>;
|
|
64
|
+
usage?: {
|
|
65
|
+
prompt_tokens: number;
|
|
66
|
+
completion_tokens: number;
|
|
67
|
+
total_tokens: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface OpenAILike {
|
|
72
|
+
chat: {
|
|
73
|
+
completions: {
|
|
74
|
+
create(params: ChatCompletionParams): Promise<ChatCompletion | AsyncIterable<ChatCompletionChunk>>;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class ShieldedOpenAI {
|
|
80
|
+
private client: OpenAILike;
|
|
81
|
+
private shield: AIShield | null = null;
|
|
82
|
+
private shieldConfig: ShieldConfig;
|
|
83
|
+
private config: ShieldedOpenAIConfig;
|
|
84
|
+
private _shieldReady: Promise<AIShield> | null = null;
|
|
85
|
+
|
|
86
|
+
constructor(client: OpenAILike, config: ShieldedOpenAIConfig = {}) {
|
|
87
|
+
this.client = client;
|
|
88
|
+
this.config = config;
|
|
89
|
+
this.shieldConfig = config.shield ?? {};
|
|
90
|
+
|
|
91
|
+
if (config.shieldInstance) {
|
|
92
|
+
this.shield = config.shieldInstance;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Lazy-init shield (avoid import at construction time) */
|
|
97
|
+
private async getShield(): Promise<AIShield> {
|
|
98
|
+
if (this.shield) return this.shield;
|
|
99
|
+
if (this._shieldReady) return this._shieldReady;
|
|
100
|
+
|
|
101
|
+
this._shieldReady = import("ai-shield-core").then((mod) => {
|
|
102
|
+
this.shield = new mod.AIShield(this.shieldConfig);
|
|
103
|
+
return this.shield;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return this._shieldReady;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Build scan context from messages */
|
|
110
|
+
private buildContext(params: ChatCompletionParams): ScanContext {
|
|
111
|
+
if (this.config.contextFactory) {
|
|
112
|
+
return this.config.contextFactory(params.messages);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const context: ScanContext = {};
|
|
116
|
+
if (this.config.agentId) {
|
|
117
|
+
context.agentId = this.config.agentId;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Include tool names if tools are being called
|
|
121
|
+
if (params.tools) {
|
|
122
|
+
context.tools = params.tools.map((t) => ({ name: t.function.name }));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return context;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Extract text content from messages for scanning */
|
|
129
|
+
private extractUserContent(messages: ChatMessage[]): string {
|
|
130
|
+
const parts: string[] = [];
|
|
131
|
+
|
|
132
|
+
for (const msg of messages) {
|
|
133
|
+
// Only scan user messages (not system/assistant)
|
|
134
|
+
if (msg.role !== "user") continue;
|
|
135
|
+
|
|
136
|
+
if (typeof msg.content === "string") {
|
|
137
|
+
parts.push(msg.content);
|
|
138
|
+
} else if (Array.isArray(msg.content)) {
|
|
139
|
+
for (const block of msg.content) {
|
|
140
|
+
if (block.type === "text" && block.text) {
|
|
141
|
+
parts.push(block.text);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return parts.join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Scan input and validate budget — shared between streaming and non-streaming */
|
|
151
|
+
private async scanInput(params: ChatCompletionParams): Promise<{
|
|
152
|
+
shieldInstance: AIShield;
|
|
153
|
+
context: ScanContext;
|
|
154
|
+
userContent: string;
|
|
155
|
+
inputResult: ScanResult;
|
|
156
|
+
finalParams: ChatCompletionParams;
|
|
157
|
+
}> {
|
|
158
|
+
const shieldInstance = await this.getShield();
|
|
159
|
+
const context = this.buildContext(params);
|
|
160
|
+
const userContent = this.extractUserContent(params.messages);
|
|
161
|
+
|
|
162
|
+
// --- Scan input ---
|
|
163
|
+
const inputResult = await shieldInstance.scan(userContent, context);
|
|
164
|
+
|
|
165
|
+
if (inputResult.decision === "block") {
|
|
166
|
+
this.config.onBlocked?.(inputResult, params.messages);
|
|
167
|
+
throw new ShieldBlockError("Input blocked by AI Shield", inputResult);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (inputResult.decision === "warn") {
|
|
171
|
+
this.config.onWarning?.(inputResult, params.messages);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- Replace sanitized content if PII was masked ---
|
|
175
|
+
let finalParams = params;
|
|
176
|
+
if (inputResult.sanitized !== userContent) {
|
|
177
|
+
finalParams = this.replaceUserContent(params, inputResult.sanitized);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Cost pre-check ---
|
|
181
|
+
if (this.config.agentId) {
|
|
182
|
+
const estimate = await shieldInstance.checkBudget(
|
|
183
|
+
this.config.agentId,
|
|
184
|
+
params.model,
|
|
185
|
+
userContent.length * 0.75, // rough token estimate
|
|
186
|
+
);
|
|
187
|
+
if (!estimate.allowed) {
|
|
188
|
+
throw new ShieldBudgetError(
|
|
189
|
+
`Budget exceeded: $${estimate.currentSpend.toFixed(4)} / $${(estimate.currentSpend + estimate.remainingBudget).toFixed(4)}`,
|
|
190
|
+
estimate,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { shieldInstance, context, userContent, inputResult, finalParams };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Create chat completion with Shield protection (non-streaming) */
|
|
199
|
+
async createChatCompletion(
|
|
200
|
+
params: ChatCompletionParams,
|
|
201
|
+
): Promise<ChatCompletion & { _shield?: { input: ScanResult; output?: ScanResult } }> {
|
|
202
|
+
const { shieldInstance, context, inputResult, finalParams } = await this.scanInput(params);
|
|
203
|
+
|
|
204
|
+
// --- Make the actual API call ---
|
|
205
|
+
const response = await this.client.chat.completions.create({ ...finalParams, stream: false }) as ChatCompletion;
|
|
206
|
+
|
|
207
|
+
// --- Record cost ---
|
|
208
|
+
if (this.config.agentId && response.usage) {
|
|
209
|
+
await shieldInstance.recordCost(
|
|
210
|
+
this.config.agentId,
|
|
211
|
+
params.model,
|
|
212
|
+
response.usage.prompt_tokens,
|
|
213
|
+
response.usage.completion_tokens,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Scan output ---
|
|
218
|
+
let outputResult: ScanResult | undefined;
|
|
219
|
+
if (this.config.scanOutput) {
|
|
220
|
+
const outputText = response.choices[0]?.message?.content ?? "";
|
|
221
|
+
if (outputText) {
|
|
222
|
+
outputResult = await shieldInstance.scan(outputText, context);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...response,
|
|
228
|
+
_shield: { input: inputResult, output: outputResult },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Create streaming chat completion with Shield protection */
|
|
233
|
+
async createChatCompletionStream(
|
|
234
|
+
params: Omit<ChatCompletionParams, "stream">,
|
|
235
|
+
): Promise<ShieldedChatStream> {
|
|
236
|
+
const { shieldInstance, context, inputResult, finalParams } =
|
|
237
|
+
await this.scanInput(params as ChatCompletionParams);
|
|
238
|
+
|
|
239
|
+
// --- Make streaming API call ---
|
|
240
|
+
const stream = await this.client.chat.completions.create({
|
|
241
|
+
...finalParams,
|
|
242
|
+
stream: true,
|
|
243
|
+
}) as AsyncIterable<ChatCompletionChunk>;
|
|
244
|
+
|
|
245
|
+
return new ShieldedChatStream(
|
|
246
|
+
stream,
|
|
247
|
+
inputResult,
|
|
248
|
+
shieldInstance,
|
|
249
|
+
context,
|
|
250
|
+
this.config.scanOutput ?? false,
|
|
251
|
+
this.config.agentId,
|
|
252
|
+
finalParams.model,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Replace user message content with sanitized version */
|
|
257
|
+
private replaceUserContent(
|
|
258
|
+
params: ChatCompletionParams,
|
|
259
|
+
sanitized: string,
|
|
260
|
+
): ChatCompletionParams {
|
|
261
|
+
const messages = params.messages.map((msg) => {
|
|
262
|
+
if (msg.role !== "user") return msg;
|
|
263
|
+
|
|
264
|
+
if (typeof msg.content === "string") {
|
|
265
|
+
return { ...msg, content: sanitized };
|
|
266
|
+
}
|
|
267
|
+
// For multi-part content, replace text blocks
|
|
268
|
+
if (Array.isArray(msg.content)) {
|
|
269
|
+
let remaining = sanitized;
|
|
270
|
+
const newContent = msg.content.map((block) => {
|
|
271
|
+
if (block.type === "text" && block.text) {
|
|
272
|
+
const replacement = remaining.substring(0, block.text.length);
|
|
273
|
+
remaining = remaining.substring(block.text.length + 1); // +1 for \n
|
|
274
|
+
return { ...block, text: replacement };
|
|
275
|
+
}
|
|
276
|
+
return block;
|
|
277
|
+
});
|
|
278
|
+
return { ...msg, content: newContent };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return msg;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return { ...params, messages };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Access the underlying OpenAI client */
|
|
288
|
+
get raw(): OpenAILike {
|
|
289
|
+
return this.client;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Graceful shutdown */
|
|
293
|
+
async close(): Promise<void> {
|
|
294
|
+
if (this.shield) {
|
|
295
|
+
await this.shield.close();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================
|
|
301
|
+
// ShieldedChatStream — Async iterable wrapper for streaming
|
|
302
|
+
// Scans input before stream, accumulates output, scans after
|
|
303
|
+
// ============================================================
|
|
304
|
+
|
|
305
|
+
export class ShieldedChatStream implements AsyncIterable<ChatCompletionChunk> {
|
|
306
|
+
private _inputResult: ScanResult;
|
|
307
|
+
private _outputResult: ScanResult | undefined;
|
|
308
|
+
private _done = false;
|
|
309
|
+
private _fullText = "";
|
|
310
|
+
private _stream: AsyncIterable<ChatCompletionChunk>;
|
|
311
|
+
private _shieldInstance: AIShield;
|
|
312
|
+
private _context: ScanContext;
|
|
313
|
+
private _scanOutput: boolean;
|
|
314
|
+
private _agentId: string | undefined;
|
|
315
|
+
private _model: string;
|
|
316
|
+
private _usage: { prompt_tokens: number; completion_tokens: number } | undefined;
|
|
317
|
+
|
|
318
|
+
constructor(
|
|
319
|
+
stream: AsyncIterable<ChatCompletionChunk>,
|
|
320
|
+
inputResult: ScanResult,
|
|
321
|
+
shieldInstance: AIShield,
|
|
322
|
+
context: ScanContext,
|
|
323
|
+
scanOutput: boolean,
|
|
324
|
+
agentId: string | undefined,
|
|
325
|
+
model: string,
|
|
326
|
+
) {
|
|
327
|
+
this._stream = stream;
|
|
328
|
+
this._inputResult = inputResult;
|
|
329
|
+
this._shieldInstance = shieldInstance;
|
|
330
|
+
this._context = context;
|
|
331
|
+
this._scanOutput = scanOutput;
|
|
332
|
+
this._agentId = agentId;
|
|
333
|
+
this._model = model;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<ChatCompletionChunk> {
|
|
337
|
+
for await (const chunk of this._stream) {
|
|
338
|
+
// Accumulate text content
|
|
339
|
+
const content = chunk.choices[0]?.delta?.content;
|
|
340
|
+
if (content) {
|
|
341
|
+
this._fullText += content;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Capture usage if present (typically in the last chunk)
|
|
345
|
+
if (chunk.usage) {
|
|
346
|
+
this._usage = {
|
|
347
|
+
prompt_tokens: chunk.usage.prompt_tokens,
|
|
348
|
+
completion_tokens: chunk.usage.completion_tokens,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
yield chunk;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// --- Post-stream: record cost ---
|
|
356
|
+
if (this._agentId && this._usage) {
|
|
357
|
+
await this._shieldInstance.recordCost(
|
|
358
|
+
this._agentId,
|
|
359
|
+
this._model,
|
|
360
|
+
this._usage.prompt_tokens,
|
|
361
|
+
this._usage.completion_tokens,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- Post-stream: scan output ---
|
|
366
|
+
if (this._scanOutput && this._fullText) {
|
|
367
|
+
this._outputResult = await this._shieldInstance.scan(
|
|
368
|
+
this._fullText,
|
|
369
|
+
this._context,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this._done = true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Input scan result (available immediately) */
|
|
377
|
+
get inputResult(): ScanResult {
|
|
378
|
+
return this._inputResult;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Output scan result (available after stream completes) */
|
|
382
|
+
get outputResult(): ScanResult | undefined {
|
|
383
|
+
return this._outputResult;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Combined shield results */
|
|
387
|
+
get shieldResult(): { input: ScanResult; output?: ScanResult } {
|
|
388
|
+
return { input: this._inputResult, output: this._outputResult };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Whether the stream has completed */
|
|
392
|
+
get done(): boolean {
|
|
393
|
+
return this._done;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Full accumulated text from the stream */
|
|
397
|
+
get text(): string {
|
|
398
|
+
return this._fullText;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// --- Error types ---
|
|
403
|
+
|
|
404
|
+
export class ShieldBlockError extends Error {
|
|
405
|
+
constructor(
|
|
406
|
+
message: string,
|
|
407
|
+
public readonly scanResult: ScanResult,
|
|
408
|
+
) {
|
|
409
|
+
super(message);
|
|
410
|
+
this.name = "ShieldBlockError";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export class ShieldBudgetError extends Error {
|
|
415
|
+
constructor(
|
|
416
|
+
message: string,
|
|
417
|
+
public readonly budgetCheck: { allowed: boolean; currentSpend: number; remainingBudget: number },
|
|
418
|
+
) {
|
|
419
|
+
super(message);
|
|
420
|
+
this.name = "ShieldBudgetError";
|
|
421
|
+
}
|
|
422
|
+
}
|