@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/README.md +153 -8
- package/dist/index.d.ts +76 -20
- package/dist/index.js +410 -133
- package/dist/index.mjs +410 -132
- package/package.json +19 -29
- package/tsup.config.ts +0 -37
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
|
-
|
|
392
|
+
_config;
|
|
278
393
|
activeTypes;
|
|
279
394
|
constructor(config) {
|
|
280
|
-
this.
|
|
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.
|
|
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:
|
|
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.
|
|
303
|
-
for (const custom of this.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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",
|
|
1817
|
-
if (result.redacted)
|
|
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
|
-
|
|
1845
|
-
|
|
1846
|
-
else
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/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]
|
|
2366
|
+
const headers = splitCSVLine(lines[0]);
|
|
2067
2367
|
const rows = lines.slice(1).map((line) => {
|
|
2068
|
-
const values = line
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
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
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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(
|
|
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,
|