ai-shield-core 0.1.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 (76) hide show
  1. package/dist/audit/logger.d.ts.map +1 -1
  2. package/dist/audit/logger.js +13 -14
  3. package/dist/audit/types.js +1 -2
  4. package/dist/cache/lru.js +1 -5
  5. package/dist/canary/memory.d.ts +75 -0
  6. package/dist/canary/memory.d.ts.map +1 -0
  7. package/dist/canary/memory.js +194 -0
  8. package/dist/context/wrap-context.d.ts +169 -0
  9. package/dist/context/wrap-context.d.ts.map +1 -0
  10. package/dist/context/wrap-context.js +278 -0
  11. package/dist/cost/anomaly.js +1 -4
  12. package/dist/cost/pricing.d.ts.map +1 -1
  13. package/dist/cost/pricing.js +26 -19
  14. package/dist/cost/tracker.d.ts +19 -1
  15. package/dist/cost/tracker.d.ts.map +1 -1
  16. package/dist/cost/tracker.js +27 -10
  17. package/dist/index.d.ts +34 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +55 -37
  20. package/dist/judge/async-judge.d.ts +85 -0
  21. package/dist/judge/async-judge.d.ts.map +1 -0
  22. package/dist/judge/async-judge.js +146 -0
  23. package/dist/policy/circuit-breaker.d.ts +70 -0
  24. package/dist/policy/circuit-breaker.d.ts.map +1 -0
  25. package/dist/policy/circuit-breaker.js +376 -0
  26. package/dist/policy/engine.js +1 -5
  27. package/dist/policy/tools.js +4 -8
  28. package/dist/scanner/canary.js +4 -8
  29. package/dist/scanner/chain.js +1 -5
  30. package/dist/scanner/heuristic.d.ts +27 -0
  31. package/dist/scanner/heuristic.d.ts.map +1 -1
  32. package/dist/scanner/heuristic.js +118 -7
  33. package/dist/scanner/ingestion.d.ts +147 -0
  34. package/dist/scanner/ingestion.d.ts.map +1 -0
  35. package/dist/scanner/ingestion.js +520 -0
  36. package/dist/scanner/output.d.ts +73 -0
  37. package/dist/scanner/output.d.ts.map +1 -0
  38. package/dist/scanner/output.js +297 -0
  39. package/dist/scanner/pii.d.ts.map +1 -1
  40. package/dist/scanner/pii.js +24 -12
  41. package/dist/shield.d.ts.map +1 -1
  42. package/dist/shield.js +34 -26
  43. package/dist/types.d.ts +156 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/types.js +1 -2
  46. package/package.json +4 -3
  47. package/src/audit/logger.ts +6 -1
  48. package/src/canary/memory.ts +259 -0
  49. package/src/context/wrap-context.ts +475 -0
  50. package/src/cost/pricing.ts +21 -9
  51. package/src/cost/tracker.ts +35 -1
  52. package/src/index.ts +113 -2
  53. package/src/judge/async-judge.ts +254 -0
  54. package/src/policy/circuit-breaker.ts +449 -0
  55. package/src/scanner/heuristic.ts +125 -2
  56. package/src/scanner/ingestion.ts +624 -0
  57. package/src/scanner/output.ts +386 -0
  58. package/src/scanner/pii.ts +21 -7
  59. package/src/shield.ts +15 -2
  60. package/src/types.ts +194 -2
  61. package/tsconfig.json +2 -1
  62. package/dist/audit/logger.js.map +0 -1
  63. package/dist/audit/types.js.map +0 -1
  64. package/dist/cache/lru.js.map +0 -1
  65. package/dist/cost/anomaly.js.map +0 -1
  66. package/dist/cost/pricing.js.map +0 -1
  67. package/dist/cost/tracker.js.map +0 -1
  68. package/dist/index.js.map +0 -1
  69. package/dist/policy/engine.js.map +0 -1
  70. package/dist/policy/tools.js.map +0 -1
  71. package/dist/scanner/canary.js.map +0 -1
  72. package/dist/scanner/chain.js.map +0 -1
  73. package/dist/scanner/heuristic.js.map +0 -1
  74. package/dist/scanner/pii.js.map +0 -1
  75. package/dist/shield.js.map +0 -1
  76. package/dist/types.js.map +0 -1
@@ -0,0 +1,278 @@
1
+ import { createHash } from "node:crypto";
2
+ import { IngestionScanner } from "../scanner/ingestion.js";
3
+ /**
4
+ * Build a `WrappedContext` from typed inputs.
5
+ *
6
+ * Trust assignment:
7
+ * - `system` -> system
8
+ * - `retrieved`/`tools`/`memory`/`web`/`agent-output` -> untrusted
9
+ * - `user` -> untrusted (a user is not trusted in this threat model — they
10
+ * can also inject; the `untrusted` label means "scan aggressively")
11
+ * - any segment whose `label` matches one of `trustedLabels` -> trusted
12
+ *
13
+ * Trust does NOT mean "skip scanning". It only governs how
14
+ * `assemblePrompt()` and the per-segment policy decide whether to
15
+ * include the segment in the final assembled prompt.
16
+ */
17
+ export function wrapContext(input) {
18
+ const segments = [];
19
+ const trustedLabels = (input.trustedLabels ?? []).map((s) => s.toLowerCase());
20
+ // Critic H1 — substring match would let an attacker-supplied label
21
+ // like "untrusted-doc-INTERNAL-kb-poisoned" claim trust because it
22
+ // CONTAINS the trusted prefix. Match exact or path-anchored only.
23
+ const isTrustedLabel = (label) => {
24
+ if (!label)
25
+ return false;
26
+ const lc = label.toLowerCase();
27
+ return trustedLabels.some((tl) => lc === tl || lc.startsWith(tl + "/"));
28
+ };
29
+ const push = (content, source, trust, label) => {
30
+ if (typeof content !== "string" || content.length === 0)
31
+ return;
32
+ segments.push({
33
+ source,
34
+ trust,
35
+ content,
36
+ label,
37
+ contentHash: hashContent(content),
38
+ });
39
+ };
40
+ // System: always trust=system. The `source` field is unused for
41
+ // system segments because `trust === "system"` is the authoritative
42
+ // signal — Analyst A2 round 1 review. We keep `source: "user"` here
43
+ // only because `ContextSegment.source` is non-optional; any code that
44
+ // branches on `seg.source` MUST first check `seg.trust !== "system"`.
45
+ if (input.system) {
46
+ push(input.system, "user", "system", "system-prompt");
47
+ }
48
+ // User messages.
49
+ if (input.user) {
50
+ const userInputs = Array.isArray(input.user) ? input.user : [input.user];
51
+ for (const u of userInputs) {
52
+ push(u, "user", "untrusted", "user");
53
+ }
54
+ }
55
+ // Helper for the array-of-{content,label} groups.
56
+ const pushGroup = (items, source) => {
57
+ if (!items)
58
+ return;
59
+ for (const item of items) {
60
+ const content = typeof item === "string" ? item : item.content;
61
+ const label = typeof item === "string" ? undefined : item.label;
62
+ const trust = isTrustedLabel(label) ? "trusted" : "untrusted";
63
+ push(content, source, trust, label);
64
+ }
65
+ };
66
+ pushGroup(input.retrieved, "rag");
67
+ pushGroup(input.tools, "tool-desc");
68
+ pushGroup(input.memory, "memory");
69
+ pushGroup(input.web, "web");
70
+ pushGroup(input.agentOutput, "agent-output");
71
+ return { segments };
72
+ }
73
+ /**
74
+ * Scan every segment with the source-specific ingestion profile.
75
+ * Mutates `ctx` in place by attaching `scanResults` + `decision`,
76
+ * AND returns the same object for chaining.
77
+ */
78
+ export async function scanWrappedContext(ctx, options = {}) {
79
+ const scanner = new IngestionScanner({
80
+ strictness: options.strictness ?? "high",
81
+ });
82
+ const results = [];
83
+ let worst = "allow";
84
+ for (let i = 0; i < ctx.segments.length; i += 1) {
85
+ const seg = ctx.segments[i];
86
+ // System segments skip the scanner — they're developer-authored and
87
+ // running the heuristic over a real system prompt would flood with
88
+ // false positives (system prompts ARE instructions, by definition).
89
+ if (seg.trust === "system") {
90
+ results.push({ segmentIndex: i, decision: "allow", violations: [] });
91
+ continue;
92
+ }
93
+ const scanContext = {
94
+ source: seg.source,
95
+ trustTier: seg.trust,
96
+ };
97
+ const r = await scanner.scan(seg.content, scanContext);
98
+ results.push({
99
+ segmentIndex: i,
100
+ decision: r.decision,
101
+ violations: r.violations,
102
+ });
103
+ if (priority(r.decision) > priority(worst)) {
104
+ worst = r.decision;
105
+ }
106
+ }
107
+ ctx.scanResults = results;
108
+ ctx.decision = worst;
109
+ return ctx;
110
+ }
111
+ export function assemblePrompt(ctx, options = {}) {
112
+ const fences = {
113
+ untrusted: options.fences?.untrusted ?? {
114
+ open: "<UNTRUSTED_CONTENT source=",
115
+ close: "</UNTRUSTED_CONTENT>",
116
+ },
117
+ blocked: options.fences?.blocked ?? {
118
+ open: "<BLOCKED_CONTENT source=",
119
+ close: "</BLOCKED_CONTENT>",
120
+ },
121
+ };
122
+ // Pre-build a segment→index map ONCE. Avoids O(n²) `indexOf` inside the
123
+ // assembly loop AND removes a TOCTOU on mutable `ctx.segments` (Critic
124
+ // H2 + Analyst A4 round 1 review).
125
+ const segmentIndexMap = new Map();
126
+ ctx.segments.forEach((s, i) => segmentIndexMap.set(s, i));
127
+ const segmentResultMap = new Map();
128
+ for (const r of ctx.scanResults ?? []) {
129
+ segmentResultMap.set(r.segmentIndex, r);
130
+ }
131
+ const ordered = [];
132
+ // 1. system
133
+ ordered.push(...ctx.segments.filter((s) => s.trust === "system"));
134
+ // 2. trusted (retrieved/memory/tool-desc the dev marked as trusted)
135
+ ordered.push(...ctx.segments.filter((s) => s.trust === "trusted"));
136
+ // 3. user (untrusted, source="user")
137
+ ordered.push(...ctx.segments.filter((s) => s.source === "user" && s.trust === "untrusted"));
138
+ // 4. all remaining untrusted, preserve original order within group.
139
+ for (const s of ctx.segments) {
140
+ if (s.trust === "untrusted" && s.source !== "user") {
141
+ ordered.push(s);
142
+ }
143
+ }
144
+ const parts = [];
145
+ for (const seg of ordered) {
146
+ const segIdx = segmentIndexMap.get(seg) ?? -1;
147
+ const segResult = segIdx >= 0 ? segmentResultMap.get(segIdx) : undefined;
148
+ const blocked = segResult?.decision === "block";
149
+ if (blocked) {
150
+ if (options.strictMode) {
151
+ // Drop entirely.
152
+ continue;
153
+ }
154
+ parts.push(`${fences.blocked.open}"${seg.source}" label="${seg.label ?? ""}">\n${seg.content}\n${fences.blocked.close}`);
155
+ continue;
156
+ }
157
+ if (seg.trust === "system") {
158
+ parts.push(seg.content);
159
+ }
160
+ else if (seg.trust === "trusted") {
161
+ parts.push(seg.content);
162
+ }
163
+ else if (seg.source === "user" && seg.trust === "untrusted") {
164
+ // User input keeps its natural shape — fencing every user message
165
+ // creates more noise than signal.
166
+ parts.push(seg.content);
167
+ }
168
+ else {
169
+ parts.push(`${fences.untrusted.open}"${seg.source}" label="${seg.label ?? ""}">\n${seg.content}\n${fences.untrusted.close}`);
170
+ }
171
+ }
172
+ return parts.join("\n\n");
173
+ }
174
+ function hashContent(content) {
175
+ return createHash("sha256").update(content).digest("hex");
176
+ }
177
+ function priority(d) {
178
+ return d === "block" ? 2 : d === "warn" ? 1 : 0;
179
+ }
180
+ /**
181
+ * Convenience aggregator: violations across all scanned segments.
182
+ */
183
+ export function flattenViolations(ctx) {
184
+ if (!ctx.scanResults)
185
+ return [];
186
+ return ctx.scanResults.flatMap((r) => r.violations);
187
+ }
188
+ /**
189
+ * Scan one agent-to-agent hand-off and propagate trust along the chain.
190
+ *
191
+ * @param payload The producing agent's output (= consuming agent's input).
192
+ * @param fromAgentId Agent that produced `payload`.
193
+ * @param toAgentId Agent about to consume `payload`.
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * import { propagateTrust } from "ai-shield-core";
198
+ *
199
+ * // A → B
200
+ * let chain = await propagateTrust(aOutput, "researcher", "planner");
201
+ * // B → C, contamination at A stays sticky through to C
202
+ * chain = await propagateTrust(bOutput, "planner", "executor", {
203
+ * priorChain: chain.hops,
204
+ * });
205
+ * if (chain.effectiveTrust !== "trusted" && !chain.safe) {
206
+ * // an upstream agent was poisoned — do not let the executor act on it
207
+ * haltPipeline(chain.violations);
208
+ * }
209
+ * ```
210
+ */
211
+ export async function propagateTrust(payload, fromAgentId, toAgentId, options = {}) {
212
+ const fromTrust = options.fromTrust ?? "untrusted";
213
+ const priorChain = options.priorChain ?? [];
214
+ const scanner = new IngestionScanner({
215
+ strictness: options.strictness ?? "high",
216
+ });
217
+ const scanContext = {
218
+ source: "agent-output",
219
+ trustTier: fromTrust,
220
+ agentId: fromAgentId,
221
+ };
222
+ const scan = await scanner.scan(payload, scanContext);
223
+ const hopViolations = scan.violations.map((v) => ({
224
+ ...v,
225
+ detail: `${v.detail ?? ""} (${fromAgentId}→${toAgentId})`.trim(),
226
+ }));
227
+ // Was anything upstream already contaminated?
228
+ const upstreamWorst = priorChain.reduce((worst, h) => (priority(h.decision) > priority(worst) ? h.decision : worst), "allow");
229
+ const upstreamContaminated = upstreamWorst !== "allow";
230
+ // This hop's own decision.
231
+ const hopDecision = scan.decision;
232
+ // Make contamination explicit as a multi-agent violation (distinct from
233
+ // the per-segment `ingested_injection` the scanner already produced).
234
+ if (hopDecision !== "allow") {
235
+ hopViolations.push({
236
+ type: "trust_propagation",
237
+ scanner: "trust-chain",
238
+ score: hopDecision === "block" ? 1.0 : 0.5,
239
+ threshold: 0.5,
240
+ message: `Contagion risk in hand-off ${fromAgentId}→${toAgentId}`,
241
+ detail: `Agent output flagged at this hop`,
242
+ });
243
+ }
244
+ else if (upstreamContaminated) {
245
+ hopViolations.push({
246
+ type: "trust_propagation",
247
+ scanner: "trust-chain",
248
+ score: 0.5,
249
+ threshold: 0.5,
250
+ message: `Payload reaching ${toAgentId} originates from a contaminated chain`,
251
+ detail: `Upstream contamination is sticky across hops`,
252
+ });
253
+ }
254
+ const hop = {
255
+ agentId: fromAgentId,
256
+ trust: fromTrust,
257
+ decision: hopDecision,
258
+ violations: hopViolations,
259
+ };
260
+ const hops = [...priorChain, hop];
261
+ // Worst decision across the full chain (sticky).
262
+ const chainDecision = priority(hopDecision) >= priority(upstreamWorst)
263
+ ? hopDecision
264
+ : upstreamWorst;
265
+ // Effective trust degrades to untrusted on ANY contamination in the chain.
266
+ // A clean hand-off from a `trusted` agent with a clean chain stays trusted.
267
+ const effectiveTrust = chainDecision === "allow" && fromTrust === "trusted"
268
+ ? "trusted"
269
+ : "untrusted";
270
+ return {
271
+ safe: chainDecision === "allow",
272
+ decision: chainDecision,
273
+ effectiveTrust,
274
+ hops,
275
+ violations: hops.flatMap((h) => h.violations),
276
+ };
277
+ }
278
+ //# sourceMappingURL=wrap-context.js.map
@@ -1,12 +1,9 @@
1
- "use strict";
2
1
  // ============================================================
3
2
  // Cost Anomaly Detection — Z-Score based
4
3
  // Flags unusual spending patterns
5
4
  // ============================================================
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.detectAnomaly = detectAnomaly;
8
5
  /** Detect anomalies using z-score (>2.5 standard deviations = anomaly) */
9
- function detectAnomaly(currentValue, historicalValues, threshold = 2.5) {
6
+ export function detectAnomaly(currentValue, historicalValues, threshold = 2.5) {
10
7
  if (historicalValues.length < 3) {
11
8
  // Not enough data to determine anomaly
12
9
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/cost/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOhD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAsBtD,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAY3D;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,MAAM,CAMR"}
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/cost/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAchD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CA2BtD,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAY3D;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,MAAM,CAMR"}
@@ -1,13 +1,15 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MODEL_PRICING = void 0;
4
- exports.getModelPricing = getModelPricing;
5
- exports.estimateCost = estimateCost;
6
1
  // ============================================================
7
- // Model Pricing Table — Updated Feb 2026
8
- // Prices in USD per 1M tokens
2
+ // Model Pricing Table — Updated June 2026
3
+ // Prices in USD per 1M tokens.
4
+ // Includes `cachedInputPer1M` for providers that support prompt caching
5
+ // (Anthropic cache reads land at ~10% of standard input rate).
6
+ //
7
+ // Note: with the Opus 4.7 generation Anthropic dropped the Opus input/output
8
+ // rate from $15/$75 to $5/$25 and serves the 1M context window at standard
9
+ // pricing (no long-context premium). Earlier tables that still list Opus at
10
+ // $15/$75 over-estimate Opus cost by ~3x.
9
11
  // ============================================================
10
- exports.MODEL_PRICING = {
12
+ export const MODEL_PRICING = {
11
13
  // OpenAI
12
14
  "gpt-5.2": { inputPer1M: 2.50, outputPer1M: 10.0 },
13
15
  "gpt-5.1": { inputPer1M: 2.50, outputPer1M: 10.0 },
@@ -18,24 +20,29 @@ exports.MODEL_PRICING = {
18
20
  "o3": { inputPer1M: 10.0, outputPer1M: 40.0 },
19
21
  "o3-mini": { inputPer1M: 1.10, outputPer1M: 4.40 },
20
22
  "o4-mini": { inputPer1M: 1.10, outputPer1M: 4.40 },
21
- // Anthropic
22
- "claude-opus-4-6": { inputPer1M: 15.0, outputPer1M: 75.0 },
23
- "claude-sonnet-4-6": { inputPer1M: 3.0, outputPer1M: 15.0 },
24
- "claude-haiku-4-5": { inputPer1M: 0.80, outputPer1M: 4.0 },
23
+ // Anthropic — June 2026 line-up (Fable 5, Opus 4.8/4.7/4.6, Sonnet 4.6, Haiku 4.5)
24
+ "claude-fable-5": { inputPer1M: 10.0, outputPer1M: 50.0, cachedInputPer1M: 1.0 },
25
+ "claude-opus-4-8": { inputPer1M: 5.0, outputPer1M: 25.0, cachedInputPer1M: 0.50 },
26
+ "claude-opus-4-7": { inputPer1M: 5.0, outputPer1M: 25.0, cachedInputPer1M: 0.50 },
27
+ "claude-opus-4-6": { inputPer1M: 5.0, outputPer1M: 25.0, cachedInputPer1M: 0.50 },
28
+ "claude-sonnet-4-6": { inputPer1M: 3.0, outputPer1M: 15.0, cachedInputPer1M: 0.30 },
29
+ "claude-sonnet-4-5": { inputPer1M: 3.0, outputPer1M: 15.0, cachedInputPer1M: 0.30 },
30
+ "claude-haiku-4-5": { inputPer1M: 1.0, outputPer1M: 5.0, cachedInputPer1M: 0.10 },
25
31
  // Aliases
26
32
  "gpt-5.2-turbo": { inputPer1M: 2.50, outputPer1M: 10.0 },
27
- opus: { inputPer1M: 15.0, outputPer1M: 75.0 },
28
- sonnet: { inputPer1M: 3.0, outputPer1M: 15.0 },
29
- haiku: { inputPer1M: 0.80, outputPer1M: 4.0 },
33
+ fable: { inputPer1M: 10.0, outputPer1M: 50.0, cachedInputPer1M: 1.0 },
34
+ opus: { inputPer1M: 5.0, outputPer1M: 25.0, cachedInputPer1M: 0.50 },
35
+ sonnet: { inputPer1M: 3.0, outputPer1M: 15.0, cachedInputPer1M: 0.30 },
36
+ haiku: { inputPer1M: 1.0, outputPer1M: 5.0, cachedInputPer1M: 0.10 },
30
37
  };
31
38
  /** Get pricing for a model, fallback to gpt-4o-mini rates */
32
- function getModelPricing(model) {
39
+ export function getModelPricing(model) {
33
40
  // Try exact match
34
- const exact = exports.MODEL_PRICING[model];
41
+ const exact = MODEL_PRICING[model];
35
42
  if (exact)
36
43
  return exact;
37
44
  // Try prefix match (e.g., "gpt-4o-2024-08-06" → "gpt-4o")
38
- for (const [key, pricing] of Object.entries(exports.MODEL_PRICING)) {
45
+ for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
39
46
  if (model.startsWith(key))
40
47
  return pricing;
41
48
  }
@@ -43,7 +50,7 @@ function getModelPricing(model) {
43
50
  return { inputPer1M: 0.15, outputPer1M: 0.60 };
44
51
  }
45
52
  /** Estimate cost for a given number of tokens */
46
- function estimateCost(model, inputTokens, outputTokens) {
53
+ export function estimateCost(model, inputTokens, outputTokens) {
47
54
  const pricing = getModelPricing(model);
48
55
  return ((inputTokens / 1_000_000) * pricing.inputPer1M +
49
56
  (outputTokens / 1_000_000) * pricing.outputPer1M);
@@ -5,15 +5,33 @@ export interface RedisLike {
5
5
  incrbyfloat(key: string, increment: number): Promise<string>;
6
6
  expire(key: string, seconds: number): Promise<number>;
7
7
  }
8
+ export interface CostTrackerOptions {
9
+ /**
10
+ * Cap on in-memory CostRecord retention (ring-buffer).
11
+ * Default: 10_000. Set to 0 to disable record retention entirely
12
+ * (use this in long-running processes that only care about budget
13
+ * counters, not per-request records).
14
+ * Override via env: AI_SHIELD_MAX_RECORDS.
15
+ */
16
+ maxRecords?: number;
17
+ }
8
18
  export declare class CostTracker {
9
19
  private store;
10
20
  private budgets;
11
21
  private records;
12
- constructor(budgets?: Record<string, BudgetConfig>, redis?: RedisLike);
22
+ private maxRecords;
23
+ constructor(budgets?: Record<string, BudgetConfig>, redis?: RedisLike, options?: CostTrackerOptions);
13
24
  /** Check if a request is within budget BEFORE sending to LLM */
14
25
  checkBudget(entityId: string, model: string, estimatedInputTokens: number, estimatedOutputTokens?: number): Promise<BudgetCheckResult>;
15
26
  /** Record actual cost AFTER receiving response */
16
27
  recordCost(entityId: string, model: string, inputTokens: number, outputTokens: number): Promise<CostRecord>;
28
+ /**
29
+ * Append a record with ring-buffer semantics to prevent unbounded memory growth.
30
+ * When maxRecords is 0, records are not retained.
31
+ */
32
+ private appendRecord;
33
+ /** Clear all in-memory records (e.g., after export) */
34
+ clearRecords(): void;
17
35
  /** Get current spend for an entity */
18
36
  getCurrentSpend(entityId: string): Promise<number>;
19
37
  /** Get all recorded costs (for export/audit) */
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/cost/tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EAEjB,UAAU,EACX,MAAM,aAAa,CAAC;AASrB,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD;AAgCD,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,OAAO,CAAoB;gBAGjC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAM,EAC1C,KAAK,CAAC,EAAE,SAAS;IAMnB,gEAAgE;IAC1D,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,oBAAoB,EAAE,MAAM,EAC5B,qBAAqB,GAAE,MAAY,GAClC,OAAO,CAAC,iBAAiB,CAAC;IAgC7B,kDAAkD;IAC5C,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,UAAU,CAAC;IA+BtB,sCAAsC;IAChC,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOxD,gDAAgD;IAChD,UAAU,IAAI,UAAU,EAAE;IAI1B,OAAO,CAAC,SAAS;IAmBjB,OAAO,CAAC,aAAa;CAUtB"}
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/cost/tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EAEjB,UAAU,EACX,MAAM,aAAa,CAAC;AASrB,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD;AAgCD,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,UAAU,CAAS;gBAGzB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAM,EAC1C,KAAK,CAAC,EAAE,SAAS,EACjB,OAAO,GAAE,kBAAuB;IAQlC,gEAAgE;IAC1D,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,oBAAoB,EAAE,MAAM,EAC5B,qBAAqB,GAAE,MAAY,GAClC,OAAO,CAAC,iBAAiB,CAAC;IAgC7B,kDAAkD;IAC5C,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,UAAU,CAAC;IA+BtB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAUpB,uDAAuD;IACvD,YAAY,IAAI,IAAI;IAIpB,sCAAsC;IAChC,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOxD,gDAAgD;IAChD,UAAU,IAAI,UAAU,EAAE;IAI1B,OAAO,CAAC,SAAS;IAmBjB,OAAO,CAAC,aAAa;CAUtB"}
@@ -1,7 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CostTracker = void 0;
4
- const pricing_js_1 = require("./pricing.js");
1
+ import { estimateCost } from "./pricing.js";
5
2
  /** In-memory fallback store */
6
3
  class MemoryStore {
7
4
  data = new Map();
@@ -30,13 +27,16 @@ class MemoryStore {
30
27
  return 1;
31
28
  }
32
29
  }
33
- class CostTracker {
30
+ export class CostTracker {
34
31
  store;
35
32
  budgets;
36
33
  records = [];
37
- constructor(budgets = {}, redis) {
34
+ maxRecords;
35
+ constructor(budgets = {}, redis, options = {}) {
38
36
  this.store = redis ?? new MemoryStore();
39
37
  this.budgets = new Map(Object.entries(budgets));
38
+ const envCap = Number(process.env.AI_SHIELD_MAX_RECORDS);
39
+ this.maxRecords = options.maxRecords ?? (Number.isFinite(envCap) && envCap >= 0 ? envCap : 10_000);
40
40
  }
41
41
  /** Check if a request is within budget BEFORE sending to LLM */
42
42
  async checkBudget(entityId, model, estimatedInputTokens, estimatedOutputTokens = 500) {
@@ -44,7 +44,7 @@ class CostTracker {
44
44
  if (!budget) {
45
45
  return { allowed: true, currentSpend: 0, remainingBudget: Infinity };
46
46
  }
47
- const estimated = (0, pricing_js_1.estimateCost)(model, estimatedInputTokens, estimatedOutputTokens);
47
+ const estimated = estimateCost(model, estimatedInputTokens, estimatedOutputTokens);
48
48
  const key = this.budgetKey(entityId, budget.period);
49
49
  const currentSpend = parseFloat((await this.store.get(key)) ?? "0");
50
50
  if (currentSpend + estimated > budget.hardLimit) {
@@ -67,7 +67,7 @@ class CostTracker {
67
67
  }
68
68
  /** Record actual cost AFTER receiving response */
69
69
  async recordCost(entityId, model, inputTokens, outputTokens) {
70
- const cost = (0, pricing_js_1.estimateCost)(model, inputTokens, outputTokens);
70
+ const cost = estimateCost(model, inputTokens, outputTokens);
71
71
  const record = {
72
72
  entityId,
73
73
  model,
@@ -90,9 +90,27 @@ class CostTracker {
90
90
  await this.store.incrbyfloat(globalKey, cost);
91
91
  await this.store.expire(globalKey, this.periodSeconds(globalBudget.period) * 2);
92
92
  }
93
- this.records.push(record);
93
+ this.appendRecord(record);
94
94
  return record;
95
95
  }
96
+ /**
97
+ * Append a record with ring-buffer semantics to prevent unbounded memory growth.
98
+ * When maxRecords is 0, records are not retained.
99
+ */
100
+ appendRecord(record) {
101
+ if (this.maxRecords === 0)
102
+ return;
103
+ this.records.push(record);
104
+ if (this.records.length > this.maxRecords) {
105
+ // Drop oldest entries — O(1) amortized using splice(0, overflow)
106
+ const overflow = this.records.length - this.maxRecords;
107
+ this.records.splice(0, overflow);
108
+ }
109
+ }
110
+ /** Clear all in-memory records (e.g., after export) */
111
+ clearRecords() {
112
+ this.records.length = 0;
113
+ }
96
114
  /** Get current spend for an entity */
97
115
  async getCurrentSpend(entityId) {
98
116
  const budget = this.budgets.get(entityId);
@@ -132,5 +150,4 @@ class CostTracker {
132
150
  }
133
151
  }
134
152
  }
135
- exports.CostTracker = CostTracker;
136
153
  //# sourceMappingURL=tracker.js.map
package/dist/index.d.ts CHANGED
@@ -1,18 +1,49 @@
1
1
  export { AIShield } from "./shield.js";
2
- export { HeuristicScanner, type HeuristicConfig } from "./scanner/heuristic.js";
2
+ export { HeuristicScanner, normalizeForInjectionScan, collapseSpacedLetters, type HeuristicConfig, } from "./scanner/heuristic.js";
3
3
  export { PIIScanner } from "./scanner/pii.js";
4
4
  export { ScannerChain, type ChainConfig } from "./scanner/chain.js";
5
5
  export { injectCanary, checkCanaryLeak } from "./scanner/canary.js";
6
+ export { IngestionScanner, scanIngested, scanToolOutput, trustTierForSource, tryDecodeObfuscation, type IngestionScannerConfig, type IngestionScanResult, } from "./scanner/ingestion.js";
7
+ export { OutputScanner, scanOutput, type OutputScanConfig, type OutputScanResult, type OutputSink, } from "./scanner/output.js";
8
+ export { wrapContext, scanWrappedContext, assemblePrompt, flattenViolations, propagateTrust, type WrapContextInput, type AssembleOptions, type AgentHop, type PropagateTrustOptions, type TrustPropagationResult, } from "./context/wrap-context.js";
9
+ export { createAsyncJudge, type AsyncJudge, type AsyncJudgeConfig, type JudgeVerdict, type JudgeBackend, type JudgeBackendLike, } from "./judge/async-judge.js";
10
+ export { mintMemoryCanary, verifyMemoryCanary, rotateMemoryCanary, buildSentinelEntry, bulkVerify, type MintMemoryCanaryOptions, } from "./canary/memory.js";
6
11
  export { PolicyEngine, type PolicyPreset } from "./policy/engine.js";
7
12
  export { ToolPolicyScanner } from "./policy/tools.js";
13
+ export { CircuitBreakerRegistry, makeBreakerScope, type CircuitBreakerOptions, } from "./policy/circuit-breaker.js";
8
14
  export { CostTracker, type RedisLike } from "./cost/tracker.js";
9
15
  export { detectAnomaly, type AnomalyResult } from "./cost/anomaly.js";
10
16
  export { getModelPricing, estimateCost, MODEL_PRICING } from "./cost/pricing.js";
11
17
  export { AuditLogger, ConsoleAuditStore, MemoryAuditStore } from "./audit/logger.js";
12
18
  export type { AuditStore } from "./audit/types.js";
13
19
  export { ScanLRUCache, type LRUCacheConfig } from "./cache/lru.js";
14
- export type { ScanDecision, ScanResult, ScannerResult, Scanner, ScanContext, Violation, ViolationType, PIIType, PIIAction, PIIEntity, PIIConfig, ToolCall, ToolPermissions, ToolPolicy, ToolManifestPin, BudgetPeriod, BudgetConfig, CostEstimate, CostRecord, BudgetCheckResult, ModelPricing, AuditRecord, AuditConfig, ShieldConfig, InjectionConfig, CostConfig, CacheConfig, ToolConfig, PresetName, } from "./types.js";
20
+ export type { ScanDecision, ScanResult, ScannerResult, Scanner, ScanContext, Violation, ViolationType, IngestionSource, TrustTier, ContextSegment, WrappedContext, MemoryCanaryEntry, MemoryCanaryVerification, CircuitState, CircuitBreakerConfig, CircuitBreakerDecision, CounterStoreLike, PIIType, PIIAction, PIIEntity, PIIConfig, ToolCall, ToolPermissions, ToolPolicy, ToolManifestPin, BudgetPeriod, BudgetConfig, CostEstimate, CostRecord, BudgetCheckResult, ModelPricing, AuditRecord, AuditConfig, ShieldConfig, InjectionConfig, CostConfig, CacheConfig, ToolConfig, PresetName, } from "./types.js";
15
21
  import type { ShieldConfig, ScanResult, ScanContext } from "./types.js";
16
- /** Quick scan — one line, maximum protection */
22
+ /**
23
+ * Quick scan — one line, maximum protection.
24
+ *
25
+ * **Performance warning:** This creates a new AIShield instance on every call.
26
+ * For production use with multiple calls, create a single `new AIShield(config)`
27
+ * instance and reuse it — this avoids repeated scanner chain setup and teardown.
28
+ *
29
+ * Use `createShieldSingleton()` for a cached version that reuses a single instance.
30
+ */
17
31
  export declare function shield(input: string, configOrContext?: ShieldConfig | ScanContext): Promise<ScanResult>;
32
+ /**
33
+ * Create a cached shield function that reuses a single AIShield instance.
34
+ * Much better performance than `shield()` for repeated calls.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const scan = createShieldSingleton({ injection: { strictness: "high" } });
39
+ * const r1 = await scan("input 1");
40
+ * const r2 = await scan("input 2");
41
+ * // Call scan.close() when done (e.g., on process exit)
42
+ * await scan.close();
43
+ * ```
44
+ */
45
+ export declare function createShieldSingleton(config?: ShieldConfig): {
46
+ (input: string, context?: ScanContext): Promise<ScanResult>;
47
+ close(): Promise<void>;
48
+ };
18
49
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvC,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EAAE,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGjF,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGnE,YAAY,EAEV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,OAAO,EACP,WAAW,EACX,SAAS,EACT,aAAa,EAEb,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,EAET,QAAQ,EACR,eAAe,EACf,UAAU,EACV,eAAe,EAEf,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,YAAY,EAEZ,WAAW,EACX,WAAW,EAEX,YAAY,EACZ,eAAe,EACf,UAAU,EACV,WAAW,EACX,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC;AAKpB,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExE,gDAAgD;AAChD,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,eAAe,CAAC,EAAE,YAAY,GAAG,WAAW,GAC3C,OAAO,CAAC,UAAU,CAAC,CAarB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}