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