@bulwark-ai/gateway 0.1.2 → 0.1.4

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/dist/index.js CHANGED
@@ -8,6 +8,9 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __esm = (fn, res) => function __init() {
9
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
10
  };
11
+ var __commonJS = (cb, mod) => function __require() {
12
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
+ };
11
14
  var __export = (target, all) => {
12
15
  for (var name in all)
13
16
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -134,6 +137,11 @@ CREATE INDEX IF NOT EXISTS idx_bulwark_chunks_tenant ON bulwark_chunks(tenant_id
134
137
  _initialized = false;
135
138
  _initPromise = null;
136
139
  constructor(connectionString, poolConfig) {
140
+ if (!process.env.BULWARK_POSTGRES_EXPERIMENTAL) {
141
+ throw new Error(
142
+ "[Bulwark] Postgres adapter is NOT production-ready. The sync Database interface is incompatible with pg's async driver \u2014 reads return empty data, writes are fire-and-forget. Use SQLite for now. Set BULWARK_POSTGRES_EXPERIMENTAL=1 to bypass this check at your own risk."
143
+ );
144
+ }
137
145
  this.connectionString = connectionString;
138
146
  this.poolConfig = {
139
147
  max: poolConfig?.max || 20,
@@ -202,6 +210,96 @@ CREATE INDEX IF NOT EXISTS idx_bulwark_chunks_tenant ON bulwark_chunks(tenant_id
202
210
  }
203
211
  });
204
212
 
213
+ // package.json
214
+ var require_package = __commonJS({
215
+ "package.json"(exports2, module2) {
216
+ module2.exports = {
217
+ name: "@bulwark-ai/gateway",
218
+ version: "0.1.4",
219
+ description: "Enterprise AI governance gateway \u2014 PII detection, prompt injection guard, budget control, audit logging, RAG, multi-tenant. Drop into any Node.js app.",
220
+ main: "dist/index.js",
221
+ types: "dist/index.d.ts",
222
+ exports: {
223
+ ".": {
224
+ types: "./dist/index.d.ts",
225
+ import: "./dist/index.mjs",
226
+ require: "./dist/index.js"
227
+ }
228
+ },
229
+ files: ["dist", "README.md", "LICENSE"],
230
+ scripts: {
231
+ build: "tsup",
232
+ dev: "tsup --watch",
233
+ test: "vitest run",
234
+ "test:watch": "vitest",
235
+ lint: "tsc --noEmit"
236
+ },
237
+ keywords: [
238
+ "ai",
239
+ "gateway",
240
+ "ai",
241
+ "llm",
242
+ "gateway",
243
+ "governance",
244
+ "pii",
245
+ "prompt-injection",
246
+ "gdpr",
247
+ "soc2",
248
+ "hipaa",
249
+ "ccpa",
250
+ "audit",
251
+ "openai",
252
+ "anthropic",
253
+ "mistral",
254
+ "google",
255
+ "ollama",
256
+ "multi-tenant",
257
+ "rag",
258
+ "enterprise",
259
+ "budget",
260
+ "compliance",
261
+ "security",
262
+ "streaming",
263
+ "rate-limit",
264
+ "cost-tracking",
265
+ "admin"
266
+ ],
267
+ author: "Bulwark AI",
268
+ license: "SEE LICENSE IN LICENSE",
269
+ dependencies: {
270
+ "@anthropic-ai/sdk": "^0.39.0",
271
+ "better-sqlite3": "^11.0.0",
272
+ openai: "^4.80.0",
273
+ uuid: "^11.0.0"
274
+ },
275
+ devDependencies: {
276
+ "@types/better-sqlite3": "^7.6.0",
277
+ "@types/express": "^5.0.6",
278
+ "@types/node": "^22.0.0",
279
+ "@types/react": "^19.2.14",
280
+ "@types/uuid": "^10.0.0",
281
+ react: "^19.2.4",
282
+ tsup: "^8.0.0",
283
+ typescript: "^5.7.0",
284
+ vitest: "^3.0.0"
285
+ },
286
+ peerDependencies: {
287
+ express: "^4.0.0 || ^5.0.0",
288
+ pg: "^8.0.0",
289
+ ioredis: "^5.0.0"
290
+ },
291
+ peerDependenciesMeta: {
292
+ express: { optional: true },
293
+ pg: { optional: true },
294
+ ioredis: { optional: true }
295
+ },
296
+ engines: {
297
+ node: ">=18"
298
+ }
299
+ };
300
+ }
301
+ });
302
+
205
303
  // src/index.ts
206
304
  var index_exports = {};
207
305
  __export(index_exports, {
@@ -243,7 +341,6 @@ __export(index_exports, {
243
341
  createDatabase: () => createDatabase,
244
342
  createNextAuditHandler: () => createNextAuditHandler,
245
343
  createNextHandler: () => createNextHandler,
246
- createStreamAdapter: () => createStreamAdapter,
247
344
  getDashboard: () => getDashboard,
248
345
  hardenSystemPrompt: () => hardenSystemPrompt,
249
346
  parseCSV: () => parseCSV,
@@ -273,16 +370,36 @@ var PII_PATTERNS = {
273
370
  // Generic — covers most EU national ID formats
274
371
  medical_id: /\b(?:NHS|EHIC|SVN|AMM)[-\s]?\d{6,12}\b/gi
275
372
  };
373
+ function luhnCheck(num) {
374
+ const digits = num.replace(/[\s-]/g, "");
375
+ if (!/^\d+$/.test(digits) || digits.length < 13) return false;
376
+ let sum = 0;
377
+ let alternate = false;
378
+ for (let i = digits.length - 1; i >= 0; i--) {
379
+ let n = parseInt(digits[i], 10);
380
+ if (alternate) {
381
+ n *= 2;
382
+ if (n > 9) n -= 9;
383
+ }
384
+ sum += n;
385
+ alternate = !alternate;
386
+ }
387
+ return sum % 10 === 0;
388
+ }
276
389
  var PIIDetector = class {
277
- config;
390
+ _config;
278
391
  activeTypes;
279
392
  constructor(config) {
280
- this.config = config;
393
+ this._config = config;
281
394
  this.activeTypes = config.types || ["email", "phone", "ssn", "credit_card", "iban"];
282
395
  }
396
+ /** Whether PII detection is enabled */
397
+ get config() {
398
+ return this._config;
399
+ }
283
400
  /** Scan text for PII. Returns matches and optionally redacted text. */
284
401
  scan(text) {
285
- if (!this.config.enabled) return { text, matches: [], blocked: false, redacted: false };
402
+ if (!this._config.enabled) return { text, matches: [], blocked: false, redacted: false };
286
403
  const matches = [];
287
404
  for (const type of this.activeTypes) {
288
405
  const pattern = PII_PATTERNS[type];
@@ -290,17 +407,19 @@ var PIIDetector = class {
290
407
  const regex = new RegExp(pattern.source, pattern.flags);
291
408
  let match;
292
409
  while ((match = regex.exec(text)) !== null) {
410
+ if (type === "credit_card" && !luhnCheck(match[0])) continue;
293
411
  matches.push({
294
412
  type,
295
- value: match[0],
413
+ value: "[REDACTED]",
414
+ // SECURITY: Never store raw PII values in match objects
296
415
  redacted: `[${type.toUpperCase()}]`,
297
416
  start: match.index,
298
417
  end: match.index + match[0].length
299
418
  });
300
419
  }
301
420
  }
302
- if (this.config.customPatterns) {
303
- for (const custom of this.config.customPatterns) {
421
+ if (this._config.customPatterns) {
422
+ for (const custom of this._config.customPatterns) {
304
423
  try {
305
424
  if (/\([^)]*[+*][^)]*\)[+*]/.test(custom.pattern)) continue;
306
425
  if (/(\.\*){3,}/.test(custom.pattern)) continue;
@@ -315,7 +434,7 @@ var PIIDetector = class {
315
434
  }
316
435
  matches.push({
317
436
  type: custom.name,
318
- value: match[0],
437
+ value: "[REDACTED]",
319
438
  redacted: `[${custom.name.toUpperCase()}]`,
320
439
  start: match.index,
321
440
  end: match.index + match[0].length
@@ -326,7 +445,7 @@ var PIIDetector = class {
326
445
  }
327
446
  }
328
447
  if (matches.length === 0) return { text, matches: [], blocked: false, redacted: false };
329
- const action = this.config.action || "warn";
448
+ const action = this._config.action || "warn";
330
449
  if (action === "block") {
331
450
  return { text, matches, blocked: true, redacted: false };
332
451
  }
@@ -394,11 +513,16 @@ var PolicyEngine = class {
394
513
  break;
395
514
  case "regex_block":
396
515
  if (policy.regex) {
397
- const regex = new RegExp(policy.regex, "gi");
398
- const match = regex.exec(text);
399
- if (match) {
400
- violated = true;
401
- matchedPattern = match[0];
516
+ if (/\([^)]*[+*][^)]*\)[+*]/.test(policy.regex)) break;
517
+ if (/(\.\*){3,}/.test(policy.regex)) break;
518
+ try {
519
+ const regex = new RegExp(policy.regex, "gi");
520
+ const match = regex.exec(text);
521
+ if (match) {
522
+ violated = true;
523
+ matchedPattern = match[0];
524
+ }
525
+ } catch {
402
526
  }
403
527
  }
404
528
  break;
@@ -564,7 +688,7 @@ var CostCalculator = class {
564
688
  const outputCost = outputTokens / 1e6 * pricing.output;
565
689
  return {
566
690
  model,
567
- provider: model.startsWith("claude") ? "anthropic" : "openai",
691
+ provider: this.detectProvider(model),
568
692
  inputTokens,
569
693
  outputTokens,
570
694
  inputCost: Math.round(inputCost * 1e6) / 1e6,
@@ -573,6 +697,15 @@ var CostCalculator = class {
573
697
  totalCost: Math.round((inputCost + outputCost) * 1e6) / 1e6
574
698
  };
575
699
  }
700
+ /** Detect provider from model name */
701
+ detectProvider(model) {
702
+ const m = model.toLowerCase();
703
+ if (m.startsWith("claude")) return "anthropic";
704
+ if (m.startsWith("mistral") || m.startsWith("codestral") || m.startsWith("pixtral")) return "mistral";
705
+ if (m.startsWith("gemini") || m.startsWith("palm")) return "google";
706
+ if (m.startsWith("llama") || m.startsWith("phi") || m.startsWith("qwen") || m.startsWith("deepseek") || m.startsWith("codellama")) return "ollama";
707
+ return "openai";
708
+ }
576
709
  /** Update pricing for a model */
577
710
  setModelPrice(model, input, output) {
578
711
  this.pricing[model] = { input, output };
@@ -584,6 +717,8 @@ var BudgetManager = class {
584
717
  enabled;
585
718
  config;
586
719
  db;
720
+ /** Tracks which thresholds have already been crossed per scope to avoid duplicate alerts */
721
+ crossedThresholds = /* @__PURE__ */ new Map();
587
722
  constructor(db, config) {
588
723
  this.db = db;
589
724
  this.config = config;
@@ -607,8 +742,12 @@ var BudgetManager = class {
607
742
  return { ok: this.config.onExceeded !== "block", used, limit, costUsd: row?.total_cost || 0 };
608
743
  }
609
744
  if (limit > 0 && this.config.alertThresholds && this.config.onAlert) {
745
+ const scopeKey = `user:${scope.userId}`;
746
+ if (!this.crossedThresholds.has(scopeKey)) this.crossedThresholds.set(scopeKey, /* @__PURE__ */ new Set());
747
+ const crossed = this.crossedThresholds.get(scopeKey);
610
748
  for (const threshold of this.config.alertThresholds) {
611
- if (used / limit >= threshold) {
749
+ if (used / limit >= threshold && !crossed.has(threshold)) {
750
+ crossed.add(threshold);
612
751
  this.config.onAlert({ type: "user", id: scope.userId, threshold, used, limit, costUsd: row?.total_cost || 0 });
613
752
  }
614
753
  }
@@ -829,6 +968,7 @@ var SQLiteDatabase = class {
829
968
  };
830
969
  function createDatabase(connection) {
831
970
  if (connection.startsWith("postgres://") || connection.startsWith("postgresql://")) {
971
+ console.warn("[bulwark] WARNING: Postgres adapter is EXPERIMENTAL and NOT production-ready. Governance controls may not work reliably. Use SQLite for production until async DB layer ships in v0.2.");
832
972
  const { PostgresDatabase: PostgresDatabase2 } = (init_database_postgres(), __toCommonJS(database_postgres_exports));
833
973
  return new PostgresDatabase2(connection);
834
974
  }
@@ -838,9 +978,36 @@ function createDatabase(connection) {
838
978
 
839
979
  // src/providers/openai.ts
840
980
  var import_openai = __toESM(require("openai"));
981
+
982
+ // src/providers/base.ts
983
+ function validateBaseUrl(url) {
984
+ try {
985
+ const parsed = new URL(url);
986
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1";
987
+ if (parsed.protocol !== "https:" && !isLocalhost) {
988
+ throw new Error(`Insecure protocol: ${parsed.protocol}. Use HTTPS.`);
989
+ }
990
+ const blockedHosts = ["169.254.169.254", "metadata.google.internal", "100.100.100.200"];
991
+ if (blockedHosts.includes(parsed.hostname)) {
992
+ throw new Error(`Blocked host: ${parsed.hostname}`);
993
+ }
994
+ if (!isLocalhost) {
995
+ const parts = parsed.hostname.split(".").map(Number);
996
+ if (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168) {
997
+ throw new Error(`Private IP blocked: ${parsed.hostname}`);
998
+ }
999
+ }
1000
+ } catch (err) {
1001
+ if (err instanceof Error && err.message.includes("blocked") || err instanceof Error && err.message.includes("Insecure")) throw err;
1002
+ throw new Error(`Invalid baseUrl: ${url}`);
1003
+ }
1004
+ }
1005
+
1006
+ // src/providers/openai.ts
841
1007
  var OpenAIProvider = class {
842
1008
  client;
843
1009
  constructor(config) {
1010
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
844
1011
  this.client = new import_openai.default({ apiKey: config.apiKey, baseURL: config.baseUrl });
845
1012
  }
846
1013
  async chat(request) {
@@ -895,6 +1062,7 @@ var import_sdk = __toESM(require("@anthropic-ai/sdk"));
895
1062
  var AnthropicProvider = class {
896
1063
  client;
897
1064
  constructor(config) {
1065
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
898
1066
  this.client = new import_sdk.default({ apiKey: config.apiKey, baseURL: config.baseUrl });
899
1067
  }
900
1068
  async chat(request) {
@@ -953,6 +1121,7 @@ var MistralProvider = class {
953
1121
  apiKey;
954
1122
  baseUrl;
955
1123
  constructor(config) {
1124
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
956
1125
  this.apiKey = config.apiKey;
957
1126
  this.baseUrl = config.baseUrl || "https://api.mistral.ai/v1";
958
1127
  }
@@ -994,6 +1163,7 @@ var GoogleProvider = class {
994
1163
  apiKey;
995
1164
  baseUrl;
996
1165
  constructor(config) {
1166
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
997
1167
  this.apiKey = config.apiKey;
998
1168
  this.baseUrl = config.baseUrl || "https://generativelanguage.googleapis.com/v1beta";
999
1169
  }
@@ -1004,9 +1174,9 @@ var GoogleProvider = class {
1004
1174
  role: m.role === "assistant" ? "model" : "user",
1005
1175
  parts: [{ text: m.content }]
1006
1176
  }));
1007
- const response = await fetch(`${this.baseUrl}/models/${model}:generateContent?key=${this.apiKey}`, {
1177
+ const response = await fetch(`${this.baseUrl}/models/${model}:generateContent`, {
1008
1178
  method: "POST",
1009
- headers: { "Content-Type": "application/json" },
1179
+ headers: { "Content-Type": "application/json", "x-goog-api-key": this.apiKey },
1010
1180
  body: JSON.stringify({
1011
1181
  contents,
1012
1182
  systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction.content }] } : void 0,
@@ -1040,6 +1210,7 @@ var GoogleProvider = class {
1040
1210
  var OllamaProvider = class {
1041
1211
  baseUrl;
1042
1212
  constructor(config) {
1213
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
1043
1214
  this.baseUrl = config.baseUrl || "http://localhost:11434";
1044
1215
  }
1045
1216
  async chat(request) {
@@ -1075,6 +1246,50 @@ var OllamaProvider = class {
1075
1246
  }
1076
1247
  };
1077
1248
 
1249
+ // src/providers/azure-openai.ts
1250
+ var AzureOpenAIProvider = class {
1251
+ apiKey;
1252
+ endpoint;
1253
+ apiVersion;
1254
+ constructor(config) {
1255
+ this.apiKey = config.apiKey;
1256
+ this.endpoint = config.baseUrl || "";
1257
+ this.apiVersion = config.apiVersion || "2024-10-21";
1258
+ if (!this.endpoint) throw new Error("Azure OpenAI requires baseUrl (endpoint URL)");
1259
+ }
1260
+ async chat(request) {
1261
+ const url = `${this.endpoint}/chat/completions?api-version=${this.apiVersion}`;
1262
+ const response = await fetch(url, {
1263
+ method: "POST",
1264
+ headers: {
1265
+ "Content-Type": "application/json",
1266
+ "api-key": this.apiKey
1267
+ },
1268
+ body: JSON.stringify({
1269
+ messages: request.messages.map((m) => ({ role: m.role, content: m.content })),
1270
+ temperature: request.temperature,
1271
+ max_tokens: request.maxTokens,
1272
+ top_p: request.topP,
1273
+ stop: request.stop
1274
+ })
1275
+ });
1276
+ if (!response.ok) {
1277
+ const err = await response.text();
1278
+ throw new Error(`Azure OpenAI error (${response.status}): ${err}`);
1279
+ }
1280
+ const data = await response.json();
1281
+ return {
1282
+ content: data.choices[0]?.message?.content || "",
1283
+ usage: {
1284
+ inputTokens: data.usage?.prompt_tokens || 0,
1285
+ outputTokens: data.usage?.completion_tokens || 0,
1286
+ totalTokens: data.usage?.total_tokens || 0
1287
+ },
1288
+ finishReason: data.choices[0]?.finish_reason
1289
+ };
1290
+ }
1291
+ };
1292
+
1078
1293
  // src/rag/knowledge-base.ts
1079
1294
  var import_uuid2 = require("uuid");
1080
1295
 
@@ -1318,6 +1533,8 @@ var KnowledgeBase = class {
1318
1533
  if (tenantId) {
1319
1534
  sql += " WHERE tenant_id = ?";
1320
1535
  params.push(tenantId);
1536
+ } else {
1537
+ sql += " WHERE (tenant_id IS NULL OR tenant_id = '')";
1321
1538
  }
1322
1539
  sql += " ORDER BY created_at DESC";
1323
1540
  return this.db.queryAll(sql, params);
@@ -1340,8 +1557,11 @@ var MemoryCacheStore = class {
1340
1557
  store = /* @__PURE__ */ new Map();
1341
1558
  counters = /* @__PURE__ */ new Map();
1342
1559
  cleanupTimer;
1343
- constructor() {
1560
+ maxEntries;
1561
+ constructor(options) {
1562
+ this.maxEntries = options?.maxEntries || 1e4;
1344
1563
  this.cleanupTimer = setInterval(() => this.cleanup(), 6e4);
1564
+ this.cleanupTimer.unref();
1345
1565
  }
1346
1566
  /** Stop background cleanup — call on shutdown */
1347
1567
  close() {
@@ -1368,6 +1588,10 @@ var MemoryCacheStore = class {
1368
1588
  return entry.value;
1369
1589
  }
1370
1590
  async set(key, value, ttlSeconds) {
1591
+ if (this.store.size >= this.maxEntries) {
1592
+ const oldestKey = this.store.keys().next().value;
1593
+ if (oldestKey) this.store.delete(oldestKey);
1594
+ }
1371
1595
  this.store.set(key, {
1372
1596
  value,
1373
1597
  expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1e3 : void 0
@@ -1417,9 +1641,7 @@ var RateLimiter = class {
1417
1641
  if (!scopeId) return { allowed: true, remaining: Infinity, resetAt: 0 };
1418
1642
  const windowKey = `ratelimit:${this.config.scope}:${scopeId}:${this.currentWindow()}`;
1419
1643
  const count = await this.store.incr(windowKey);
1420
- if (count === 1) {
1421
- await this.store.expire(windowKey, this.config.windowSeconds);
1422
- }
1644
+ await this.store.expire(windowKey, this.config.windowSeconds);
1423
1645
  const remaining = Math.max(0, this.config.maxRequests - count);
1424
1646
  const resetAt = Math.ceil(Date.now() / 1e3 / this.config.windowSeconds) * this.config.windowSeconds * 1e3;
1425
1647
  return {
@@ -1482,15 +1704,22 @@ var TenantManager = class {
1482
1704
  if (updates.name) this.db.run("UPDATE bulwark_tenants SET name = ? WHERE id = ?", [updates.name, id]);
1483
1705
  if (updates.settings) this.db.run("UPDATE bulwark_tenants SET settings = ? WHERE id = ?", [JSON.stringify(updates.settings), id]);
1484
1706
  }
1485
- /** Delete a tenant and ALL its data */
1707
+ /** Delete a tenant and ALL its data (transactional) */
1486
1708
  delete(id) {
1487
- this.db.run("DELETE FROM bulwark_chunks WHERE tenant_id = ?", [id]);
1488
- this.db.run("DELETE FROM bulwark_knowledge_sources WHERE tenant_id = ?", [id]);
1489
- this.db.run("DELETE FROM bulwark_usage WHERE tenant_id = ?", [id]);
1490
- this.db.run("DELETE FROM bulwark_audit WHERE tenant_id = ?", [id]);
1491
- this.db.run("DELETE FROM bulwark_policies WHERE tenant_id = ?", [id]);
1492
- this.db.run("DELETE FROM bulwark_budgets WHERE tenant_id = ?", [id]);
1493
- this.db.run("DELETE FROM bulwark_tenants WHERE id = ?", [id]);
1709
+ this.db.run("BEGIN TRANSACTION", []);
1710
+ try {
1711
+ this.db.run("DELETE FROM bulwark_chunks WHERE tenant_id = ?", [id]);
1712
+ this.db.run("DELETE FROM bulwark_knowledge_sources WHERE tenant_id = ?", [id]);
1713
+ this.db.run("DELETE FROM bulwark_usage WHERE tenant_id = ?", [id]);
1714
+ this.db.run("DELETE FROM bulwark_audit WHERE tenant_id = ?", [id]);
1715
+ this.db.run("DELETE FROM bulwark_policies WHERE tenant_id = ?", [id]);
1716
+ this.db.run("DELETE FROM bulwark_budgets WHERE tenant_id = ?", [id]);
1717
+ this.db.run("DELETE FROM bulwark_tenants WHERE id = ?", [id]);
1718
+ this.db.run("COMMIT", []);
1719
+ } catch (err) {
1720
+ this.db.run("ROLLBACK", []);
1721
+ throw err;
1722
+ }
1494
1723
  }
1495
1724
  /** Get usage stats for a tenant */
1496
1725
  getUsage(id) {
@@ -1535,10 +1764,23 @@ var AIGateway = class {
1535
1764
  timeoutMs;
1536
1765
  retryConfig;
1537
1766
  fallbacks;
1767
+ failMode;
1768
+ _enabled;
1538
1769
  initialized = false;
1539
1770
  shutdownRequested = false;
1540
1771
  activeRequests = 0;
1541
1772
  constructor(config) {
1773
+ if (config.mode) {
1774
+ const presets = {
1775
+ strict: { pii: { enabled: true, action: "block" }, budgets: { enabled: true, defaultUserLimit: 1e5, onExceeded: "block" }, promptGuard: { enabled: true, action: "block", sensitivity: "high" }, audit: true },
1776
+ balanced: { pii: { enabled: true, action: "redact" }, budgets: { enabled: true, defaultUserLimit: 5e5 }, audit: true },
1777
+ dev: { pii: { enabled: false }, budgets: { enabled: false }, audit: true }
1778
+ };
1779
+ const preset = presets[config.mode];
1780
+ config = { ...preset, ...config, pii: config.pii ?? preset.pii, budgets: config.budgets ?? preset.budgets };
1781
+ }
1782
+ this.failMode = config.failMode || "fail-closed";
1783
+ this._enabled = config.enabled !== false;
1542
1784
  if (!config.providers || Object.keys(config.providers).length === 0) {
1543
1785
  throw new BulwarkError("INVALID_CONFIG", "At least one provider must be configured");
1544
1786
  }
@@ -1576,6 +1818,7 @@ var AIGateway = class {
1576
1818
  if (config.providers.mistral) this.providers.set("mistral", new MistralProvider(config.providers.mistral));
1577
1819
  if (config.providers.google) this.providers.set("google", new GoogleProvider(config.providers.google));
1578
1820
  if (config.providers.ollama) this.providers.set("ollama", new OllamaProvider(config.providers.ollama));
1821
+ if (config.providers.azure) this.providers.set("azure", new AzureOpenAIProvider(config.providers.azure));
1579
1822
  if (config.rag?.enabled && config.providers.openai?.apiKey) {
1580
1823
  this.kb = new KnowledgeBase(this.db, config.rag, config.providers.openai.apiKey);
1581
1824
  }
@@ -1595,12 +1838,16 @@ var AIGateway = class {
1595
1838
  * Pipeline: Validate → PII scan → Policy check → Rate limit → Budget check → [RAG augment] → LLM call (with timeout) → Token count → Cost calc → Audit log
1596
1839
  */
1597
1840
  async chat(request) {
1841
+ if (this._enabled === false) {
1842
+ throw new BulwarkError("GATEWAY_DISABLED", "AI gateway is disabled. Set enabled: true to resume.");
1843
+ }
1598
1844
  if (this.shutdownRequested) {
1599
1845
  throw new BulwarkError("SHUTTING_DOWN", "Gateway is shutting down, not accepting new requests");
1600
1846
  }
1601
1847
  await this.init();
1602
1848
  this.activeRequests++;
1603
1849
  const start = Date.now();
1850
+ const trace = request.debug ? [] : void 0;
1604
1851
  try {
1605
1852
  this.validateRequest(request);
1606
1853
  const model = request.model || "gpt-4o";
@@ -1690,7 +1937,7 @@ var AIGateway = class {
1690
1937
  const hasSystemMsg = messages.some((m) => m.role === "system");
1691
1938
  if (hasSystemMsg) {
1692
1939
  messages = messages.map(
1693
- (m) => m.role === "system" ? { ...m, content: hardenSystemPrompt(m.content, { preventExtraction: true, enforceGDPR: !!this.piiDetector }) } : m
1940
+ (m) => m.role === "system" ? { ...m, content: hardenSystemPrompt(m.content, { preventExtraction: true, enforceGDPR: this.piiDetector.config?.enabled ?? false }) } : m
1694
1941
  );
1695
1942
  }
1696
1943
  if (request.knowledgeBase && this.kb) {
@@ -1711,7 +1958,7 @@ ${context}
1711
1958
  if (hasSystemMsg) {
1712
1959
  messages = messages.map((m) => m.role === "system" ? { ...m, content: m.content + ragInstruction } : m);
1713
1960
  } else {
1714
- const basePrompt = hardenSystemPrompt("You are a helpful assistant.", { preventExtraction: true, enforceGDPR: !!this.piiDetector });
1961
+ const basePrompt = hardenSystemPrompt("You are a helpful assistant.", { preventExtraction: true, enforceGDPR: this.piiDetector.config?.enabled ?? false });
1715
1962
  messages = [{ role: "system", content: basePrompt + ragInstruction }, ...messages];
1716
1963
  }
1717
1964
  }
@@ -1753,7 +2000,8 @@ ${context}
1753
2000
  outputTokens: llmResponse.usage.outputTokens,
1754
2001
  costUsd: cost.totalCost,
1755
2002
  durationMs,
1756
- piiDetections: totalPii || void 0
2003
+ piiDetections: totalPii || void 0,
2004
+ metadata: request.metadata
1757
2005
  });
1758
2006
  return {
1759
2007
  content: outputContent,
@@ -1767,7 +2015,8 @@ ${context}
1767
2015
  ] : void 0,
1768
2016
  sources,
1769
2017
  auditId,
1770
- durationMs
2018
+ durationMs,
2019
+ trace
1771
2020
  };
1772
2021
  } finally {
1773
2022
  this.activeRequests--;
@@ -1795,26 +2044,31 @@ ${context}
1795
2044
  try {
1796
2045
  this.validateRequest(request);
1797
2046
  const model = request.model || "gpt-4o";
1798
- const { provider, providerInstance } = this.resolveProvider(model);
1799
2047
  let messages = [...request.messages];
1800
- const lastMsg = messages[messages.length - 1];
1801
- if (lastMsg?.role === "user") {
1802
- const guardResult = this.promptGuard.scan(lastMsg.content);
1803
- if (!guardResult.safe && !guardResult.sanitizedText) {
1804
- throw new BulwarkError("PROMPT_INJECTION", `Prompt injection detected: ${guardResult.injections.map((i) => i.pattern).join(", ")}`);
1805
- }
1806
- if (guardResult.sanitizedText) {
1807
- messages = [...messages.slice(0, -1), { ...lastMsg, content: guardResult.sanitizedText }];
2048
+ for (let i = 0; i < messages.length; i++) {
2049
+ const msg = messages[i];
2050
+ if (msg.role === "user") {
2051
+ const guardResult = this.promptGuard.scan(msg.content);
2052
+ if (!guardResult.safe) {
2053
+ if (guardResult.sanitizedText) {
2054
+ messages = [...messages.slice(0, i), { ...msg, content: guardResult.sanitizedText }, ...messages.slice(i + 1)];
2055
+ } else {
2056
+ throw new BulwarkError("PROMPT_INJECTION", `Prompt injection detected in message ${i}: ${guardResult.injections.map((j) => j.pattern).join(", ")}`, { injections: guardResult.injections });
2057
+ }
2058
+ }
1808
2059
  }
1809
2060
  }
1810
2061
  const piiTypes = [];
1811
2062
  if (request.pii !== false) {
1812
- const userMsg = messages[messages.length - 1];
1813
- if (userMsg?.role === "user") {
1814
- const result = this.piiDetector.scan(userMsg.content);
2063
+ for (let i = 0; i < messages.length; i++) {
2064
+ const msg = messages[i];
2065
+ if (msg.role !== "user") continue;
2066
+ const result = this.piiDetector.scan(msg.content);
1815
2067
  piiTypes.push(...result.matches.map((m) => m.type));
1816
- if (result.blocked) throw new BulwarkError("PII_BLOCKED", "PII detected and blocked");
1817
- if (result.redacted) messages = [...messages.slice(0, -1), { ...userMsg, content: result.text }];
2068
+ if (result.blocked) throw new BulwarkError("PII_BLOCKED", `PII detected and blocked in message ${i}`);
2069
+ if (result.redacted) {
2070
+ messages = [...messages.slice(0, i), { ...msg, content: result.text }, ...messages.slice(i + 1)];
2071
+ }
1818
2072
  }
1819
2073
  }
1820
2074
  if (!request.skipPolicies) {
@@ -1830,6 +2084,12 @@ ${context}
1830
2084
  const allowed = await this.budgetManager.checkBudget({ userId: request.userId, teamId: request.teamId, tenantId: request.tenantId });
1831
2085
  if (!allowed.ok) throw new BulwarkError("BUDGET_EXCEEDED", "Budget exceeded");
1832
2086
  }
2087
+ const hasSystemMsg = messages.some((m) => m.role === "system");
2088
+ if (hasSystemMsg) {
2089
+ messages = messages.map(
2090
+ (m) => m.role === "system" ? { ...m, content: hardenSystemPrompt(m.content, { preventExtraction: true, enforceGDPR: this.piiDetector.config?.enabled ?? false }) } : m
2091
+ );
2092
+ }
1833
2093
  let sources = void 0;
1834
2094
  if (request.knowledgeBase && this.kb) {
1835
2095
  const results = await this.kb.search(messages[messages.length - 1]?.content || "", { tenantId: request.tenantId, topK: 6 });
@@ -1838,14 +2098,20 @@ ${context}
1838
2098
  const context = results.map((r, i) => `[${i + 1}] ${r.chunk.sourceName}: ${r.chunk.content}`).join("\n\n");
1839
2099
  const ragInstruction = `
1840
2100
 
2101
+ Use ONLY the following knowledge base context to answer. Cite sources using [1], [2] etc.
2102
+
1841
2103
  --- KNOWLEDGE BASE CONTEXT ---
1842
2104
  ${context}
1843
2105
  --- END CONTEXT ---`;
1844
- const sysMsg = messages.find((m) => m.role === "system");
1845
- if (sysMsg) messages = messages.map((m) => m.role === "system" ? { ...m, content: m.content + ragInstruction } : m);
1846
- else messages = [{ role: "system", content: "You are a helpful assistant." + ragInstruction }, ...messages];
2106
+ if (hasSystemMsg) {
2107
+ messages = messages.map((m) => m.role === "system" ? { ...m, content: m.content + ragInstruction } : m);
2108
+ } else {
2109
+ const basePrompt = hardenSystemPrompt("You are a helpful assistant.", { preventExtraction: true, enforceGDPR: this.piiDetector.config?.enabled ?? false });
2110
+ messages = [{ role: "system", content: basePrompt + ragInstruction }, ...messages];
2111
+ }
1847
2112
  }
1848
2113
  }
2114
+ const { provider, providerInstance } = this.resolveProvider(model);
1849
2115
  if (sources) yield { type: "sources", sources };
1850
2116
  if (piiTypes.length > 0) yield { type: "pii_warning", piiTypes };
1851
2117
  if (!providerInstance.chatStream) {
@@ -1971,6 +2237,7 @@ ${context}
1971
2237
  if (m.startsWith("mistral") || m.startsWith("codestral") || m.startsWith("pixtral")) return this.getProvider("mistral");
1972
2238
  if (m.startsWith("gemini") || m.startsWith("palm")) return this.getProvider("google");
1973
2239
  if (m.startsWith("llama") || m.startsWith("phi") || m.startsWith("qwen") || m.startsWith("deepseek") || m.startsWith("codellama")) return this.getProvider("ollama");
2240
+ if (this.providers.has("azure")) return this.getProvider("azure");
1974
2241
  return this.getProvider("openai");
1975
2242
  }
1976
2243
  getProvider(name) {
@@ -2010,6 +2277,13 @@ ${context}
2010
2277
  get tenants() {
2011
2278
  return this._tenantManager;
2012
2279
  }
2280
+ /** Kill switch — disable/enable the gateway at runtime */
2281
+ get enabled() {
2282
+ return this._enabled;
2283
+ }
2284
+ set enabled(value) {
2285
+ this._enabled = value;
2286
+ }
2013
2287
  };
2014
2288
  var BulwarkError = class _BulwarkError extends Error {
2015
2289
  code;
@@ -2032,6 +2306,7 @@ var BulwarkError = class _BulwarkError extends Error {
2032
2306
  INVALID_CONFIG: 400,
2033
2307
  PII_BLOCKED: 403,
2034
2308
  POLICY_BLOCKED: 403,
2309
+ PROMPT_INJECTION: 400,
2035
2310
  PROVIDER_NOT_CONFIGURED: 404,
2036
2311
  RATE_LIMITED: 429,
2037
2312
  BUDGET_EXCEEDED: 429,
@@ -2060,12 +2335,35 @@ async function parsePDF(buffer) {
2060
2335
  function parseHTML(html) {
2061
2336
  return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ").trim();
2062
2337
  }
2338
+ function splitCSVLine(line) {
2339
+ const fields = [];
2340
+ let current = "";
2341
+ let inQuotes = false;
2342
+ for (let i = 0; i < line.length; i++) {
2343
+ const ch = line[i];
2344
+ if (ch === '"') {
2345
+ if (inQuotes && line[i + 1] === '"') {
2346
+ current += '"';
2347
+ i++;
2348
+ } else {
2349
+ inQuotes = !inQuotes;
2350
+ }
2351
+ } else if (ch === "," && !inQuotes) {
2352
+ fields.push(current.trim());
2353
+ current = "";
2354
+ } else {
2355
+ current += ch;
2356
+ }
2357
+ }
2358
+ fields.push(current.trim());
2359
+ return fields;
2360
+ }
2063
2361
  function parseCSV(csv) {
2064
2362
  const lines = csv.split("\n").filter((l) => l.trim());
2065
2363
  if (lines.length === 0) return "";
2066
- const headers = lines[0].split(",").map((h) => h.trim().replace(/^"|"$/g, ""));
2364
+ const headers = splitCSVLine(lines[0]);
2067
2365
  const rows = lines.slice(1).map((line) => {
2068
- const values = line.split(",").map((v) => v.trim().replace(/^"|"$/g, ""));
2366
+ const values = splitCSVLine(line);
2069
2367
  return headers.map((h, i) => `${h}: ${values[i] || ""}`).join(", ");
2070
2368
  });
2071
2369
  return rows.join("\n");
@@ -2178,19 +2476,6 @@ var ResponseCache = class {
2178
2476
  }
2179
2477
  };
2180
2478
 
2181
- // src/streaming.ts
2182
- async function* createStreamAdapter(providerStream, metadata) {
2183
- if (metadata.sources && metadata.sources.length > 0) {
2184
- yield { type: "sources", data: { sources: metadata.sources } };
2185
- }
2186
- if (metadata.piiWarnings && metadata.piiWarnings.length > 0) {
2187
- yield { type: "pii_warning", data: { piiTypes: metadata.piiWarnings } };
2188
- }
2189
- for await (const chunk of providerStream) {
2190
- yield { type: "delta", data: { content: chunk } };
2191
- }
2192
- }
2193
-
2194
2479
  // src/compliance/gdpr.ts
2195
2480
  var GDPRManager = class {
2196
2481
  db;
@@ -2277,8 +2562,11 @@ var GDPRManager = class {
2277
2562
  }
2278
2563
  deleteAndCount(sql, param) {
2279
2564
  try {
2565
+ const countSql = sql.replace(/^DELETE FROM/, "SELECT COUNT(*) as c FROM");
2566
+ const row = this.db.queryOne(countSql, [param]);
2567
+ const count = row?.c || 0;
2280
2568
  this.db.run(sql, [param]);
2281
- return 0;
2569
+ return count;
2282
2570
  } catch {
2283
2571
  return 0;
2284
2572
  }
@@ -2286,6 +2574,13 @@ var GDPRManager = class {
2286
2574
  };
2287
2575
 
2288
2576
  // src/compliance/soc2.ts
2577
+ var BULWARK_VERSION = (() => {
2578
+ try {
2579
+ return require_package().version;
2580
+ } catch {
2581
+ return "0.1.x";
2582
+ }
2583
+ })();
2289
2584
  var SOC2Manager = class {
2290
2585
  db;
2291
2586
  config;
@@ -2427,7 +2722,7 @@ var SOC2Manager = class {
2427
2722
  // consumer should populate from their provider config
2428
2723
  uptime: Math.floor((Date.now() - this.startTime) / 1e3),
2429
2724
  activeRequests,
2430
- version: "0.1.0"
2725
+ version: BULWARK_VERSION
2431
2726
  };
2432
2727
  }
2433
2728
  };
@@ -2754,50 +3049,6 @@ var DataResidencyManager = class {
2754
3049
  }
2755
3050
  };
2756
3051
 
2757
- // src/providers/azure-openai.ts
2758
- var AzureOpenAIProvider = class {
2759
- apiKey;
2760
- endpoint;
2761
- apiVersion;
2762
- constructor(config) {
2763
- this.apiKey = config.apiKey;
2764
- this.endpoint = config.baseUrl || "";
2765
- this.apiVersion = config.apiVersion || "2024-10-21";
2766
- if (!this.endpoint) throw new Error("Azure OpenAI requires baseUrl (endpoint URL)");
2767
- }
2768
- async chat(request) {
2769
- const url = `${this.endpoint}/chat/completions?api-version=${this.apiVersion}`;
2770
- const response = await fetch(url, {
2771
- method: "POST",
2772
- headers: {
2773
- "Content-Type": "application/json",
2774
- "api-key": this.apiKey
2775
- },
2776
- body: JSON.stringify({
2777
- messages: request.messages.map((m) => ({ role: m.role, content: m.content })),
2778
- temperature: request.temperature,
2779
- max_tokens: request.maxTokens,
2780
- top_p: request.topP,
2781
- stop: request.stop
2782
- })
2783
- });
2784
- if (!response.ok) {
2785
- const err = await response.text();
2786
- throw new Error(`Azure OpenAI error (${response.status}): ${err}`);
2787
- }
2788
- const data = await response.json();
2789
- return {
2790
- content: data.choices[0]?.message?.content || "",
2791
- usage: {
2792
- inputTokens: data.usage?.prompt_tokens || 0,
2793
- outputTokens: data.usage?.completion_tokens || 0,
2794
- totalTokens: data.usage?.total_tokens || 0
2795
- },
2796
- finishReason: data.choices[0]?.finish_reason
2797
- };
2798
- }
2799
- };
2800
-
2801
3052
  // src/middleware/express.ts
2802
3053
  function bulwarkMiddleware(gateway) {
2803
3054
  return (req, _res, next) => {
@@ -2812,7 +3063,7 @@ function bulwarkRouter(gateway, options) {
2812
3063
  router.post("/chat", async (req, res) => {
2813
3064
  try {
2814
3065
  const authCtx = options?.auth?.(req) || {};
2815
- const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase, pii, skipPolicies, stream: _stream } = req.body;
3066
+ const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase } = req.body;
2816
3067
  const response = await gateway.chat({
2817
3068
  messages,
2818
3069
  model,
@@ -2821,8 +3072,6 @@ function bulwarkRouter(gateway, options) {
2821
3072
  topP,
2822
3073
  stop,
2823
3074
  knowledgeBase,
2824
- pii,
2825
- skipPolicies,
2826
3075
  userId: authCtx.userId,
2827
3076
  teamId: authCtx.teamId,
2828
3077
  tenantId: authCtx.tenantId
@@ -2848,7 +3097,7 @@ function bulwarkRouter(gateway, options) {
2848
3097
  }, 3e5);
2849
3098
  try {
2850
3099
  const authCtx = options?.auth?.(req) || {};
2851
- const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase, pii, skipPolicies } = req.body;
3100
+ const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase } = req.body;
2852
3101
  const stream = gateway.chatStream({
2853
3102
  messages,
2854
3103
  model,
@@ -2857,8 +3106,6 @@ function bulwarkRouter(gateway, options) {
2857
3106
  topP,
2858
3107
  stop,
2859
3108
  knowledgeBase,
2860
- pii,
2861
- skipPolicies,
2862
3109
  userId: authCtx.userId,
2863
3110
  teamId: authCtx.teamId,
2864
3111
  tenantId: authCtx.tenantId
@@ -2921,11 +3168,18 @@ function createNextHandler(gateway, options) {
2921
3168
  try {
2922
3169
  const body = await req.json();
2923
3170
  const authCtx = options?.auth?.(req) || {};
3171
+ const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase } = body;
2924
3172
  const response = await gateway.chat({
2925
- ...body,
2926
- userId: authCtx.userId || body.userId,
2927
- teamId: authCtx.teamId || body.teamId,
2928
- tenantId: authCtx.tenantId || body.tenantId
3173
+ messages,
3174
+ model,
3175
+ temperature,
3176
+ maxTokens,
3177
+ topP,
3178
+ stop,
3179
+ knowledgeBase,
3180
+ userId: authCtx.userId,
3181
+ teamId: authCtx.teamId,
3182
+ tenantId: authCtx.tenantId
2929
3183
  });
2930
3184
  return jsonResponse(response);
2931
3185
  } catch (err) {
@@ -2936,15 +3190,17 @@ function createNextHandler(gateway, options) {
2936
3190
  }
2937
3191
  };
2938
3192
  }
2939
- function createNextAuditHandler(gateway) {
3193
+ function createNextAuditHandler(gateway, options) {
2940
3194
  return async function GET(req) {
3195
+ const authCtx = options?.auth?.(req);
3196
+ if (!authCtx) return jsonResponse({ error: "Authentication required", code: "UNAUTHORIZED" }, 401);
2941
3197
  const params = req.nextUrl?.searchParams || new URL(req.url || "http://localhost").searchParams;
2942
3198
  const limit = Math.min(Math.max(1, Number(params.get("limit")) || 50), 1e3);
2943
3199
  const offset = Math.max(0, Number(params.get("offset")) || 0);
2944
3200
  const entries = await gateway.audit.query({
2945
- userId: params.get("userId") || void 0,
3201
+ userId: authCtx.userId || params.get("userId") || void 0,
2946
3202
  teamId: params.get("teamId") || void 0,
2947
- tenantId: params.get("tenantId") || void 0,
3203
+ tenantId: authCtx.tenantId || params.get("tenantId") || void 0,
2948
3204
  action: params.get("action") || void 0,
2949
3205
  from: params.get("from") || void 0,
2950
3206
  to: params.get("to") || void 0,
@@ -2963,11 +3219,18 @@ function bulwarkPlugin(fastify, options, done) {
2963
3219
  const request = req;
2964
3220
  const authCtx = auth?.(req) || {};
2965
3221
  const body = request.body;
3222
+ const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase } = body;
2966
3223
  const response = await gateway.chat({
2967
- ...body,
2968
- userId: authCtx.userId || body.userId,
2969
- teamId: authCtx.teamId || body.teamId,
2970
- tenantId: authCtx.tenantId || body.tenantId
3224
+ messages,
3225
+ model,
3226
+ temperature,
3227
+ maxTokens,
3228
+ topP,
3229
+ stop,
3230
+ knowledgeBase,
3231
+ userId: authCtx.userId,
3232
+ teamId: authCtx.teamId,
3233
+ tenantId: authCtx.tenantId
2971
3234
  });
2972
3235
  return reply.send(response);
2973
3236
  } catch (err) {
@@ -2978,11 +3241,13 @@ function bulwarkPlugin(fastify, options, done) {
2978
3241
  }
2979
3242
  });
2980
3243
  fastify.get(`${options.prefix || ""}/audit`, async (req, reply) => {
3244
+ const authCtx = auth?.(req);
3245
+ if (!authCtx) return reply.status(401).send({ error: "Authentication required", code: "UNAUTHORIZED" });
2981
3246
  const q = req.query;
2982
3247
  const entries = await gateway.audit.query({
2983
- userId: q.userId,
3248
+ userId: authCtx.userId || q.userId,
2984
3249
  teamId: q.teamId,
2985
- tenantId: q.tenantId,
3250
+ tenantId: authCtx.tenantId || q.tenantId,
2986
3251
  action: q.action,
2987
3252
  from: q.from,
2988
3253
  to: q.to,
@@ -3047,6 +3312,7 @@ function createAdminRouter(gateway, options) {
3047
3312
  if (!gateway.rag) return res.status(400).json({ error: "RAG not enabled" });
3048
3313
  const { content, name, type, tenantId } = req.body;
3049
3314
  if (!content || !name) return res.status(400).json({ error: "content and name required" });
3315
+ if (typeof content === "string" && content.length > 10 * 1024 * 1024) return res.status(400).json({ error: "Content too large (max 10MB)" });
3050
3316
  try {
3051
3317
  const result = await gateway.rag.ingest(content, { name, type: type || "text", tenantId });
3052
3318
  res.json({ success: true, ...result });
@@ -3072,8 +3338,12 @@ function createAdminRouter(gateway, options) {
3072
3338
  });
3073
3339
  router.get("/policies", (_req, res) => res.json({ policies: gateway.policies.getPolicies() }));
3074
3340
  router.post("/policies", (req, res) => {
3341
+ const policy = req.body;
3342
+ if (!policy.id || !policy.name || !policy.type || !policy.action) {
3343
+ return res.status(400).json({ error: "Policy requires: id, name, type, action" });
3344
+ }
3075
3345
  try {
3076
- gateway.policies.addPolicy(req.body);
3346
+ gateway.policies.addPolicy(policy);
3077
3347
  res.json({ success: true });
3078
3348
  } catch (err) {
3079
3349
  res.status(400).json({ error: err instanceof Error ? err.message : "Failed" });
@@ -3110,6 +3380,12 @@ function createAdminRouter(gateway, options) {
3110
3380
  router.post("/budgets", (req, res) => {
3111
3381
  const { scopeType, scopeId, monthlyTokenLimit, monthlyCostLimit, tenantId } = req.body;
3112
3382
  if (!scopeType || !scopeId) return res.status(400).json({ error: "scopeType and scopeId required" });
3383
+ if (monthlyTokenLimit !== void 0 && (typeof monthlyTokenLimit !== "number" || monthlyTokenLimit < 0)) {
3384
+ return res.status(400).json({ error: "monthlyTokenLimit must be a non-negative number" });
3385
+ }
3386
+ if (monthlyCostLimit !== void 0 && (typeof monthlyCostLimit !== "number" || monthlyCostLimit < 0)) {
3387
+ return res.status(400).json({ error: "monthlyCostLimit must be a non-negative number" });
3388
+ }
3113
3389
  const id = `budget_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3114
3390
  gateway.database.run(
3115
3391
  "INSERT INTO bulwark_budgets (id, tenant_id, scope_type, scope_id, monthly_token_limit, monthly_cost_limit) VALUES (?, ?, ?, ?, ?, ?)",
@@ -3191,7 +3467,6 @@ function createAdminRouter(gateway, options) {
3191
3467
  createDatabase,
3192
3468
  createNextAuditHandler,
3193
3469
  createNextHandler,
3194
- createStreamAdapter,
3195
3470
  getDashboard,
3196
3471
  hardenSystemPrompt,
3197
3472
  parseCSV,