@opentrust/gateway 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/config.d.ts +7 -0
  2. package/dist/config.d.ts.map +1 -0
  3. package/dist/config.js +81 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/handlers/anthropic.d.ts +7 -0
  6. package/dist/handlers/anthropic.d.ts.map +1 -0
  7. package/dist/handlers/anthropic.js +49 -0
  8. package/dist/handlers/anthropic.js.map +1 -0
  9. package/dist/handlers/gemini.d.ts +7 -0
  10. package/dist/handlers/gemini.d.ts.map +1 -0
  11. package/dist/handlers/gemini.js +41 -0
  12. package/dist/handlers/gemini.js.map +1 -0
  13. package/dist/handlers/models.d.ts +7 -0
  14. package/dist/handlers/models.d.ts.map +1 -0
  15. package/dist/handlers/models.js +29 -0
  16. package/dist/handlers/models.js.map +1 -0
  17. package/dist/handlers/openai.d.ts +11 -0
  18. package/dist/handlers/openai.d.ts.map +1 -0
  19. package/dist/handlers/openai.js +45 -0
  20. package/dist/handlers/openai.js.map +1 -0
  21. package/dist/index.d.ts +12 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +126 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/restorer.d.ts +8 -0
  26. package/dist/restorer.d.ts.map +1 -0
  27. package/dist/restorer.js +54 -0
  28. package/dist/restorer.js.map +1 -0
  29. package/dist/sanitizer/core.d.ts +16 -0
  30. package/dist/sanitizer/core.d.ts.map +1 -0
  31. package/dist/sanitizer/core.js +77 -0
  32. package/dist/sanitizer/core.js.map +1 -0
  33. package/dist/sanitizer/index.d.ts +3 -0
  34. package/dist/sanitizer/index.d.ts.map +1 -0
  35. package/dist/sanitizer/index.js +3 -0
  36. package/dist/sanitizer/index.js.map +1 -0
  37. package/dist/sanitizer/reversible.d.ts +8 -0
  38. package/dist/sanitizer/reversible.d.ts.map +1 -0
  39. package/dist/sanitizer/reversible.js +62 -0
  40. package/dist/sanitizer/reversible.js.map +1 -0
  41. package/dist/types.d.ts +41 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +5 -0
  44. package/dist/types.js.map +1 -0
  45. package/dist/utils/http.d.ts +9 -0
  46. package/dist/utils/http.d.ts.map +1 -0
  47. package/dist/utils/http.js +35 -0
  48. package/dist/utils/http.js.map +1 -0
  49. package/dist/utils/stream.d.ts +14 -0
  50. package/dist/utils/stream.d.ts.map +1 -0
  51. package/dist/utils/stream.js +55 -0
  52. package/dist/utils/stream.js.map +1 -0
  53. package/package.json +53 -0
  54. package/src/config.ts +91 -0
  55. package/src/handlers/anthropic.ts +53 -0
  56. package/src/handlers/gemini.ts +46 -0
  57. package/src/handlers/models.ts +31 -0
  58. package/src/handlers/openai.ts +55 -0
  59. package/src/index.ts +115 -0
  60. package/src/restorer.ts +49 -0
  61. package/src/sanitizer/core.ts +102 -0
  62. package/src/sanitizer/index.ts +2 -0
  63. package/src/sanitizer/reversible.ts +78 -0
  64. package/src/types.ts +33 -0
  65. package/src/utils/http.ts +41 -0
  66. package/src/utils/stream.ts +64 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Sanitizer core — entity definitions, pattern matching, entropy detection.
3
+ * Shared between gateway (reversible) and guards (one-way).
4
+ */
5
+ const ENTITIES = [
6
+ { category: "URL", categoryKey: "url", pattern: /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g },
7
+ { category: "EMAIL", categoryKey: "email", pattern: /[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g },
8
+ { category: "CREDIT_CARD", categoryKey: "credit_card", pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g },
9
+ { category: "BANK_CARD", categoryKey: "bank_card", pattern: /\b\d{16,19}\b/g },
10
+ { category: "SSN", categoryKey: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
11
+ { category: "IBAN", categoryKey: "iban", pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}[A-Z0-9]{0,16}\b/g },
12
+ { category: "IP_ADDRESS", categoryKey: "ip", pattern: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g },
13
+ { category: "PHONE", categoryKey: "phone", pattern: /[+]?[0-9]{1,3}?[-\s.]?[(]?[0-9]{3}[)]?[-\s.][0-9]{3,4}[-\s.][0-9]{4,6}\b/g },
14
+ ];
15
+ // ---------------------------------------------------------------------------
16
+ // Secret detection
17
+ // ---------------------------------------------------------------------------
18
+ export const SECRET_PREFIXES = [
19
+ "sk-", "sk_", "pk_", "ghp_", "AKIA", "xox", "SG.", "hf_",
20
+ "api-", "token-", "secret-",
21
+ ];
22
+ const BEARER_PATTERN = /Bearer\s+[A-Za-z0-9\-_.~+/]+=*/g;
23
+ const SECRET_PREFIX_PATTERN = new RegExp(`(?:${SECRET_PREFIXES.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})[A-Za-z0-9\\-_.~+/]{8,}=*`, "g");
24
+ // ---------------------------------------------------------------------------
25
+ // Shannon entropy
26
+ // ---------------------------------------------------------------------------
27
+ export function shannonEntropy(s) {
28
+ if (s.length === 0)
29
+ return 0;
30
+ const freq = new Map();
31
+ for (const ch of s)
32
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
33
+ let entropy = 0;
34
+ for (const count of freq.values()) {
35
+ const p = count / s.length;
36
+ entropy -= p * Math.log2(p);
37
+ }
38
+ return entropy;
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // Match collection (public — used by both gateway and guards)
42
+ // ---------------------------------------------------------------------------
43
+ export function collectMatches(content) {
44
+ const matches = [];
45
+ for (const entity of ENTITIES) {
46
+ entity.pattern.lastIndex = 0;
47
+ let m;
48
+ while ((m = entity.pattern.exec(content)) !== null) {
49
+ matches.push({ originalText: m[0], category: entity.categoryKey, placeholder: "" });
50
+ }
51
+ }
52
+ SECRET_PREFIX_PATTERN.lastIndex = 0;
53
+ let m;
54
+ while ((m = SECRET_PREFIX_PATTERN.exec(content)) !== null) {
55
+ matches.push({ originalText: m[0], category: "secret", placeholder: "" });
56
+ }
57
+ BEARER_PATTERN.lastIndex = 0;
58
+ while ((m = BEARER_PATTERN.exec(content)) !== null) {
59
+ matches.push({ originalText: m[0], category: "secret", placeholder: "" });
60
+ }
61
+ // High-entropy tokens
62
+ const tokenPattern = /\b[A-Za-z0-9\-_.~+/]{20,}={0,3}\b/g;
63
+ tokenPattern.lastIndex = 0;
64
+ while ((m = tokenPattern.exec(content)) !== null) {
65
+ const token = m[0];
66
+ if (matches.some((e) => e.originalText === token))
67
+ continue;
68
+ if (/^[a-z]+$/.test(token))
69
+ continue;
70
+ if (shannonEntropy(token) >= 4.0) {
71
+ matches.push({ originalText: token, category: "secret", placeholder: "" });
72
+ }
73
+ }
74
+ return matches;
75
+ }
76
+ export { ENTITIES };
77
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/sanitizer/core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { collectMatches, shannonEntropy, SECRET_PREFIXES } from "./core.js";
2
+ export { sanitize, sanitizeMessages } from "./reversible.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sanitizer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { collectMatches, shannonEntropy, SECRET_PREFIXES } from "./core.js";
2
+ export { sanitize, sanitizeMessages } from "./reversible.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sanitizer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reversible sanitizer — replaces sensitive data with numbered placeholders
3
+ * and returns a mapping table for later restoration.
4
+ */
5
+ import type { SanitizeResult } from "../types.js";
6
+ export declare function sanitize(content: any): SanitizeResult;
7
+ export declare function sanitizeMessages(messages: any[]): SanitizeResult;
8
+ //# sourceMappingURL=reversible.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reversible.d.ts","sourceRoot":"","sources":["../../src/sanitizer/reversible.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAA6B,MAAM,aAAa,CAAC;AA+D7E,wBAAgB,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,cAAc,CAKrD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,cAAc,CAEhE"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Reversible sanitizer — replaces sensitive data with numbered placeholders
3
+ * and returns a mapping table for later restoration.
4
+ */
5
+ import { collectMatches } from "./core.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Text-level sanitization
8
+ // ---------------------------------------------------------------------------
9
+ function sanitizeText(text, mappingTable, counters) {
10
+ const matches = collectMatches(text);
11
+ if (matches.length === 0)
12
+ return text;
13
+ // Deduplicate by original text
14
+ const unique = new Map();
15
+ for (const m of matches) {
16
+ if (!unique.has(m.originalText))
17
+ unique.set(m.originalText, m);
18
+ }
19
+ // Sort longest-first to avoid partial replacement
20
+ const sorted = [...unique.values()].sort((a, b) => b.originalText.length - a.originalText.length);
21
+ let sanitized = text;
22
+ for (const match of sorted) {
23
+ const n = (counters.get(match.category) ?? 0) + 1;
24
+ counters.set(match.category, n);
25
+ const placeholder = `__${match.category}_${n}__`;
26
+ const parts = sanitized.split(match.originalText);
27
+ if (parts.length > 1) {
28
+ sanitized = parts.join(placeholder);
29
+ mappingTable.set(placeholder, match.originalText);
30
+ }
31
+ }
32
+ return sanitized;
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Recursive value sanitization
36
+ // ---------------------------------------------------------------------------
37
+ function sanitizeValue(value, mappingTable, counters) {
38
+ if (typeof value === "string")
39
+ return sanitizeText(value, mappingTable, counters);
40
+ if (Array.isArray(value))
41
+ return value.map((v) => sanitizeValue(v, mappingTable, counters));
42
+ if (value !== null && typeof value === "object") {
43
+ const out = {};
44
+ for (const [k, v] of Object.entries(value))
45
+ out[k] = sanitizeValue(v, mappingTable, counters);
46
+ return out;
47
+ }
48
+ return value;
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Public API
52
+ // ---------------------------------------------------------------------------
53
+ export function sanitize(content) {
54
+ const mappingTable = new Map();
55
+ const counters = new Map();
56
+ const sanitized = sanitizeValue(content, mappingTable, counters);
57
+ return { sanitized, mappingTable, redactionCount: mappingTable.size };
58
+ }
59
+ export function sanitizeMessages(messages) {
60
+ return sanitize(messages);
61
+ }
62
+ //# sourceMappingURL=reversible.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reversible.js","sourceRoot":"","sources":["../../src/sanitizer/reversible.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * AI Security Gateway types
3
+ */
4
+ export type MappingTable = Map<string, string>;
5
+ export type SanitizeResult = {
6
+ sanitized: any;
7
+ mappingTable: MappingTable;
8
+ redactionCount: number;
9
+ };
10
+ export type GatewayConfig = {
11
+ port: number;
12
+ backends: {
13
+ anthropic?: {
14
+ baseUrl: string;
15
+ apiKey: string;
16
+ };
17
+ openai?: {
18
+ baseUrl: string;
19
+ apiKey: string;
20
+ };
21
+ gemini?: {
22
+ baseUrl: string;
23
+ apiKey: string;
24
+ };
25
+ openrouter?: {
26
+ baseUrl: string;
27
+ apiKey: string;
28
+ referer?: string;
29
+ title?: string;
30
+ };
31
+ };
32
+ routing?: {
33
+ [path: string]: keyof GatewayConfig["backends"];
34
+ };
35
+ };
36
+ export type EntityMatch = {
37
+ originalText: string;
38
+ category: string;
39
+ placeholder: string;
40
+ };
41
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE/C,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,GAAG,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QACR,SAAS,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAChD,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC7C,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC7C,UAAU,CAAC,EAAE;YACX,OAAO,EAAE,MAAM,CAAC;YAChB,MAAM,EAAE,MAAM,CAAC;YACf,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,KAAK,CAAC,EAAE,MAAM,CAAC;SAChB,CAAC;KACH,CAAC;IACF,OAAO,CAAC,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,aAAa,CAAC,UAAU,CAAC,CAAA;KAAE,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * AI Security Gateway types
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * HTTP utilities shared across handlers.
3
+ */
4
+ import type { IncomingMessage, ServerResponse } from "node:http";
5
+ export declare function readBody(req: IncomingMessage, maxBytes?: number): Promise<string>;
6
+ export declare function sendJSON(res: ServerResponse, status: number, data: unknown): void;
7
+ export declare function sendError(res: ServerResponse, status: number, message: string): void;
8
+ export declare function forwardError(res: ServerResponse, upstream: Response): Promise<void>;
9
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/utils/http.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAIjE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,eAAe,EAAE,QAAQ,SAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBzF;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjF;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEpF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnF"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * HTTP utilities shared across handlers.
3
+ */
4
+ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
5
+ export function readBody(req, maxBytes = MAX_BODY_BYTES) {
6
+ return new Promise((resolve, reject) => {
7
+ let body = "";
8
+ let bytes = 0;
9
+ req.on("data", (chunk) => {
10
+ bytes += chunk.length;
11
+ if (bytes > maxBytes) {
12
+ req.destroy();
13
+ reject(new Error(`Request body exceeds ${maxBytes} bytes`));
14
+ return;
15
+ }
16
+ body += chunk.toString();
17
+ });
18
+ req.on("end", () => resolve(body));
19
+ req.on("error", reject);
20
+ });
21
+ }
22
+ export function sendJSON(res, status, data) {
23
+ res.writeHead(status, { "Content-Type": "application/json" });
24
+ res.end(JSON.stringify(data));
25
+ }
26
+ export function sendError(res, status, message) {
27
+ sendJSON(res, status, { error: message });
28
+ }
29
+ export function forwardError(res, upstream) {
30
+ return upstream.text().then((body) => {
31
+ res.writeHead(upstream.status, { "Content-Type": "application/json" });
32
+ res.end(body);
33
+ });
34
+ }
35
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/utils/http.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;AAEjD,MAAM,UAAU,QAAQ,CAAC,GAAoB,EAAE,QAAQ,GAAG,cAAc;IACtE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAC/B,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC;YACtB,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;gBACrB,GAAG,CAAC,OAAO,EAAE,CAAC;gBACd,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,QAAQ,QAAQ,CAAC,CAAC,CAAC;gBAC5D,OAAO;YACT,CAAC;YACD,IAAI,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACnC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAa;IACzE,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC9D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAmB,EAAE,MAAc,EAAE,OAAe;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAmB,EAAE,QAAkB;IAClE,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACnC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;QACvE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * SSE streaming and JSON response utilities shared across handlers.
3
+ */
4
+ import type { ServerResponse } from "node:http";
5
+ import type { MappingTable } from "../types.js";
6
+ /**
7
+ * Pipe an upstream SSE stream to the client, restoring placeholders per line.
8
+ */
9
+ export declare function handleSSEStream(upstream: Response, res: ServerResponse, mappingTable: MappingTable): Promise<void>;
10
+ /**
11
+ * Read a full JSON response from upstream, restore placeholders, and send it back.
12
+ */
13
+ export declare function handleJSONResponse(upstream: Response, res: ServerResponse, mappingTable: MappingTable): Promise<void>;
14
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/utils/stream.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,cAAc,EACnB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,cAAc,EACnB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * SSE streaming and JSON response utilities shared across handlers.
3
+ */
4
+ import { restore, restoreSSELine } from "../restorer.js";
5
+ /**
6
+ * Pipe an upstream SSE stream to the client, restoring placeholders per line.
7
+ */
8
+ export async function handleSSEStream(upstream, res, mappingTable) {
9
+ res.writeHead(200, {
10
+ "Content-Type": "text/event-stream",
11
+ "Cache-Control": "no-cache",
12
+ Connection: "keep-alive",
13
+ });
14
+ const reader = upstream.body?.getReader();
15
+ if (!reader) {
16
+ res.end();
17
+ return;
18
+ }
19
+ const decoder = new TextDecoder();
20
+ let buffer = "";
21
+ try {
22
+ while (true) {
23
+ const { done, value } = await reader.read();
24
+ if (done)
25
+ break;
26
+ buffer += decoder.decode(value, { stream: true });
27
+ const lines = buffer.split("\n");
28
+ buffer = lines.pop() || "";
29
+ for (const line of lines) {
30
+ if (!line.trim()) {
31
+ res.write("\n");
32
+ continue;
33
+ }
34
+ res.write(restoreSSELine(line, mappingTable) + "\n");
35
+ }
36
+ }
37
+ if (buffer.trim()) {
38
+ res.write(restoreSSELine(buffer, mappingTable) + "\n");
39
+ }
40
+ }
41
+ catch (err) {
42
+ console.error("[opentrust-gateway] Stream error:", err);
43
+ }
44
+ res.end();
45
+ }
46
+ /**
47
+ * Read a full JSON response from upstream, restore placeholders, and send it back.
48
+ */
49
+ export async function handleJSONResponse(upstream, res, mappingTable) {
50
+ const data = JSON.parse(await upstream.text());
51
+ const restored = restore(data, mappingTable);
52
+ res.writeHead(200, { "Content-Type": "application/json" });
53
+ res.end(JSON.stringify(restored));
54
+ }
55
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/utils/stream.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEzD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAkB,EAClB,GAAmB,EACnB,YAA0B;IAE1B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;QACjB,cAAc,EAAE,mBAAmB;QACnC,eAAe,EAAE,UAAU;QAC3B,UAAU,EAAE,YAAY;KACzB,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QAAC,OAAO;IAAC,CAAC;IAEnC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,IAAI,CAAC;QACH,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAEhB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAClD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACjC,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YAE3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;oBAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBAAC,SAAS;gBAAC,CAAC;gBAChD,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YAClB,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,GAAG,CAAC,CAAC;IAC1D,CAAC;IACD,GAAG,CAAC,GAAG,EAAE,CAAC;AACZ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,QAAkB,EAClB,GAAmB,EACnB,YAA0B;IAE1B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAC7C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;AACpC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@opentrust/gateway",
3
+ "version": "7.1.0",
4
+ "description": "AI Security Gateway - secure proxy for LLM APIs with PII sanitization and credential detection",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./sanitizer/core": "./dist/sanitizer/core.js"
11
+ },
12
+ "bin": {
13
+ "opentrust-gateway": "dist/index.js"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsx src/index.ts",
18
+ "start": "node dist/index.js",
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "tsx test/sanitizer.test.ts",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "ai-security",
25
+ "ai-gateway",
26
+ "llm-proxy",
27
+ "pii-sanitization",
28
+ "credential-detection",
29
+ "opentrust",
30
+ "anthropic",
31
+ "openai",
32
+ "gemini"
33
+ ],
34
+ "author": "OpenTrust",
35
+ "license": "Apache-2.0",
36
+ "homepage": "https://github.com/opentrust/opentrust#readme",
37
+ "repository": { "type": "git", "url": "git+https://github.com/opentrust/opentrust.git", "directory": "gateway" },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "files": [
42
+ "dist/",
43
+ "src/"
44
+ ],
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "tsx": "^4.0.0",
48
+ "typescript": "^5.6.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ }
53
+ }
package/src/config.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Gateway configuration management
3
+ */
4
+
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import type { GatewayConfig } from "./types.js";
9
+
10
+ const CONFIG_PATH = join(homedir(), ".opentrust", "gateway.json");
11
+ const LEGACY_CONFIG_PATH = join(homedir(), ".opentrust-legacy", "gateway.json");
12
+
13
+ export function loadConfig(configPath?: string): GatewayConfig {
14
+ const defaults: GatewayConfig = {
15
+ port: parseInt(process.env.GATEWAY_PORT || "8900", 10),
16
+ backends: {},
17
+ };
18
+
19
+ // Try explicit path, then new path, then legacy path
20
+ for (const p of [configPath, CONFIG_PATH, LEGACY_CONFIG_PATH]) {
21
+ if (p && existsSync(p)) {
22
+ try {
23
+ const file = JSON.parse(readFileSync(p, "utf-8"));
24
+ return applyEnvOverrides(merge(defaults, file));
25
+ } catch (err) {
26
+ console.warn(`[opentrust-gateway] Failed to load config from ${p}:`, err);
27
+ }
28
+ }
29
+ }
30
+
31
+ return applyEnvOverrides(defaults);
32
+ }
33
+
34
+ function applyEnvOverrides(config: GatewayConfig): GatewayConfig {
35
+ const env = process.env;
36
+
37
+ if (env.ANTHROPIC_API_KEY) {
38
+ config.backends.anthropic = {
39
+ baseUrl: env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
40
+ apiKey: env.ANTHROPIC_API_KEY,
41
+ };
42
+ }
43
+
44
+ if (env.OPENAI_API_KEY) {
45
+ config.backends.openai = {
46
+ baseUrl: env.OPENAI_BASE_URL || "https://api.openai.com",
47
+ apiKey: env.OPENAI_API_KEY,
48
+ };
49
+ }
50
+
51
+ if ((env.KIMI_API_KEY || env.MOONSHOT_API_KEY) && !config.backends.openai) {
52
+ config.backends.openai = {
53
+ baseUrl: env.KIMI_BASE_URL || "https://api.moonshot.cn",
54
+ apiKey: env.KIMI_API_KEY || env.MOONSHOT_API_KEY || "",
55
+ };
56
+ }
57
+
58
+ if (env.GEMINI_API_KEY || env.GOOGLE_API_KEY) {
59
+ config.backends.gemini = {
60
+ baseUrl: env.GEMINI_BASE_URL || "https://generativelanguage.googleapis.com",
61
+ apiKey: env.GEMINI_API_KEY || env.GOOGLE_API_KEY || "",
62
+ };
63
+ }
64
+
65
+ if (env.OPENROUTER_API_KEY) {
66
+ config.backends.openrouter = {
67
+ baseUrl: env.OPENROUTER_BASE_URL || "https://openrouter.ai/api",
68
+ apiKey: env.OPENROUTER_API_KEY,
69
+ ...(env.OPENROUTER_REFERER && { referer: env.OPENROUTER_REFERER }),
70
+ ...(env.OPENROUTER_TITLE && { title: env.OPENROUTER_TITLE }),
71
+ };
72
+ }
73
+
74
+ return config;
75
+ }
76
+
77
+ function merge(base: GatewayConfig, file: Partial<GatewayConfig>): GatewayConfig {
78
+ return {
79
+ port: file.port ?? base.port,
80
+ backends: { ...base.backends, ...file.backends },
81
+ routing: file.routing,
82
+ };
83
+ }
84
+
85
+ export function validateConfig(config: GatewayConfig): void {
86
+ if (config.port < 1 || config.port > 65535) throw new Error(`Invalid port: ${config.port}`);
87
+ for (const [name, backend] of Object.entries(config.backends)) {
88
+ if (!backend.baseUrl) throw new Error(`Backend ${name} missing baseUrl`);
89
+ if (!backend.apiKey) throw new Error(`Backend ${name} missing apiKey`);
90
+ }
91
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Anthropic Messages API handler — POST /v1/messages
3
+ */
4
+
5
+ import type { IncomingMessage, ServerResponse } from "node:http";
6
+ import type { GatewayConfig } from "../types.js";
7
+ import { sanitize } from "../sanitizer/index.js";
8
+ import { readBody, sendError, forwardError } from "../utils/http.js";
9
+ import { handleSSEStream, handleJSONResponse } from "../utils/stream.js";
10
+
11
+ export async function handleAnthropicRequest(
12
+ req: IncomingMessage,
13
+ res: ServerResponse,
14
+ config: GatewayConfig,
15
+ ): Promise<void> {
16
+ try {
17
+ const { model, messages, system, tools, max_tokens, temperature, stream = false, ...rest } =
18
+ JSON.parse(await readBody(req));
19
+
20
+ const { sanitized: sanitizedMessages, mappingTable } = sanitize(messages);
21
+ const sanitizedSystem = system ? sanitize(system).sanitized : system;
22
+
23
+ const backend = config.backends.anthropic;
24
+ if (!backend) { sendError(res, 500, "Anthropic backend not configured"); return; }
25
+
26
+ const response = await fetch(`${backend.baseUrl}/v1/messages`, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ "anthropic-version": (req.headers["anthropic-version"] as string) || "2023-06-01",
31
+ "x-api-key": backend.apiKey,
32
+ },
33
+ body: JSON.stringify({
34
+ model,
35
+ messages: sanitizedMessages,
36
+ ...(system && { system: sanitizedSystem }),
37
+ ...(tools && { tools }),
38
+ max_tokens,
39
+ ...(temperature !== undefined && { temperature }),
40
+ stream,
41
+ ...rest,
42
+ }),
43
+ });
44
+
45
+ if (!response.ok) { await forwardError(res, response); return; }
46
+
47
+ if (stream) await handleSSEStream(response, res, mappingTable);
48
+ else await handleJSONResponse(response, res, mappingTable);
49
+ } catch (error) {
50
+ console.error("[opentrust-gateway] Anthropic handler error:", error);
51
+ sendError(res, 500, error instanceof Error ? error.message : String(error));
52
+ }
53
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Google Gemini handler — POST /v1/models/:model:generateContent
3
+ */
4
+
5
+ import type { IncomingMessage, ServerResponse } from "node:http";
6
+ import type { GatewayConfig } from "../types.js";
7
+ import { sanitize } from "../sanitizer/index.js";
8
+ import { restore } from "../restorer.js";
9
+ import { readBody, sendError, forwardError, sendJSON } from "../utils/http.js";
10
+
11
+ export async function handleGeminiRequest(
12
+ req: IncomingMessage,
13
+ res: ServerResponse,
14
+ config: GatewayConfig,
15
+ modelName: string,
16
+ ): Promise<void> {
17
+ try {
18
+ const { contents, tools, generationConfig, ...rest } = JSON.parse(await readBody(req));
19
+ const { sanitized: sanitizedContents, mappingTable } = sanitize(contents);
20
+
21
+ const backend = config.backends.gemini;
22
+ if (!backend) { sendError(res, 500, "Gemini backend not configured"); return; }
23
+
24
+ const response = await fetch(`${backend.baseUrl}/v1/models/${modelName}:generateContent`, {
25
+ method: "POST",
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ "x-goog-api-key": backend.apiKey,
29
+ },
30
+ body: JSON.stringify({
31
+ contents: sanitizedContents,
32
+ ...(tools && { tools }),
33
+ ...(generationConfig && { generationConfig }),
34
+ ...rest,
35
+ }),
36
+ });
37
+
38
+ if (!response.ok) { await forwardError(res, response); return; }
39
+
40
+ const data = JSON.parse(await response.text());
41
+ sendJSON(res, 200, restore(data, mappingTable));
42
+ } catch (error) {
43
+ console.error("[opentrust-gateway] Gemini handler error:", error);
44
+ sendError(res, 500, error instanceof Error ? error.message : String(error));
45
+ }
46
+ }