ai-shield-openai 0.2.0 → 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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/wrapper.ts +75 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-shield-openai",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "AI Shield wrapper for OpenAI SDK — automatic input/output scanning",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "openai": ">=4.0.0"
21
21
  },
22
22
  "dependencies": {
23
- "ai-shield-core": "0.2.0"
23
+ "ai-shield-core": "0.3.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "openai": "^4.77.0",
package/src/wrapper.ts CHANGED
@@ -1,4 +1,11 @@
1
- import type { AIShield, ShieldConfig, ScanContext, ScanResult } from "ai-shield-core";
1
+ import type {
2
+ AIShield,
3
+ ShieldConfig,
4
+ ScanContext,
5
+ ScanResult,
6
+ OutputScanConfig,
7
+ OutputScanResult,
8
+ } from "ai-shield-core";
2
9
 
3
10
  // ============================================================
4
11
  // OpenAI Shield Wrapper — Drop-in replacement
@@ -15,8 +22,19 @@ export interface ShieldedOpenAIConfig {
15
22
  agentId?: string;
16
23
  /** Custom scan context factory */
17
24
  contextFactory?: (messages: ChatMessage[]) => ScanContext;
18
- /** Whether to scan output (response) too — default: false */
25
+ /**
26
+ * Legacy: run the INPUT scan chain (heuristic + PII) over the output too.
27
+ * Default false. For real output-side defense (secret leak, SQL/XSS/shell
28
+ * injection, system-prompt leak) use `outputScan` below (OWASP LLM05).
29
+ */
19
30
  scanOutput?: boolean;
31
+ /**
32
+ * Run the dedicated output scanner over the response (OWASP LLM05 / LLM02):
33
+ * secret leak, output injection, system-prompt leak, jailbreak, output-side
34
+ * PII. Pass `true` for defaults or an `OutputScanConfig`. Result lands in
35
+ * `response._shield.outputScan`.
36
+ */
37
+ outputScan?: boolean | OutputScanConfig;
20
38
  /** Callback when input is blocked */
21
39
  onBlocked?: (result: ScanResult, messages: ChatMessage[]) => void;
22
40
  /** Callback when input has warnings */
@@ -195,10 +213,29 @@ export class ShieldedOpenAI {
195
213
  return { shieldInstance, context, userContent, inputResult, finalParams };
196
214
  }
197
215
 
216
+ /** Run the dedicated OutputScanner if `outputScan` is configured. */
217
+ private async runOutputScan(
218
+ text: string,
219
+ context: ScanContext,
220
+ ): Promise<OutputScanResult | undefined> {
221
+ if (!this.config.outputScan || !text) return undefined;
222
+ const cfg = this.config.outputScan === true ? {} : this.config.outputScan;
223
+ const mod = await import("ai-shield-core");
224
+ return mod.scanOutput(text, cfg, context);
225
+ }
226
+
198
227
  /** Create chat completion with Shield protection (non-streaming) */
199
228
  async createChatCompletion(
200
229
  params: ChatCompletionParams,
201
- ): Promise<ChatCompletion & { _shield?: { input: ScanResult; output?: ScanResult } }> {
230
+ ): Promise<
231
+ ChatCompletion & {
232
+ _shield?: {
233
+ input: ScanResult;
234
+ output?: ScanResult;
235
+ outputScan?: OutputScanResult;
236
+ };
237
+ }
238
+ > {
202
239
  const { shieldInstance, context, inputResult, finalParams } = await this.scanInput(params);
203
240
 
204
241
  // --- Make the actual API call ---
@@ -215,17 +252,16 @@ export class ShieldedOpenAI {
215
252
  }
216
253
 
217
254
  // --- Scan output ---
255
+ const outputText = response.choices[0]?.message?.content ?? "";
218
256
  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
- }
257
+ if (this.config.scanOutput && outputText) {
258
+ outputResult = await shieldInstance.scan(outputText, context);
224
259
  }
260
+ const outputScan = await this.runOutputScan(outputText, context);
225
261
 
226
262
  return {
227
263
  ...response,
228
- _shield: { input: inputResult, output: outputResult },
264
+ _shield: { input: inputResult, output: outputResult, outputScan },
229
265
  };
230
266
  }
231
267
 
@@ -250,6 +286,7 @@ export class ShieldedOpenAI {
250
286
  this.config.scanOutput ?? false,
251
287
  this.config.agentId,
252
288
  finalParams.model,
289
+ this.config.outputScan,
253
290
  );
254
291
  }
255
292
 
@@ -305,12 +342,14 @@ export class ShieldedOpenAI {
305
342
  export class ShieldedChatStream implements AsyncIterable<ChatCompletionChunk> {
306
343
  private _inputResult: ScanResult;
307
344
  private _outputResult: ScanResult | undefined;
345
+ private _outputScanResult: OutputScanResult | undefined;
308
346
  private _done = false;
309
347
  private _fullText = "";
310
348
  private _stream: AsyncIterable<ChatCompletionChunk>;
311
349
  private _shieldInstance: AIShield;
312
350
  private _context: ScanContext;
313
351
  private _scanOutput: boolean;
352
+ private _outputScan: boolean | OutputScanConfig | undefined;
314
353
  private _agentId: string | undefined;
315
354
  private _model: string;
316
355
  private _usage: { prompt_tokens: number; completion_tokens: number } | undefined;
@@ -323,12 +362,14 @@ export class ShieldedChatStream implements AsyncIterable<ChatCompletionChunk> {
323
362
  scanOutput: boolean,
324
363
  agentId: string | undefined,
325
364
  model: string,
365
+ outputScan?: boolean | OutputScanConfig,
326
366
  ) {
327
367
  this._stream = stream;
328
368
  this._inputResult = inputResult;
329
369
  this._shieldInstance = shieldInstance;
330
370
  this._context = context;
331
371
  this._scanOutput = scanOutput;
372
+ this._outputScan = outputScan;
332
373
  this._agentId = agentId;
333
374
  this._model = model;
334
375
  }
@@ -369,6 +410,15 @@ export class ShieldedChatStream implements AsyncIterable<ChatCompletionChunk> {
369
410
  this._context,
370
411
  );
371
412
  }
413
+ if (this._outputScan && this._fullText) {
414
+ const cfg = this._outputScan === true ? {} : this._outputScan;
415
+ const mod = await import("ai-shield-core");
416
+ this._outputScanResult = await mod.scanOutput(
417
+ this._fullText,
418
+ cfg,
419
+ this._context,
420
+ );
421
+ }
372
422
 
373
423
  this._done = true;
374
424
  }
@@ -378,14 +428,27 @@ export class ShieldedChatStream implements AsyncIterable<ChatCompletionChunk> {
378
428
  return this._inputResult;
379
429
  }
380
430
 
381
- /** Output scan result (available after stream completes) */
431
+ /** Output scan result (legacy input-chain over output; after stream completes) */
382
432
  get outputResult(): ScanResult | undefined {
383
433
  return this._outputResult;
384
434
  }
385
435
 
436
+ /** Dedicated OutputScanner result (after stream completes, if `outputScan` set) */
437
+ get outputScanResult(): OutputScanResult | undefined {
438
+ return this._outputScanResult;
439
+ }
440
+
386
441
  /** Combined shield results */
387
- get shieldResult(): { input: ScanResult; output?: ScanResult } {
388
- return { input: this._inputResult, output: this._outputResult };
442
+ get shieldResult(): {
443
+ input: ScanResult;
444
+ output?: ScanResult;
445
+ outputScan?: OutputScanResult;
446
+ } {
447
+ return {
448
+ input: this._inputResult,
449
+ output: this._outputResult,
450
+ outputScan: this._outputScanResult,
451
+ };
389
452
  }
390
453
 
391
454
  /** Whether the stream has completed */