@bulwark-ai/gateway 0.1.2 → 0.1.3

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,98 @@ 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.3",
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
+ "better-sqlite3": "^11.0.0",
271
+ uuid: "^11.0.0"
272
+ },
273
+ devDependencies: {
274
+ "@types/better-sqlite3": "^7.6.0",
275
+ "@types/express": "^5.0.6",
276
+ "@types/node": "^22.0.0",
277
+ "@types/react": "^19.2.14",
278
+ "@types/uuid": "^10.0.0",
279
+ react: "^19.2.4",
280
+ tsup: "^8.0.0",
281
+ typescript: "^5.7.0",
282
+ vitest: "^3.0.0"
283
+ },
284
+ peerDependencies: {
285
+ openai: "^4.0.0",
286
+ "@anthropic-ai/sdk": "^0.30.0",
287
+ express: "^4.0.0 || ^5.0.0",
288
+ pg: "^8.0.0",
289
+ ioredis: "^5.0.0"
290
+ },
291
+ peerDependenciesMeta: {
292
+ openai: { optional: true },
293
+ "@anthropic-ai/sdk": { optional: true },
294
+ express: { optional: true },
295
+ pg: { optional: true },
296
+ ioredis: { optional: true }
297
+ },
298
+ engines: {
299
+ node: ">=18"
300
+ }
301
+ };
302
+ }
303
+ });
304
+
205
305
  // src/index.ts
206
306
  var index_exports = {};
207
307
  __export(index_exports, {
@@ -243,7 +343,6 @@ __export(index_exports, {
243
343
  createDatabase: () => createDatabase,
244
344
  createNextAuditHandler: () => createNextAuditHandler,
245
345
  createNextHandler: () => createNextHandler,
246
- createStreamAdapter: () => createStreamAdapter,
247
346
  getDashboard: () => getDashboard,
248
347
  hardenSystemPrompt: () => hardenSystemPrompt,
249
348
  parseCSV: () => parseCSV,
@@ -273,16 +372,36 @@ var PII_PATTERNS = {
273
372
  // Generic — covers most EU national ID formats
274
373
  medical_id: /\b(?:NHS|EHIC|SVN|AMM)[-\s]?\d{6,12}\b/gi
275
374
  };
375
+ function luhnCheck(num) {
376
+ const digits = num.replace(/[\s-]/g, "");
377
+ if (!/^\d+$/.test(digits) || digits.length < 13) return false;
378
+ let sum = 0;
379
+ let alternate = false;
380
+ for (let i = digits.length - 1; i >= 0; i--) {
381
+ let n = parseInt(digits[i], 10);
382
+ if (alternate) {
383
+ n *= 2;
384
+ if (n > 9) n -= 9;
385
+ }
386
+ sum += n;
387
+ alternate = !alternate;
388
+ }
389
+ return sum % 10 === 0;
390
+ }
276
391
  var PIIDetector = class {
277
- config;
392
+ _config;
278
393
  activeTypes;
279
394
  constructor(config) {
280
- this.config = config;
395
+ this._config = config;
281
396
  this.activeTypes = config.types || ["email", "phone", "ssn", "credit_card", "iban"];
282
397
  }
398
+ /** Whether PII detection is enabled */
399
+ get config() {
400
+ return this._config;
401
+ }
283
402
  /** Scan text for PII. Returns matches and optionally redacted text. */
284
403
  scan(text) {
285
- if (!this.config.enabled) return { text, matches: [], blocked: false, redacted: false };
404
+ if (!this._config.enabled) return { text, matches: [], blocked: false, redacted: false };
286
405
  const matches = [];
287
406
  for (const type of this.activeTypes) {
288
407
  const pattern = PII_PATTERNS[type];
@@ -290,17 +409,19 @@ var PIIDetector = class {
290
409
  const regex = new RegExp(pattern.source, pattern.flags);
291
410
  let match;
292
411
  while ((match = regex.exec(text)) !== null) {
412
+ if (type === "credit_card" && !luhnCheck(match[0])) continue;
293
413
  matches.push({
294
414
  type,
295
- value: match[0],
415
+ value: "[REDACTED]",
416
+ // SECURITY: Never store raw PII values in match objects
296
417
  redacted: `[${type.toUpperCase()}]`,
297
418
  start: match.index,
298
419
  end: match.index + match[0].length
299
420
  });
300
421
  }
301
422
  }
302
- if (this.config.customPatterns) {
303
- for (const custom of this.config.customPatterns) {
423
+ if (this._config.customPatterns) {
424
+ for (const custom of this._config.customPatterns) {
304
425
  try {
305
426
  if (/\([^)]*[+*][^)]*\)[+*]/.test(custom.pattern)) continue;
306
427
  if (/(\.\*){3,}/.test(custom.pattern)) continue;
@@ -315,7 +436,7 @@ var PIIDetector = class {
315
436
  }
316
437
  matches.push({
317
438
  type: custom.name,
318
- value: match[0],
439
+ value: "[REDACTED]",
319
440
  redacted: `[${custom.name.toUpperCase()}]`,
320
441
  start: match.index,
321
442
  end: match.index + match[0].length
@@ -326,7 +447,7 @@ var PIIDetector = class {
326
447
  }
327
448
  }
328
449
  if (matches.length === 0) return { text, matches: [], blocked: false, redacted: false };
329
- const action = this.config.action || "warn";
450
+ const action = this._config.action || "warn";
330
451
  if (action === "block") {
331
452
  return { text, matches, blocked: true, redacted: false };
332
453
  }
@@ -394,11 +515,16 @@ var PolicyEngine = class {
394
515
  break;
395
516
  case "regex_block":
396
517
  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];
518
+ if (/\([^)]*[+*][^)]*\)[+*]/.test(policy.regex)) break;
519
+ if (/(\.\*){3,}/.test(policy.regex)) break;
520
+ try {
521
+ const regex = new RegExp(policy.regex, "gi");
522
+ const match = regex.exec(text);
523
+ if (match) {
524
+ violated = true;
525
+ matchedPattern = match[0];
526
+ }
527
+ } catch {
402
528
  }
403
529
  }
404
530
  break;
@@ -564,7 +690,7 @@ var CostCalculator = class {
564
690
  const outputCost = outputTokens / 1e6 * pricing.output;
565
691
  return {
566
692
  model,
567
- provider: model.startsWith("claude") ? "anthropic" : "openai",
693
+ provider: this.detectProvider(model),
568
694
  inputTokens,
569
695
  outputTokens,
570
696
  inputCost: Math.round(inputCost * 1e6) / 1e6,
@@ -573,6 +699,15 @@ var CostCalculator = class {
573
699
  totalCost: Math.round((inputCost + outputCost) * 1e6) / 1e6
574
700
  };
575
701
  }
702
+ /** Detect provider from model name */
703
+ detectProvider(model) {
704
+ const m = model.toLowerCase();
705
+ if (m.startsWith("claude")) return "anthropic";
706
+ if (m.startsWith("mistral") || m.startsWith("codestral") || m.startsWith("pixtral")) return "mistral";
707
+ if (m.startsWith("gemini") || m.startsWith("palm")) return "google";
708
+ if (m.startsWith("llama") || m.startsWith("phi") || m.startsWith("qwen") || m.startsWith("deepseek") || m.startsWith("codellama")) return "ollama";
709
+ return "openai";
710
+ }
576
711
  /** Update pricing for a model */
577
712
  setModelPrice(model, input, output) {
578
713
  this.pricing[model] = { input, output };
@@ -584,6 +719,8 @@ var BudgetManager = class {
584
719
  enabled;
585
720
  config;
586
721
  db;
722
+ /** Tracks which thresholds have already been crossed per scope to avoid duplicate alerts */
723
+ crossedThresholds = /* @__PURE__ */ new Map();
587
724
  constructor(db, config) {
588
725
  this.db = db;
589
726
  this.config = config;
@@ -607,8 +744,12 @@ var BudgetManager = class {
607
744
  return { ok: this.config.onExceeded !== "block", used, limit, costUsd: row?.total_cost || 0 };
608
745
  }
609
746
  if (limit > 0 && this.config.alertThresholds && this.config.onAlert) {
747
+ const scopeKey = `user:${scope.userId}`;
748
+ if (!this.crossedThresholds.has(scopeKey)) this.crossedThresholds.set(scopeKey, /* @__PURE__ */ new Set());
749
+ const crossed = this.crossedThresholds.get(scopeKey);
610
750
  for (const threshold of this.config.alertThresholds) {
611
- if (used / limit >= threshold) {
751
+ if (used / limit >= threshold && !crossed.has(threshold)) {
752
+ crossed.add(threshold);
612
753
  this.config.onAlert({ type: "user", id: scope.userId, threshold, used, limit, costUsd: row?.total_cost || 0 });
613
754
  }
614
755
  }
@@ -829,6 +970,7 @@ var SQLiteDatabase = class {
829
970
  };
830
971
  function createDatabase(connection) {
831
972
  if (connection.startsWith("postgres://") || connection.startsWith("postgresql://")) {
973
+ 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
974
  const { PostgresDatabase: PostgresDatabase2 } = (init_database_postgres(), __toCommonJS(database_postgres_exports));
833
975
  return new PostgresDatabase2(connection);
834
976
  }
@@ -838,9 +980,36 @@ function createDatabase(connection) {
838
980
 
839
981
  // src/providers/openai.ts
840
982
  var import_openai = __toESM(require("openai"));
983
+
984
+ // src/providers/base.ts
985
+ function validateBaseUrl(url) {
986
+ try {
987
+ const parsed = new URL(url);
988
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1";
989
+ if (parsed.protocol !== "https:" && !isLocalhost) {
990
+ throw new Error(`Insecure protocol: ${parsed.protocol}. Use HTTPS.`);
991
+ }
992
+ const blockedHosts = ["169.254.169.254", "metadata.google.internal", "100.100.100.200"];
993
+ if (blockedHosts.includes(parsed.hostname)) {
994
+ throw new Error(`Blocked host: ${parsed.hostname}`);
995
+ }
996
+ if (!isLocalhost) {
997
+ const parts = parsed.hostname.split(".").map(Number);
998
+ if (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168) {
999
+ throw new Error(`Private IP blocked: ${parsed.hostname}`);
1000
+ }
1001
+ }
1002
+ } catch (err) {
1003
+ if (err instanceof Error && err.message.includes("blocked") || err instanceof Error && err.message.includes("Insecure")) throw err;
1004
+ throw new Error(`Invalid baseUrl: ${url}`);
1005
+ }
1006
+ }
1007
+
1008
+ // src/providers/openai.ts
841
1009
  var OpenAIProvider = class {
842
1010
  client;
843
1011
  constructor(config) {
1012
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
844
1013
  this.client = new import_openai.default({ apiKey: config.apiKey, baseURL: config.baseUrl });
845
1014
  }
846
1015
  async chat(request) {
@@ -895,6 +1064,7 @@ var import_sdk = __toESM(require("@anthropic-ai/sdk"));
895
1064
  var AnthropicProvider = class {
896
1065
  client;
897
1066
  constructor(config) {
1067
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
898
1068
  this.client = new import_sdk.default({ apiKey: config.apiKey, baseURL: config.baseUrl });
899
1069
  }
900
1070
  async chat(request) {
@@ -953,6 +1123,7 @@ var MistralProvider = class {
953
1123
  apiKey;
954
1124
  baseUrl;
955
1125
  constructor(config) {
1126
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
956
1127
  this.apiKey = config.apiKey;
957
1128
  this.baseUrl = config.baseUrl || "https://api.mistral.ai/v1";
958
1129
  }
@@ -994,6 +1165,7 @@ var GoogleProvider = class {
994
1165
  apiKey;
995
1166
  baseUrl;
996
1167
  constructor(config) {
1168
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
997
1169
  this.apiKey = config.apiKey;
998
1170
  this.baseUrl = config.baseUrl || "https://generativelanguage.googleapis.com/v1beta";
999
1171
  }
@@ -1004,9 +1176,9 @@ var GoogleProvider = class {
1004
1176
  role: m.role === "assistant" ? "model" : "user",
1005
1177
  parts: [{ text: m.content }]
1006
1178
  }));
1007
- const response = await fetch(`${this.baseUrl}/models/${model}:generateContent?key=${this.apiKey}`, {
1179
+ const response = await fetch(`${this.baseUrl}/models/${model}:generateContent`, {
1008
1180
  method: "POST",
1009
- headers: { "Content-Type": "application/json" },
1181
+ headers: { "Content-Type": "application/json", "x-goog-api-key": this.apiKey },
1010
1182
  body: JSON.stringify({
1011
1183
  contents,
1012
1184
  systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction.content }] } : void 0,
@@ -1040,6 +1212,7 @@ var GoogleProvider = class {
1040
1212
  var OllamaProvider = class {
1041
1213
  baseUrl;
1042
1214
  constructor(config) {
1215
+ if (config.baseUrl) validateBaseUrl(config.baseUrl);
1043
1216
  this.baseUrl = config.baseUrl || "http://localhost:11434";
1044
1217
  }
1045
1218
  async chat(request) {
@@ -1075,6 +1248,50 @@ var OllamaProvider = class {
1075
1248
  }
1076
1249
  };
1077
1250
 
1251
+ // src/providers/azure-openai.ts
1252
+ var AzureOpenAIProvider = class {
1253
+ apiKey;
1254
+ endpoint;
1255
+ apiVersion;
1256
+ constructor(config) {
1257
+ this.apiKey = config.apiKey;
1258
+ this.endpoint = config.baseUrl || "";
1259
+ this.apiVersion = config.apiVersion || "2024-10-21";
1260
+ if (!this.endpoint) throw new Error("Azure OpenAI requires baseUrl (endpoint URL)");
1261
+ }
1262
+ async chat(request) {
1263
+ const url = `${this.endpoint}/chat/completions?api-version=${this.apiVersion}`;
1264
+ const response = await fetch(url, {
1265
+ method: "POST",
1266
+ headers: {
1267
+ "Content-Type": "application/json",
1268
+ "api-key": this.apiKey
1269
+ },
1270
+ body: JSON.stringify({
1271
+ messages: request.messages.map((m) => ({ role: m.role, content: m.content })),
1272
+ temperature: request.temperature,
1273
+ max_tokens: request.maxTokens,
1274
+ top_p: request.topP,
1275
+ stop: request.stop
1276
+ })
1277
+ });
1278
+ if (!response.ok) {
1279
+ const err = await response.text();
1280
+ throw new Error(`Azure OpenAI error (${response.status}): ${err}`);
1281
+ }
1282
+ const data = await response.json();
1283
+ return {
1284
+ content: data.choices[0]?.message?.content || "",
1285
+ usage: {
1286
+ inputTokens: data.usage?.prompt_tokens || 0,
1287
+ outputTokens: data.usage?.completion_tokens || 0,
1288
+ totalTokens: data.usage?.total_tokens || 0
1289
+ },
1290
+ finishReason: data.choices[0]?.finish_reason
1291
+ };
1292
+ }
1293
+ };
1294
+
1078
1295
  // src/rag/knowledge-base.ts
1079
1296
  var import_uuid2 = require("uuid");
1080
1297
 
@@ -1318,6 +1535,8 @@ var KnowledgeBase = class {
1318
1535
  if (tenantId) {
1319
1536
  sql += " WHERE tenant_id = ?";
1320
1537
  params.push(tenantId);
1538
+ } else {
1539
+ sql += " WHERE (tenant_id IS NULL OR tenant_id = '')";
1321
1540
  }
1322
1541
  sql += " ORDER BY created_at DESC";
1323
1542
  return this.db.queryAll(sql, params);
@@ -1340,8 +1559,11 @@ var MemoryCacheStore = class {
1340
1559
  store = /* @__PURE__ */ new Map();
1341
1560
  counters = /* @__PURE__ */ new Map();
1342
1561
  cleanupTimer;
1343
- constructor() {
1562
+ maxEntries;
1563
+ constructor(options) {
1564
+ this.maxEntries = options?.maxEntries || 1e4;
1344
1565
  this.cleanupTimer = setInterval(() => this.cleanup(), 6e4);
1566
+ this.cleanupTimer.unref();
1345
1567
  }
1346
1568
  /** Stop background cleanup — call on shutdown */
1347
1569
  close() {
@@ -1368,6 +1590,10 @@ var MemoryCacheStore = class {
1368
1590
  return entry.value;
1369
1591
  }
1370
1592
  async set(key, value, ttlSeconds) {
1593
+ if (this.store.size >= this.maxEntries) {
1594
+ const oldestKey = this.store.keys().next().value;
1595
+ if (oldestKey) this.store.delete(oldestKey);
1596
+ }
1371
1597
  this.store.set(key, {
1372
1598
  value,
1373
1599
  expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1e3 : void 0
@@ -1417,9 +1643,7 @@ var RateLimiter = class {
1417
1643
  if (!scopeId) return { allowed: true, remaining: Infinity, resetAt: 0 };
1418
1644
  const windowKey = `ratelimit:${this.config.scope}:${scopeId}:${this.currentWindow()}`;
1419
1645
  const count = await this.store.incr(windowKey);
1420
- if (count === 1) {
1421
- await this.store.expire(windowKey, this.config.windowSeconds);
1422
- }
1646
+ await this.store.expire(windowKey, this.config.windowSeconds);
1423
1647
  const remaining = Math.max(0, this.config.maxRequests - count);
1424
1648
  const resetAt = Math.ceil(Date.now() / 1e3 / this.config.windowSeconds) * this.config.windowSeconds * 1e3;
1425
1649
  return {
@@ -1482,15 +1706,22 @@ var TenantManager = class {
1482
1706
  if (updates.name) this.db.run("UPDATE bulwark_tenants SET name = ? WHERE id = ?", [updates.name, id]);
1483
1707
  if (updates.settings) this.db.run("UPDATE bulwark_tenants SET settings = ? WHERE id = ?", [JSON.stringify(updates.settings), id]);
1484
1708
  }
1485
- /** Delete a tenant and ALL its data */
1709
+ /** Delete a tenant and ALL its data (transactional) */
1486
1710
  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]);
1711
+ this.db.run("BEGIN TRANSACTION", []);
1712
+ try {
1713
+ this.db.run("DELETE FROM bulwark_chunks WHERE tenant_id = ?", [id]);
1714
+ this.db.run("DELETE FROM bulwark_knowledge_sources WHERE tenant_id = ?", [id]);
1715
+ this.db.run("DELETE FROM bulwark_usage WHERE tenant_id = ?", [id]);
1716
+ this.db.run("DELETE FROM bulwark_audit WHERE tenant_id = ?", [id]);
1717
+ this.db.run("DELETE FROM bulwark_policies WHERE tenant_id = ?", [id]);
1718
+ this.db.run("DELETE FROM bulwark_budgets WHERE tenant_id = ?", [id]);
1719
+ this.db.run("DELETE FROM bulwark_tenants WHERE id = ?", [id]);
1720
+ this.db.run("COMMIT", []);
1721
+ } catch (err) {
1722
+ this.db.run("ROLLBACK", []);
1723
+ throw err;
1724
+ }
1494
1725
  }
1495
1726
  /** Get usage stats for a tenant */
1496
1727
  getUsage(id) {
@@ -1535,10 +1766,23 @@ var AIGateway = class {
1535
1766
  timeoutMs;
1536
1767
  retryConfig;
1537
1768
  fallbacks;
1769
+ failMode;
1770
+ _enabled;
1538
1771
  initialized = false;
1539
1772
  shutdownRequested = false;
1540
1773
  activeRequests = 0;
1541
1774
  constructor(config) {
1775
+ if (config.mode) {
1776
+ const presets = {
1777
+ strict: { pii: { enabled: true, action: "block" }, budgets: { enabled: true, defaultUserLimit: 1e5, onExceeded: "block" }, promptGuard: { enabled: true, action: "block", sensitivity: "high" }, audit: true },
1778
+ balanced: { pii: { enabled: true, action: "redact" }, budgets: { enabled: true, defaultUserLimit: 5e5 }, audit: true },
1779
+ dev: { pii: { enabled: false }, budgets: { enabled: false }, audit: true }
1780
+ };
1781
+ const preset = presets[config.mode];
1782
+ config = { ...preset, ...config, pii: config.pii ?? preset.pii, budgets: config.budgets ?? preset.budgets };
1783
+ }
1784
+ this.failMode = config.failMode || "fail-closed";
1785
+ this._enabled = config.enabled !== false;
1542
1786
  if (!config.providers || Object.keys(config.providers).length === 0) {
1543
1787
  throw new BulwarkError("INVALID_CONFIG", "At least one provider must be configured");
1544
1788
  }
@@ -1576,6 +1820,7 @@ var AIGateway = class {
1576
1820
  if (config.providers.mistral) this.providers.set("mistral", new MistralProvider(config.providers.mistral));
1577
1821
  if (config.providers.google) this.providers.set("google", new GoogleProvider(config.providers.google));
1578
1822
  if (config.providers.ollama) this.providers.set("ollama", new OllamaProvider(config.providers.ollama));
1823
+ if (config.providers.azure) this.providers.set("azure", new AzureOpenAIProvider(config.providers.azure));
1579
1824
  if (config.rag?.enabled && config.providers.openai?.apiKey) {
1580
1825
  this.kb = new KnowledgeBase(this.db, config.rag, config.providers.openai.apiKey);
1581
1826
  }
@@ -1595,12 +1840,16 @@ var AIGateway = class {
1595
1840
  * Pipeline: Validate → PII scan → Policy check → Rate limit → Budget check → [RAG augment] → LLM call (with timeout) → Token count → Cost calc → Audit log
1596
1841
  */
1597
1842
  async chat(request) {
1843
+ if (this._enabled === false) {
1844
+ throw new BulwarkError("GATEWAY_DISABLED", "AI gateway is disabled. Set enabled: true to resume.");
1845
+ }
1598
1846
  if (this.shutdownRequested) {
1599
1847
  throw new BulwarkError("SHUTTING_DOWN", "Gateway is shutting down, not accepting new requests");
1600
1848
  }
1601
1849
  await this.init();
1602
1850
  this.activeRequests++;
1603
1851
  const start = Date.now();
1852
+ const trace = request.debug ? [] : void 0;
1604
1853
  try {
1605
1854
  this.validateRequest(request);
1606
1855
  const model = request.model || "gpt-4o";
@@ -1690,7 +1939,7 @@ var AIGateway = class {
1690
1939
  const hasSystemMsg = messages.some((m) => m.role === "system");
1691
1940
  if (hasSystemMsg) {
1692
1941
  messages = messages.map(
1693
- (m) => m.role === "system" ? { ...m, content: hardenSystemPrompt(m.content, { preventExtraction: true, enforceGDPR: !!this.piiDetector }) } : m
1942
+ (m) => m.role === "system" ? { ...m, content: hardenSystemPrompt(m.content, { preventExtraction: true, enforceGDPR: this.piiDetector.config?.enabled ?? false }) } : m
1694
1943
  );
1695
1944
  }
1696
1945
  if (request.knowledgeBase && this.kb) {
@@ -1711,7 +1960,7 @@ ${context}
1711
1960
  if (hasSystemMsg) {
1712
1961
  messages = messages.map((m) => m.role === "system" ? { ...m, content: m.content + ragInstruction } : m);
1713
1962
  } else {
1714
- const basePrompt = hardenSystemPrompt("You are a helpful assistant.", { preventExtraction: true, enforceGDPR: !!this.piiDetector });
1963
+ const basePrompt = hardenSystemPrompt("You are a helpful assistant.", { preventExtraction: true, enforceGDPR: this.piiDetector.config?.enabled ?? false });
1715
1964
  messages = [{ role: "system", content: basePrompt + ragInstruction }, ...messages];
1716
1965
  }
1717
1966
  }
@@ -1753,7 +2002,8 @@ ${context}
1753
2002
  outputTokens: llmResponse.usage.outputTokens,
1754
2003
  costUsd: cost.totalCost,
1755
2004
  durationMs,
1756
- piiDetections: totalPii || void 0
2005
+ piiDetections: totalPii || void 0,
2006
+ metadata: request.metadata
1757
2007
  });
1758
2008
  return {
1759
2009
  content: outputContent,
@@ -1767,7 +2017,8 @@ ${context}
1767
2017
  ] : void 0,
1768
2018
  sources,
1769
2019
  auditId,
1770
- durationMs
2020
+ durationMs,
2021
+ trace
1771
2022
  };
1772
2023
  } finally {
1773
2024
  this.activeRequests--;
@@ -1795,26 +2046,31 @@ ${context}
1795
2046
  try {
1796
2047
  this.validateRequest(request);
1797
2048
  const model = request.model || "gpt-4o";
1798
- const { provider, providerInstance } = this.resolveProvider(model);
1799
2049
  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 }];
2050
+ for (let i = 0; i < messages.length; i++) {
2051
+ const msg = messages[i];
2052
+ if (msg.role === "user") {
2053
+ const guardResult = this.promptGuard.scan(msg.content);
2054
+ if (!guardResult.safe) {
2055
+ if (guardResult.sanitizedText) {
2056
+ messages = [...messages.slice(0, i), { ...msg, content: guardResult.sanitizedText }, ...messages.slice(i + 1)];
2057
+ } else {
2058
+ throw new BulwarkError("PROMPT_INJECTION", `Prompt injection detected in message ${i}: ${guardResult.injections.map((j) => j.pattern).join(", ")}`, { injections: guardResult.injections });
2059
+ }
2060
+ }
1808
2061
  }
1809
2062
  }
1810
2063
  const piiTypes = [];
1811
2064
  if (request.pii !== false) {
1812
- const userMsg = messages[messages.length - 1];
1813
- if (userMsg?.role === "user") {
1814
- const result = this.piiDetector.scan(userMsg.content);
2065
+ for (let i = 0; i < messages.length; i++) {
2066
+ const msg = messages[i];
2067
+ if (msg.role !== "user") continue;
2068
+ const result = this.piiDetector.scan(msg.content);
1815
2069
  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 }];
2070
+ if (result.blocked) throw new BulwarkError("PII_BLOCKED", `PII detected and blocked in message ${i}`);
2071
+ if (result.redacted) {
2072
+ messages = [...messages.slice(0, i), { ...msg, content: result.text }, ...messages.slice(i + 1)];
2073
+ }
1818
2074
  }
1819
2075
  }
1820
2076
  if (!request.skipPolicies) {
@@ -1830,6 +2086,12 @@ ${context}
1830
2086
  const allowed = await this.budgetManager.checkBudget({ userId: request.userId, teamId: request.teamId, tenantId: request.tenantId });
1831
2087
  if (!allowed.ok) throw new BulwarkError("BUDGET_EXCEEDED", "Budget exceeded");
1832
2088
  }
2089
+ const hasSystemMsg = messages.some((m) => m.role === "system");
2090
+ if (hasSystemMsg) {
2091
+ messages = messages.map(
2092
+ (m) => m.role === "system" ? { ...m, content: hardenSystemPrompt(m.content, { preventExtraction: true, enforceGDPR: this.piiDetector.config?.enabled ?? false }) } : m
2093
+ );
2094
+ }
1833
2095
  let sources = void 0;
1834
2096
  if (request.knowledgeBase && this.kb) {
1835
2097
  const results = await this.kb.search(messages[messages.length - 1]?.content || "", { tenantId: request.tenantId, topK: 6 });
@@ -1838,14 +2100,20 @@ ${context}
1838
2100
  const context = results.map((r, i) => `[${i + 1}] ${r.chunk.sourceName}: ${r.chunk.content}`).join("\n\n");
1839
2101
  const ragInstruction = `
1840
2102
 
2103
+ Use ONLY the following knowledge base context to answer. Cite sources using [1], [2] etc.
2104
+
1841
2105
  --- KNOWLEDGE BASE CONTEXT ---
1842
2106
  ${context}
1843
2107
  --- 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];
2108
+ if (hasSystemMsg) {
2109
+ messages = messages.map((m) => m.role === "system" ? { ...m, content: m.content + ragInstruction } : m);
2110
+ } else {
2111
+ const basePrompt = hardenSystemPrompt("You are a helpful assistant.", { preventExtraction: true, enforceGDPR: this.piiDetector.config?.enabled ?? false });
2112
+ messages = [{ role: "system", content: basePrompt + ragInstruction }, ...messages];
2113
+ }
1847
2114
  }
1848
2115
  }
2116
+ const { provider, providerInstance } = this.resolveProvider(model);
1849
2117
  if (sources) yield { type: "sources", sources };
1850
2118
  if (piiTypes.length > 0) yield { type: "pii_warning", piiTypes };
1851
2119
  if (!providerInstance.chatStream) {
@@ -1971,6 +2239,7 @@ ${context}
1971
2239
  if (m.startsWith("mistral") || m.startsWith("codestral") || m.startsWith("pixtral")) return this.getProvider("mistral");
1972
2240
  if (m.startsWith("gemini") || m.startsWith("palm")) return this.getProvider("google");
1973
2241
  if (m.startsWith("llama") || m.startsWith("phi") || m.startsWith("qwen") || m.startsWith("deepseek") || m.startsWith("codellama")) return this.getProvider("ollama");
2242
+ if (this.providers.has("azure")) return this.getProvider("azure");
1974
2243
  return this.getProvider("openai");
1975
2244
  }
1976
2245
  getProvider(name) {
@@ -2010,6 +2279,13 @@ ${context}
2010
2279
  get tenants() {
2011
2280
  return this._tenantManager;
2012
2281
  }
2282
+ /** Kill switch — disable/enable the gateway at runtime */
2283
+ get enabled() {
2284
+ return this._enabled;
2285
+ }
2286
+ set enabled(value) {
2287
+ this._enabled = value;
2288
+ }
2013
2289
  };
2014
2290
  var BulwarkError = class _BulwarkError extends Error {
2015
2291
  code;
@@ -2032,6 +2308,7 @@ var BulwarkError = class _BulwarkError extends Error {
2032
2308
  INVALID_CONFIG: 400,
2033
2309
  PII_BLOCKED: 403,
2034
2310
  POLICY_BLOCKED: 403,
2311
+ PROMPT_INJECTION: 400,
2035
2312
  PROVIDER_NOT_CONFIGURED: 404,
2036
2313
  RATE_LIMITED: 429,
2037
2314
  BUDGET_EXCEEDED: 429,
@@ -2060,12 +2337,35 @@ async function parsePDF(buffer) {
2060
2337
  function parseHTML(html) {
2061
2338
  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
2339
  }
2340
+ function splitCSVLine(line) {
2341
+ const fields = [];
2342
+ let current = "";
2343
+ let inQuotes = false;
2344
+ for (let i = 0; i < line.length; i++) {
2345
+ const ch = line[i];
2346
+ if (ch === '"') {
2347
+ if (inQuotes && line[i + 1] === '"') {
2348
+ current += '"';
2349
+ i++;
2350
+ } else {
2351
+ inQuotes = !inQuotes;
2352
+ }
2353
+ } else if (ch === "," && !inQuotes) {
2354
+ fields.push(current.trim());
2355
+ current = "";
2356
+ } else {
2357
+ current += ch;
2358
+ }
2359
+ }
2360
+ fields.push(current.trim());
2361
+ return fields;
2362
+ }
2063
2363
  function parseCSV(csv) {
2064
2364
  const lines = csv.split("\n").filter((l) => l.trim());
2065
2365
  if (lines.length === 0) return "";
2066
- const headers = lines[0].split(",").map((h) => h.trim().replace(/^"|"$/g, ""));
2366
+ const headers = splitCSVLine(lines[0]);
2067
2367
  const rows = lines.slice(1).map((line) => {
2068
- const values = line.split(",").map((v) => v.trim().replace(/^"|"$/g, ""));
2368
+ const values = splitCSVLine(line);
2069
2369
  return headers.map((h, i) => `${h}: ${values[i] || ""}`).join(", ");
2070
2370
  });
2071
2371
  return rows.join("\n");
@@ -2178,19 +2478,6 @@ var ResponseCache = class {
2178
2478
  }
2179
2479
  };
2180
2480
 
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
2481
  // src/compliance/gdpr.ts
2195
2482
  var GDPRManager = class {
2196
2483
  db;
@@ -2277,8 +2564,11 @@ var GDPRManager = class {
2277
2564
  }
2278
2565
  deleteAndCount(sql, param) {
2279
2566
  try {
2567
+ const countSql = sql.replace(/^DELETE FROM/, "SELECT COUNT(*) as c FROM");
2568
+ const row = this.db.queryOne(countSql, [param]);
2569
+ const count = row?.c || 0;
2280
2570
  this.db.run(sql, [param]);
2281
- return 0;
2571
+ return count;
2282
2572
  } catch {
2283
2573
  return 0;
2284
2574
  }
@@ -2286,6 +2576,13 @@ var GDPRManager = class {
2286
2576
  };
2287
2577
 
2288
2578
  // src/compliance/soc2.ts
2579
+ var BULWARK_VERSION = (() => {
2580
+ try {
2581
+ return require_package().version;
2582
+ } catch {
2583
+ return "0.1.x";
2584
+ }
2585
+ })();
2289
2586
  var SOC2Manager = class {
2290
2587
  db;
2291
2588
  config;
@@ -2427,7 +2724,7 @@ var SOC2Manager = class {
2427
2724
  // consumer should populate from their provider config
2428
2725
  uptime: Math.floor((Date.now() - this.startTime) / 1e3),
2429
2726
  activeRequests,
2430
- version: "0.1.0"
2727
+ version: BULWARK_VERSION
2431
2728
  };
2432
2729
  }
2433
2730
  };
@@ -2754,50 +3051,6 @@ var DataResidencyManager = class {
2754
3051
  }
2755
3052
  };
2756
3053
 
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
3054
  // src/middleware/express.ts
2802
3055
  function bulwarkMiddleware(gateway) {
2803
3056
  return (req, _res, next) => {
@@ -2812,7 +3065,7 @@ function bulwarkRouter(gateway, options) {
2812
3065
  router.post("/chat", async (req, res) => {
2813
3066
  try {
2814
3067
  const authCtx = options?.auth?.(req) || {};
2815
- const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase, pii, skipPolicies, stream: _stream } = req.body;
3068
+ const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase } = req.body;
2816
3069
  const response = await gateway.chat({
2817
3070
  messages,
2818
3071
  model,
@@ -2821,8 +3074,6 @@ function bulwarkRouter(gateway, options) {
2821
3074
  topP,
2822
3075
  stop,
2823
3076
  knowledgeBase,
2824
- pii,
2825
- skipPolicies,
2826
3077
  userId: authCtx.userId,
2827
3078
  teamId: authCtx.teamId,
2828
3079
  tenantId: authCtx.tenantId
@@ -2848,7 +3099,7 @@ function bulwarkRouter(gateway, options) {
2848
3099
  }, 3e5);
2849
3100
  try {
2850
3101
  const authCtx = options?.auth?.(req) || {};
2851
- const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase, pii, skipPolicies } = req.body;
3102
+ const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase } = req.body;
2852
3103
  const stream = gateway.chatStream({
2853
3104
  messages,
2854
3105
  model,
@@ -2857,8 +3108,6 @@ function bulwarkRouter(gateway, options) {
2857
3108
  topP,
2858
3109
  stop,
2859
3110
  knowledgeBase,
2860
- pii,
2861
- skipPolicies,
2862
3111
  userId: authCtx.userId,
2863
3112
  teamId: authCtx.teamId,
2864
3113
  tenantId: authCtx.tenantId
@@ -2921,11 +3170,18 @@ function createNextHandler(gateway, options) {
2921
3170
  try {
2922
3171
  const body = await req.json();
2923
3172
  const authCtx = options?.auth?.(req) || {};
3173
+ const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase } = body;
2924
3174
  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
3175
+ messages,
3176
+ model,
3177
+ temperature,
3178
+ maxTokens,
3179
+ topP,
3180
+ stop,
3181
+ knowledgeBase,
3182
+ userId: authCtx.userId,
3183
+ teamId: authCtx.teamId,
3184
+ tenantId: authCtx.tenantId
2929
3185
  });
2930
3186
  return jsonResponse(response);
2931
3187
  } catch (err) {
@@ -2936,15 +3192,17 @@ function createNextHandler(gateway, options) {
2936
3192
  }
2937
3193
  };
2938
3194
  }
2939
- function createNextAuditHandler(gateway) {
3195
+ function createNextAuditHandler(gateway, options) {
2940
3196
  return async function GET(req) {
3197
+ const authCtx = options?.auth?.(req);
3198
+ if (!authCtx) return jsonResponse({ error: "Authentication required", code: "UNAUTHORIZED" }, 401);
2941
3199
  const params = req.nextUrl?.searchParams || new URL(req.url || "http://localhost").searchParams;
2942
3200
  const limit = Math.min(Math.max(1, Number(params.get("limit")) || 50), 1e3);
2943
3201
  const offset = Math.max(0, Number(params.get("offset")) || 0);
2944
3202
  const entries = await gateway.audit.query({
2945
- userId: params.get("userId") || void 0,
3203
+ userId: authCtx.userId || params.get("userId") || void 0,
2946
3204
  teamId: params.get("teamId") || void 0,
2947
- tenantId: params.get("tenantId") || void 0,
3205
+ tenantId: authCtx.tenantId || params.get("tenantId") || void 0,
2948
3206
  action: params.get("action") || void 0,
2949
3207
  from: params.get("from") || void 0,
2950
3208
  to: params.get("to") || void 0,
@@ -2963,11 +3221,18 @@ function bulwarkPlugin(fastify, options, done) {
2963
3221
  const request = req;
2964
3222
  const authCtx = auth?.(req) || {};
2965
3223
  const body = request.body;
3224
+ const { messages, model, temperature, maxTokens, topP, stop, knowledgeBase } = body;
2966
3225
  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
3226
+ messages,
3227
+ model,
3228
+ temperature,
3229
+ maxTokens,
3230
+ topP,
3231
+ stop,
3232
+ knowledgeBase,
3233
+ userId: authCtx.userId,
3234
+ teamId: authCtx.teamId,
3235
+ tenantId: authCtx.tenantId
2971
3236
  });
2972
3237
  return reply.send(response);
2973
3238
  } catch (err) {
@@ -2978,11 +3243,13 @@ function bulwarkPlugin(fastify, options, done) {
2978
3243
  }
2979
3244
  });
2980
3245
  fastify.get(`${options.prefix || ""}/audit`, async (req, reply) => {
3246
+ const authCtx = auth?.(req);
3247
+ if (!authCtx) return reply.status(401).send({ error: "Authentication required", code: "UNAUTHORIZED" });
2981
3248
  const q = req.query;
2982
3249
  const entries = await gateway.audit.query({
2983
- userId: q.userId,
3250
+ userId: authCtx.userId || q.userId,
2984
3251
  teamId: q.teamId,
2985
- tenantId: q.tenantId,
3252
+ tenantId: authCtx.tenantId || q.tenantId,
2986
3253
  action: q.action,
2987
3254
  from: q.from,
2988
3255
  to: q.to,
@@ -3047,6 +3314,7 @@ function createAdminRouter(gateway, options) {
3047
3314
  if (!gateway.rag) return res.status(400).json({ error: "RAG not enabled" });
3048
3315
  const { content, name, type, tenantId } = req.body;
3049
3316
  if (!content || !name) return res.status(400).json({ error: "content and name required" });
3317
+ if (typeof content === "string" && content.length > 10 * 1024 * 1024) return res.status(400).json({ error: "Content too large (max 10MB)" });
3050
3318
  try {
3051
3319
  const result = await gateway.rag.ingest(content, { name, type: type || "text", tenantId });
3052
3320
  res.json({ success: true, ...result });
@@ -3072,8 +3340,12 @@ function createAdminRouter(gateway, options) {
3072
3340
  });
3073
3341
  router.get("/policies", (_req, res) => res.json({ policies: gateway.policies.getPolicies() }));
3074
3342
  router.post("/policies", (req, res) => {
3343
+ const policy = req.body;
3344
+ if (!policy.id || !policy.name || !policy.type || !policy.action) {
3345
+ return res.status(400).json({ error: "Policy requires: id, name, type, action" });
3346
+ }
3075
3347
  try {
3076
- gateway.policies.addPolicy(req.body);
3348
+ gateway.policies.addPolicy(policy);
3077
3349
  res.json({ success: true });
3078
3350
  } catch (err) {
3079
3351
  res.status(400).json({ error: err instanceof Error ? err.message : "Failed" });
@@ -3110,6 +3382,12 @@ function createAdminRouter(gateway, options) {
3110
3382
  router.post("/budgets", (req, res) => {
3111
3383
  const { scopeType, scopeId, monthlyTokenLimit, monthlyCostLimit, tenantId } = req.body;
3112
3384
  if (!scopeType || !scopeId) return res.status(400).json({ error: "scopeType and scopeId required" });
3385
+ if (monthlyTokenLimit !== void 0 && (typeof monthlyTokenLimit !== "number" || monthlyTokenLimit < 0)) {
3386
+ return res.status(400).json({ error: "monthlyTokenLimit must be a non-negative number" });
3387
+ }
3388
+ if (monthlyCostLimit !== void 0 && (typeof monthlyCostLimit !== "number" || monthlyCostLimit < 0)) {
3389
+ return res.status(400).json({ error: "monthlyCostLimit must be a non-negative number" });
3390
+ }
3113
3391
  const id = `budget_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3114
3392
  gateway.database.run(
3115
3393
  "INSERT INTO bulwark_budgets (id, tenant_id, scope_type, scope_id, monthly_token_limit, monthly_cost_limit) VALUES (?, ?, ?, ?, ?, ?)",
@@ -3191,7 +3469,6 @@ function createAdminRouter(gateway, options) {
3191
3469
  createDatabase,
3192
3470
  createNextAuditHandler,
3193
3471
  createNextHandler,
3194
- createStreamAdapter,
3195
3472
  getDashboard,
3196
3473
  hardenSystemPrompt,
3197
3474
  parseCSV,