@yan162/changewayguard 6.8.25

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 (285) hide show
  1. package/LICENSE +21 -0
  2. package/OpenClaw-linux_Mac-Guide-zh.md +89 -0
  3. package/dashboard-dist/api/122.index.js +95 -0
  4. package/dashboard-dist/api/122.index.js.map +1 -0
  5. package/dashboard-dist/api/143.index.js +2734 -0
  6. package/dashboard-dist/api/143.index.js.map +1 -0
  7. package/dashboard-dist/api/154.index.js +4151 -0
  8. package/dashboard-dist/api/154.index.js.map +1 -0
  9. package/dashboard-dist/api/173.index.js +24112 -0
  10. package/dashboard-dist/api/173.index.js.map +1 -0
  11. package/dashboard-dist/api/217.index.js +44 -0
  12. package/dashboard-dist/api/217.index.js.map +1 -0
  13. package/dashboard-dist/api/222.index.js +90 -0
  14. package/dashboard-dist/api/222.index.js.map +1 -0
  15. package/dashboard-dist/api/280.index.js +213 -0
  16. package/dashboard-dist/api/280.index.js.map +1 -0
  17. package/dashboard-dist/api/369.index.js +115 -0
  18. package/dashboard-dist/api/369.index.js.map +1 -0
  19. package/dashboard-dist/api/374.index.js +1896 -0
  20. package/dashboard-dist/api/374.index.js.map +1 -0
  21. package/dashboard-dist/api/424.index.js +135 -0
  22. package/dashboard-dist/api/424.index.js.map +1 -0
  23. package/dashboard-dist/api/445.index.js +3562 -0
  24. package/dashboard-dist/api/445.index.js.map +1 -0
  25. package/dashboard-dist/api/555.index.js +496 -0
  26. package/dashboard-dist/api/555.index.js.map +1 -0
  27. package/dashboard-dist/api/573.index.js +806 -0
  28. package/dashboard-dist/api/573.index.js.map +1 -0
  29. package/dashboard-dist/api/580.index.js +1420 -0
  30. package/dashboard-dist/api/580.index.js.map +1 -0
  31. package/dashboard-dist/api/581.index.js +67 -0
  32. package/dashboard-dist/api/581.index.js.map +1 -0
  33. package/dashboard-dist/api/598.index.js +328 -0
  34. package/dashboard-dist/api/598.index.js.map +1 -0
  35. package/dashboard-dist/api/720.index.js +105 -0
  36. package/dashboard-dist/api/720.index.js.map +1 -0
  37. package/dashboard-dist/api/744.index.js +333 -0
  38. package/dashboard-dist/api/744.index.js.map +1 -0
  39. package/dashboard-dist/api/818.index.js +374 -0
  40. package/dashboard-dist/api/818.index.js.map +1 -0
  41. package/dashboard-dist/api/831.index.js +99 -0
  42. package/dashboard-dist/api/831.index.js.map +1 -0
  43. package/dashboard-dist/api/84.index.js +64 -0
  44. package/dashboard-dist/api/84.index.js.map +1 -0
  45. package/dashboard-dist/api/900.index.js +81 -0
  46. package/dashboard-dist/api/900.index.js.map +1 -0
  47. package/dashboard-dist/api/917.index.js +88 -0
  48. package/dashboard-dist/api/917.index.js.map +1 -0
  49. package/dashboard-dist/api/927.index.js +4250 -0
  50. package/dashboard-dist/api/927.index.js.map +1 -0
  51. package/dashboard-dist/api/948.index.js +64 -0
  52. package/dashboard-dist/api/948.index.js.map +1 -0
  53. package/dashboard-dist/api/982.index.js +67 -0
  54. package/dashboard-dist/api/982.index.js.map +1 -0
  55. package/dashboard-dist/api/99.index.js +1176 -0
  56. package/dashboard-dist/api/99.index.js.map +1 -0
  57. package/dashboard-dist/api/drizzle/sqlite/0000_short_captain_stacy.sql +70 -0
  58. package/dashboard-dist/api/drizzle/sqlite/0001_closed_magus.sql +10 -0
  59. package/dashboard-dist/api/drizzle/sqlite/0002_agent_capability_observation.sql +38 -0
  60. package/dashboard-dist/api/drizzle/sqlite/0003_auth_magic_link.sql +28 -0
  61. package/dashboard-dist/api/drizzle/sqlite/0004_static_scan_fields.sql +8 -0
  62. package/dashboard-dist/api/drizzle/sqlite/0005_gateway_activity.sql +24 -0
  63. package/dashboard-dist/api/drizzle/sqlite/0006_sour_marauders.sql +41 -0
  64. package/dashboard-dist/api/drizzle/sqlite/meta/0000_snapshot.json +460 -0
  65. package/dashboard-dist/api/drizzle/sqlite/meta/0001_snapshot.json +536 -0
  66. package/dashboard-dist/api/drizzle/sqlite/meta/0006_snapshot.json +1249 -0
  67. package/dashboard-dist/api/drizzle/sqlite/meta/_journal.json +55 -0
  68. package/dashboard-dist/api/index.js +28482 -0
  69. package/dashboard-dist/api/index.js.map +1 -0
  70. package/dashboard-dist/api/package.json +16 -0
  71. package/dashboard-dist/api/sourcemap-register.cjs +1 -0
  72. package/dashboard-dist/web/assets/index-BKUfzbIg.js +148 -0
  73. package/dashboard-dist/web/assets/index-rHRH99IQ.css +1 -0
  74. package/dashboard-dist/web/changeway-logo.png +0 -0
  75. package/dashboard-dist/web/favicon.svg +29 -0
  76. package/dashboard-dist/web/index.html +15 -0
  77. package/dashboard-dist/web/logo.svg +16 -0
  78. package/dist/agent/activation.d.ts +21 -0
  79. package/dist/agent/activation.d.ts.map +1 -0
  80. package/dist/agent/activation.js +94 -0
  81. package/dist/agent/activation.js.map +1 -0
  82. package/dist/agent/auth.d.ts +73 -0
  83. package/dist/agent/auth.d.ts.map +1 -0
  84. package/dist/agent/auth.js +363 -0
  85. package/dist/agent/auth.js.map +1 -0
  86. package/dist/agent/behavior-detector.d.ts +150 -0
  87. package/dist/agent/behavior-detector.d.ts.map +1 -0
  88. package/dist/agent/behavior-detector.js +559 -0
  89. package/dist/agent/behavior-detector.js.map +1 -0
  90. package/dist/agent/business-reporter.d.ts +114 -0
  91. package/dist/agent/business-reporter.d.ts.map +1 -0
  92. package/dist/agent/business-reporter.js +359 -0
  93. package/dist/agent/business-reporter.js.map +1 -0
  94. package/dist/agent/config-sync.d.ts +70 -0
  95. package/dist/agent/config-sync.d.ts.map +1 -0
  96. package/dist/agent/config-sync.js +133 -0
  97. package/dist/agent/config-sync.js.map +1 -0
  98. package/dist/agent/config.d.ts +98 -0
  99. package/dist/agent/config.d.ts.map +1 -0
  100. package/dist/agent/config.js +348 -0
  101. package/dist/agent/config.js.map +1 -0
  102. package/dist/agent/content-injection-scanner.d.ts +35 -0
  103. package/dist/agent/content-injection-scanner.d.ts.map +1 -0
  104. package/dist/agent/content-injection-scanner.js +270 -0
  105. package/dist/agent/content-injection-scanner.js.map +1 -0
  106. package/dist/agent/engine-log-writer.d.ts +6 -0
  107. package/dist/agent/engine-log-writer.d.ts.map +1 -0
  108. package/dist/agent/engine-log-writer.js +18 -0
  109. package/dist/agent/engine-log-writer.js.map +1 -0
  110. package/dist/agent/env.d.ts +19 -0
  111. package/dist/agent/env.d.ts.map +1 -0
  112. package/dist/agent/env.js +44 -0
  113. package/dist/agent/env.js.map +1 -0
  114. package/dist/agent/event-reporter.d.ts +87 -0
  115. package/dist/agent/event-reporter.d.ts.map +1 -0
  116. package/dist/agent/event-reporter.js +306 -0
  117. package/dist/agent/event-reporter.js.map +1 -0
  118. package/dist/agent/file-watcher.d.ts +50 -0
  119. package/dist/agent/file-watcher.d.ts.map +1 -0
  120. package/dist/agent/file-watcher.js +135 -0
  121. package/dist/agent/file-watcher.js.map +1 -0
  122. package/dist/agent/fs-utils.d.ts +22 -0
  123. package/dist/agent/fs-utils.d.ts.map +1 -0
  124. package/dist/agent/fs-utils.js +41 -0
  125. package/dist/agent/fs-utils.js.map +1 -0
  126. package/dist/agent/gateway-manager.d.ts +59 -0
  127. package/dist/agent/gateway-manager.d.ts.map +1 -0
  128. package/dist/agent/gateway-manager.js +583 -0
  129. package/dist/agent/gateway-manager.js.map +1 -0
  130. package/dist/agent/hook-types.d.ts +276 -0
  131. package/dist/agent/hook-types.d.ts.map +1 -0
  132. package/dist/agent/hook-types.js +51 -0
  133. package/dist/agent/hook-types.js.map +1 -0
  134. package/dist/agent/http-client.d.ts +19 -0
  135. package/dist/agent/http-client.d.ts.map +1 -0
  136. package/dist/agent/http-client.js +37 -0
  137. package/dist/agent/http-client.js.map +1 -0
  138. package/dist/agent/index.d.ts +8 -0
  139. package/dist/agent/index.d.ts.map +1 -0
  140. package/dist/agent/index.js +8 -0
  141. package/dist/agent/index.js.map +1 -0
  142. package/dist/agent/openclaw-hybrid-audit-changeway.js +1447 -0
  143. package/dist/agent/prompt-gate.d.ts +16 -0
  144. package/dist/agent/prompt-gate.d.ts.map +1 -0
  145. package/dist/agent/prompt-gate.js +58 -0
  146. package/dist/agent/prompt-gate.js.map +1 -0
  147. package/dist/agent/prompt-input.d.ts +9 -0
  148. package/dist/agent/prompt-input.d.ts.map +1 -0
  149. package/dist/agent/prompt-input.js +173 -0
  150. package/dist/agent/prompt-input.js.map +1 -0
  151. package/dist/agent/prompt-output.d.ts +4 -0
  152. package/dist/agent/prompt-output.d.ts.map +1 -0
  153. package/dist/agent/prompt-output.js +19 -0
  154. package/dist/agent/prompt-output.js.map +1 -0
  155. package/dist/agent/runner.d.ts +23 -0
  156. package/dist/agent/runner.d.ts.map +1 -0
  157. package/dist/agent/runner.js +165 -0
  158. package/dist/agent/runner.js.map +1 -0
  159. package/dist/agent/runtime-mode.d.ts +10 -0
  160. package/dist/agent/runtime-mode.d.ts.map +1 -0
  161. package/dist/agent/runtime-mode.js +19 -0
  162. package/dist/agent/runtime-mode.js.map +1 -0
  163. package/dist/agent/sanitizer.d.ts +10 -0
  164. package/dist/agent/sanitizer.d.ts.map +1 -0
  165. package/dist/agent/sanitizer.js +175 -0
  166. package/dist/agent/sanitizer.js.map +1 -0
  167. package/dist/agent/scan-activity.d.ts +19 -0
  168. package/dist/agent/scan-activity.d.ts.map +1 -0
  169. package/dist/agent/scan-activity.js +34 -0
  170. package/dist/agent/scan-activity.js.map +1 -0
  171. package/dist/agent/types.d.ts +177 -0
  172. package/dist/agent/types.d.ts.map +1 -0
  173. package/dist/agent/types.js +5 -0
  174. package/dist/agent/types.js.map +1 -0
  175. package/dist/agent/workspace-scanner.d.ts +35 -0
  176. package/dist/agent/workspace-scanner.d.ts.map +1 -0
  177. package/dist/agent/workspace-scanner.js +137 -0
  178. package/dist/agent/workspace-scanner.js.map +1 -0
  179. package/dist/dashboard-launcher.d.ts +52 -0
  180. package/dist/dashboard-launcher.d.ts.map +1 -0
  181. package/dist/dashboard-launcher.js +363 -0
  182. package/dist/dashboard-launcher.js.map +1 -0
  183. package/dist/gateway/activity.d.ts +52 -0
  184. package/dist/gateway/activity.d.ts.map +1 -0
  185. package/dist/gateway/activity.js +111 -0
  186. package/dist/gateway/activity.js.map +1 -0
  187. package/dist/gateway/config.d.ts +50 -0
  188. package/dist/gateway/config.d.ts.map +1 -0
  189. package/dist/gateway/config.js +200 -0
  190. package/dist/gateway/config.js.map +1 -0
  191. package/dist/gateway/handlers/anthropic.d.ts +12 -0
  192. package/dist/gateway/handlers/anthropic.d.ts.map +1 -0
  193. package/dist/gateway/handlers/anthropic.js +254 -0
  194. package/dist/gateway/handlers/anthropic.js.map +1 -0
  195. package/dist/gateway/handlers/gemini.d.ts +12 -0
  196. package/dist/gateway/handlers/gemini.d.ts.map +1 -0
  197. package/dist/gateway/handlers/gemini.js +101 -0
  198. package/dist/gateway/handlers/gemini.js.map +1 -0
  199. package/dist/gateway/handlers/models.d.ts +4 -0
  200. package/dist/gateway/handlers/models.d.ts.map +1 -0
  201. package/dist/gateway/handlers/models.js +36 -0
  202. package/dist/gateway/handlers/models.js.map +1 -0
  203. package/dist/gateway/handlers/openai.d.ts +16 -0
  204. package/dist/gateway/handlers/openai.d.ts.map +1 -0
  205. package/dist/gateway/handlers/openai.js +254 -0
  206. package/dist/gateway/handlers/openai.js.map +1 -0
  207. package/dist/gateway/index.d.ts +27 -0
  208. package/dist/gateway/index.d.ts.map +1 -0
  209. package/dist/gateway/index.js +290 -0
  210. package/dist/gateway/index.js.map +1 -0
  211. package/dist/gateway/mapping-store.d.ts +38 -0
  212. package/dist/gateway/mapping-store.d.ts.map +1 -0
  213. package/dist/gateway/mapping-store.js +74 -0
  214. package/dist/gateway/mapping-store.js.map +1 -0
  215. package/dist/gateway/restorer.d.ts +63 -0
  216. package/dist/gateway/restorer.d.ts.map +1 -0
  217. package/dist/gateway/restorer.js +284 -0
  218. package/dist/gateway/restorer.js.map +1 -0
  219. package/dist/gateway/sanitizer.d.ts +17 -0
  220. package/dist/gateway/sanitizer.d.ts.map +1 -0
  221. package/dist/gateway/sanitizer.js +228 -0
  222. package/dist/gateway/sanitizer.js.map +1 -0
  223. package/dist/gateway/types.d.ts +53 -0
  224. package/dist/gateway/types.d.ts.map +1 -0
  225. package/dist/gateway/types.js +5 -0
  226. package/dist/gateway/types.js.map +1 -0
  227. package/dist/index.d.ts +19 -0
  228. package/dist/index.d.ts.map +1 -0
  229. package/dist/index.js +2990 -0
  230. package/dist/index.js.map +1 -0
  231. package/dist/memory/index.d.ts +5 -0
  232. package/dist/memory/index.d.ts.map +1 -0
  233. package/dist/memory/index.js +5 -0
  234. package/dist/memory/index.js.map +1 -0
  235. package/dist/memory/store.d.ts +82 -0
  236. package/dist/memory/store.d.ts.map +1 -0
  237. package/dist/memory/store.js +194 -0
  238. package/dist/memory/store.js.map +1 -0
  239. package/dist/platform-client/index.d.ts +63 -0
  240. package/dist/platform-client/index.d.ts.map +1 -0
  241. package/dist/platform-client/index.js +294 -0
  242. package/dist/platform-client/index.js.map +1 -0
  243. package/dist/platform-client/types.d.ts +109 -0
  244. package/dist/platform-client/types.d.ts.map +1 -0
  245. package/dist/platform-client/types.js +3 -0
  246. package/dist/platform-client/types.js.map +1 -0
  247. package/dist/workspace-agents-guide.d.ts +22 -0
  248. package/dist/workspace-agents-guide.d.ts.map +1 -0
  249. package/dist/workspace-agents-guide.js +92 -0
  250. package/dist/workspace-agents-guide.js.map +1 -0
  251. package/dist/workspace-agents-sync.d.ts +24 -0
  252. package/dist/workspace-agents-sync.d.ts.map +1 -0
  253. package/dist/workspace-agents-sync.js +41 -0
  254. package/dist/workspace-agents-sync.js.map +1 -0
  255. package/dist/workspace-agents-watcher.d.ts +23 -0
  256. package/dist/workspace-agents-watcher.d.ts.map +1 -0
  257. package/dist/workspace-agents-watcher.js +152 -0
  258. package/dist/workspace-agents-watcher.js.map +1 -0
  259. package/dist/workspace-discovery.d.ts +11 -0
  260. package/dist/workspace-discovery.d.ts.map +1 -0
  261. package/dist/workspace-discovery.js +116 -0
  262. package/dist/workspace-discovery.js.map +1 -0
  263. package/gateway/package-lock.json +597 -0
  264. package/gateway/package.json +57 -0
  265. package/gateway/pnpm-lock.yaml +342 -0
  266. package/gateway/src/activity.ts +142 -0
  267. package/gateway/src/config.ts +246 -0
  268. package/gateway/src/handlers/anthropic.ts +328 -0
  269. package/gateway/src/handlers/gemini.ts +122 -0
  270. package/gateway/src/handlers/models.ts +45 -0
  271. package/gateway/src/handlers/openai.ts +333 -0
  272. package/gateway/src/index.ts +344 -0
  273. package/gateway/src/mapping-store.ts +88 -0
  274. package/gateway/src/restorer.ts +322 -0
  275. package/gateway/src/sanitizer.ts +298 -0
  276. package/gateway/src/types.ts +73 -0
  277. package/gateway/tsconfig.json +20 -0
  278. package/openclaw.plugin.json +86 -0
  279. package/package.json +74 -0
  280. package/samples/Untitled +1 -0
  281. package/samples/clean-email.txt +20 -0
  282. package/samples/test-document.md +53 -0
  283. package/samples/test-email-popup.txt +44 -0
  284. package/samples/test-email.txt +32 -0
  285. package/samples/test-webpage.html +51 -0
@@ -0,0 +1,246 @@
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, ApiType } from "./types.js";
9
+
10
+ const DEFAULT_CONFIG_PATH = join(homedir(), ".openclaw", "extensions", "moltguard", "data", "gateway.json");
11
+
12
+ /**
13
+ * Load gateway configuration from file or environment
14
+ */
15
+ export function loadConfig(configPath?: string): GatewayConfig {
16
+ const path = configPath || DEFAULT_CONFIG_PATH;
17
+
18
+ // Default configuration
19
+ const defaultConfig: GatewayConfig = {
20
+ port: parseInt(process.env.GATEWAY_PORT || "53669", 10),
21
+ backends: {},
22
+ };
23
+
24
+ // Try to load from file
25
+ if (existsSync(path)) {
26
+ try {
27
+ const fileContent = readFileSync(path, "utf-8");
28
+ const fileConfig = JSON.parse(fileContent);
29
+ return mergeConfig(defaultConfig, fileConfig);
30
+ } catch (error) {
31
+ console.warn(
32
+ `[ai-security-gateway] Failed to load config from ${path}:`,
33
+ error,
34
+ );
35
+ }
36
+ }
37
+
38
+ // Load from environment variables
39
+ return loadFromEnv(defaultConfig);
40
+ }
41
+
42
+ /**
43
+ * Load backend configs from environment variables
44
+ */
45
+ function loadFromEnv(config: GatewayConfig): GatewayConfig {
46
+ // Anthropic
47
+ if (process.env.ANTHROPIC_API_KEY) {
48
+ config.backends.anthropic = {
49
+ baseUrl: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
50
+ apiKey: process.env.ANTHROPIC_API_KEY,
51
+ type: "anthropic",
52
+ };
53
+ }
54
+
55
+ // OpenAI
56
+ if (process.env.OPENAI_API_KEY) {
57
+ config.backends.openai = {
58
+ baseUrl: process.env.OPENAI_BASE_URL || "https://api.openai.com",
59
+ apiKey: process.env.OPENAI_API_KEY,
60
+ type: "openai",
61
+ };
62
+ }
63
+
64
+ // Kimi (Moonshot) — only set if openai backend not already configured
65
+ if (
66
+ (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY) &&
67
+ !config.backends.openai
68
+ ) {
69
+ config.backends.kimi = {
70
+ baseUrl: process.env.KIMI_BASE_URL || "https://api.moonshot.cn",
71
+ apiKey: process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || "",
72
+ type: "openai",
73
+ };
74
+ }
75
+
76
+ // Gemini
77
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
78
+ config.backends.gemini = {
79
+ baseUrl:
80
+ process.env.GEMINI_BASE_URL ||
81
+ "https://generativelanguage.googleapis.com",
82
+ apiKey: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || "",
83
+ type: "gemini",
84
+ };
85
+ }
86
+
87
+ // OpenRouter
88
+ if (process.env.OPENROUTER_API_KEY) {
89
+ config.backends.openrouter = {
90
+ baseUrl: process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api",
91
+ apiKey: process.env.OPENROUTER_API_KEY,
92
+ type: "openai",
93
+ ...(process.env.OPENROUTER_REFERER && {
94
+ referer: process.env.OPENROUTER_REFERER,
95
+ }),
96
+ ...(process.env.OPENROUTER_TITLE && {
97
+ title: process.env.OPENROUTER_TITLE,
98
+ }),
99
+ };
100
+ }
101
+
102
+ return config;
103
+ }
104
+
105
+ /**
106
+ * Merge file config with default config
107
+ */
108
+ function mergeConfig(
109
+ defaultConfig: GatewayConfig,
110
+ fileConfig: Partial<GatewayConfig>,
111
+ ): GatewayConfig {
112
+ return {
113
+ port: fileConfig.port ?? defaultConfig.port,
114
+ backends: {
115
+ ...defaultConfig.backends,
116
+ ...fileConfig.backends,
117
+ },
118
+ routing: fileConfig.routing,
119
+ defaultBackends: fileConfig.defaultBackends,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Validate configuration
125
+ */
126
+ export function validateConfig(config: GatewayConfig): void {
127
+ if (config.port < 1 || config.port > 65535) {
128
+ throw new Error(`Invalid port: ${config.port}`);
129
+ }
130
+
131
+ // Note: Backends are now optional. Gateway will act as transparent proxy.
132
+ // If no backends configured, gateway will forward requests based on routing rules
133
+ // or pass through to the original target.
134
+
135
+ // Validate each backend (if any)
136
+ for (const [name, backend] of Object.entries(config.backends)) {
137
+ if (!backend.baseUrl) {
138
+ throw new Error(`Backend ${name} missing baseUrl`);
139
+ }
140
+ if (!backend.apiKey) {
141
+ throw new Error(`Backend ${name} missing apiKey`);
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Infer API type from backend name
148
+ */
149
+ export function inferApiType(name: string): ApiType {
150
+ const lower = name.toLowerCase();
151
+ if (lower.includes("anthropic") || lower.includes("claude")) {
152
+ return "anthropic";
153
+ }
154
+ if (lower.includes("gemini") || lower.includes("google")) {
155
+ return "gemini";
156
+ }
157
+ // Default to OpenAI-compatible for everything else
158
+ return "openai";
159
+ }
160
+
161
+ /**
162
+ * Get API type for a backend
163
+ */
164
+ export function getBackendApiType(name: string, config: GatewayConfig): ApiType {
165
+ const backend = config.backends[name];
166
+ if (backend?.type) {
167
+ return backend.type;
168
+ }
169
+ return inferApiType(name);
170
+ }
171
+
172
+ /**
173
+ * Find backend by API key
174
+ */
175
+ export function findBackendByApiKey(
176
+ apiKey: string,
177
+ config: GatewayConfig,
178
+ ): { name: string; backend: typeof config.backends[string] } | null {
179
+ for (const [name, backend] of Object.entries(config.backends)) {
180
+ if (backend.apiKey === apiKey) {
181
+ return { name, backend };
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Find default backend for an API type
189
+ */
190
+ export function findDefaultBackend(
191
+ apiType: ApiType,
192
+ config: GatewayConfig,
193
+ ): { name: string; backend: typeof config.backends[string] } | null {
194
+ // Check explicit default first
195
+ const defaultName = config.defaultBackends?.[apiType];
196
+ if (defaultName && config.backends[defaultName]) {
197
+ return { name: defaultName, backend: config.backends[defaultName] };
198
+ }
199
+
200
+ // Find first backend matching the API type
201
+ for (const [name, backend] of Object.entries(config.backends)) {
202
+ if (getBackendApiType(name, config) === apiType) {
203
+ return { name, backend };
204
+ }
205
+ }
206
+
207
+ return null;
208
+ }
209
+
210
+ /**
211
+ * Find backend by request path prefix
212
+ * Matches the longest pathPrefix that is a prefix of the request path
213
+ */
214
+ export function findBackendByPathPrefix(
215
+ requestPath: string,
216
+ config: GatewayConfig,
217
+ ): { name: string; backend: typeof config.backends[string] } | null {
218
+ let bestMatch: { name: string; backend: typeof config.backends[string] } | null = null;
219
+ let bestMatchLength = 0;
220
+
221
+ for (const [name, backend] of Object.entries(config.backends)) {
222
+ if (backend.pathPrefix && requestPath.startsWith(backend.pathPrefix)) {
223
+ if (backend.pathPrefix.length > bestMatchLength) {
224
+ bestMatch = { name, backend };
225
+ bestMatchLength = backend.pathPrefix.length;
226
+ }
227
+ }
228
+ }
229
+
230
+ return bestMatch;
231
+ }
232
+
233
+ /**
234
+ * Find backend by model name
235
+ */
236
+ export function findBackendByModel(
237
+ modelName: string,
238
+ config: GatewayConfig,
239
+ ): { name: string; backend: typeof config.backends[string] } | null {
240
+ for (const [name, backend] of Object.entries(config.backends)) {
241
+ if (backend.models?.includes(modelName)) {
242
+ return { name, backend };
243
+ }
244
+ }
245
+ return null;
246
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * AI Security Gateway - Anthropic Messages API handler
3
+ *
4
+ * Handles POST /v1/messages requests in Anthropic's native format.
5
+ */
6
+
7
+ import type { IncomingMessage, ServerResponse } from "node:http";
8
+ import type { BackendConfig, MappingTable } from "../types.js";
9
+ import { sanitize } from "../sanitizer.js";
10
+ import { restore, createStreamRestorer } from "../restorer.js";
11
+ import { generateRequestId, logSanitizeEvent, logRestoreEvent } from "../activity.js";
12
+
13
+ /**
14
+ * Handle Anthropic API request
15
+ */
16
+ export async function handleAnthropicRequest(
17
+ req: IncomingMessage,
18
+ res: ServerResponse,
19
+ backend: BackendConfig,
20
+ ): Promise<void> {
21
+ try {
22
+ const requestId = generateRequestId();
23
+ const sanitizeStart = Date.now();
24
+
25
+ // 1. Parse request body
26
+ const body = await readBody(req);
27
+ const requestData = JSON.parse(body);
28
+
29
+ const {
30
+ model,
31
+ messages,
32
+ system,
33
+ tools,
34
+ max_tokens,
35
+ temperature,
36
+ stream = false,
37
+ ...rest
38
+ } = requestData;
39
+
40
+ // 2. Sanitize messages
41
+ const { sanitized: sanitizedMessages, mappingTable, redactionCount } = sanitize(messages);
42
+
43
+ // 3. Sanitize system prompt if present
44
+ let systemRedactionCount = 0;
45
+ const sanitizedSystem = system
46
+ ? (() => {
47
+ const result = sanitize(system);
48
+ systemRedactionCount = result.redactionCount;
49
+ return result.sanitized;
50
+ })()
51
+ : system;
52
+
53
+ const totalRedactionCount = redactionCount + systemRedactionCount;
54
+
55
+ // Log sanitization event
56
+ if (totalRedactionCount > 0) {
57
+ logSanitizeEvent({
58
+ requestId,
59
+ backend: "anthropic",
60
+ endpoint: "/v1/messages",
61
+ model,
62
+ mappingTable,
63
+ redactionCount: totalRedactionCount,
64
+ durationMs: Date.now() - sanitizeStart,
65
+ });
66
+ }
67
+
68
+ // Note: We reuse the same mapping table so placeholders are consistent
69
+
70
+ // Debug: log what was sanitized
71
+ if (totalRedactionCount > 0) {
72
+ console.log(`[ai-security-gateway] Sanitized ${totalRedactionCount} items`);
73
+ }
74
+
75
+ // 4. Build sanitized request
76
+ const sanitizedRequest = {
77
+ model,
78
+ messages: sanitizedMessages,
79
+ ...(system && { system: sanitizedSystem }),
80
+ ...(tools && { tools }),
81
+ max_tokens,
82
+ ...(temperature !== undefined && { temperature }),
83
+ stream,
84
+ ...rest,
85
+ };
86
+
87
+ // 5. Forward to real Anthropic API
88
+ // Note: baseUrl already includes the full path prefix (e.g., /v1)
89
+ const apiUrl = `${backend.baseUrl}/messages`;
90
+ const response = await fetch(apiUrl, {
91
+ method: "POST",
92
+ headers: {
93
+ "Content-Type": "application/json",
94
+ "anthropic-version": req.headers["anthropic-version"] as string || "2023-06-01",
95
+ "x-api-key": backend.apiKey,
96
+ },
97
+ body: JSON.stringify(sanitizedRequest),
98
+ });
99
+
100
+ if (!response.ok) {
101
+ // Forward error response
102
+ res.writeHead(response.status, { "Content-Type": "application/json" });
103
+ const errorBody = await response.text();
104
+ res.end(errorBody);
105
+ return;
106
+ }
107
+
108
+ // 7. Handle streaming or non-streaming response
109
+ if (stream) {
110
+ await handleAnthropicStream(response, res, mappingTable, requestId, model);
111
+ } else {
112
+ await handleAnthropicNonStream(response, res, mappingTable, requestId, model);
113
+ }
114
+ } catch (error) {
115
+ console.error("[ai-security-gateway] Anthropic handler error:", error);
116
+ res.writeHead(500, { "Content-Type": "application/json" });
117
+ res.end(
118
+ JSON.stringify({
119
+ error: "Internal gateway error",
120
+ message: error instanceof Error ? error.message : String(error),
121
+ }),
122
+ );
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Handle streaming response with smart placeholder restoration
128
+ *
129
+ * Uses StreamRestorer to detect `__` and buffer potential placeholders.
130
+ * Only buffers when necessary, maintaining streaming UX.
131
+ */
132
+ async function handleAnthropicStream(
133
+ response: Response,
134
+ res: ServerResponse,
135
+ mappingTable: MappingTable,
136
+ requestId: string,
137
+ model?: string,
138
+ ): Promise<void> {
139
+ const restoreStart = Date.now();
140
+
141
+ // Debug: log mapping table size
142
+ if (mappingTable.size > 0) {
143
+ console.log(`[ai-security-gateway] Streaming with ${mappingTable.size} placeholders to restore`);
144
+ }
145
+
146
+ // Set SSE headers
147
+ res.writeHead(200, {
148
+ "Content-Type": "text/event-stream",
149
+ "Cache-Control": "no-cache",
150
+ "Connection": "keep-alive",
151
+ });
152
+
153
+ const reader = response.body?.getReader();
154
+ if (!reader) {
155
+ res.end();
156
+ return;
157
+ }
158
+
159
+ const decoder = new TextDecoder();
160
+ let lineBuffer = "";
161
+
162
+ // Create stream restorer for text content
163
+ const streamRestorer = createStreamRestorer(mappingTable);
164
+
165
+ try {
166
+ while (true) {
167
+ const { done, value } = await reader.read();
168
+ if (done) break;
169
+
170
+ // Decode chunk
171
+ lineBuffer += decoder.decode(value, { stream: true });
172
+
173
+ // Process complete lines
174
+ const lines = lineBuffer.split("\n");
175
+ lineBuffer = lines.pop() || ""; // Keep incomplete line in buffer
176
+
177
+ for (const line of lines) {
178
+ if (!line.trim()) {
179
+ res.write("\n");
180
+ continue;
181
+ }
182
+
183
+ // Handle event lines (pass through)
184
+ if (line.startsWith("event:")) {
185
+ res.write(line + "\n");
186
+ continue;
187
+ }
188
+
189
+ // Handle data lines
190
+ if (!line.startsWith("data: ")) {
191
+ res.write(line + "\n");
192
+ continue;
193
+ }
194
+
195
+ const dataContent = line.slice(6);
196
+
197
+ try {
198
+ const parsed = JSON.parse(dataContent) as AnthropicSSEChunk;
199
+
200
+ // Check for text delta
201
+ if (parsed.type === "content_block_delta" && parsed.delta?.type === "text_delta") {
202
+ const textContent = parsed.delta.text;
203
+
204
+ if (textContent !== undefined && mappingTable.size > 0) {
205
+ // Process text through stream restorer
206
+ const restored = streamRestorer.process(textContent);
207
+
208
+ if (restored.length > 0) {
209
+ // We have restorable content - output it
210
+ const restoredChunk = {
211
+ ...parsed,
212
+ delta: { ...parsed.delta, text: restored },
213
+ };
214
+ res.write(`data: ${JSON.stringify(restoredChunk)}\n`);
215
+ }
216
+ // If restorer is buffering, don't output anything yet
217
+ } else {
218
+ // No text content or no mappings - pass through
219
+ res.write(line + "\n");
220
+ }
221
+ } else {
222
+ // Non-text events - pass through
223
+ res.write(line + "\n");
224
+ }
225
+ } catch {
226
+ // Not valid JSON, pass through
227
+ res.write(line + "\n");
228
+ }
229
+ }
230
+ }
231
+
232
+ // Write any remaining line buffer
233
+ if (lineBuffer.trim()) {
234
+ res.write(lineBuffer + "\n");
235
+ }
236
+
237
+ // Finalize stream restorer - flush any remaining buffered content
238
+ const finalContent = streamRestorer.finalize();
239
+ if (finalContent.length > 0) {
240
+ // Create a final text delta chunk with remaining content
241
+ const finalChunk: AnthropicSSEChunk = {
242
+ type: "content_block_delta",
243
+ index: 0,
244
+ delta: { type: "text_delta", text: finalContent },
245
+ };
246
+ res.write(`data: ${JSON.stringify(finalChunk)}\n`);
247
+ }
248
+
249
+ // Log restoration event
250
+ if (mappingTable.size > 0) {
251
+ logRestoreEvent({
252
+ requestId,
253
+ backend: "anthropic",
254
+ endpoint: "/v1/messages",
255
+ model,
256
+ mappingTable,
257
+ restorationCount: mappingTable.size,
258
+ durationMs: Date.now() - restoreStart,
259
+ });
260
+ }
261
+
262
+ res.end();
263
+ } catch (error) {
264
+ console.error("[ai-security-gateway] Stream error:", error);
265
+ res.end();
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Anthropic SSE chunk structure
271
+ */
272
+ interface AnthropicSSEChunk {
273
+ type: string;
274
+ index?: number;
275
+ delta?: {
276
+ type: string;
277
+ text?: string;
278
+ };
279
+ [key: string]: unknown;
280
+ }
281
+
282
+ /**
283
+ * Handle non-streaming response
284
+ */
285
+ async function handleAnthropicNonStream(
286
+ response: Response,
287
+ res: ServerResponse,
288
+ mappingTable: MappingTable,
289
+ requestId: string,
290
+ model?: string,
291
+ ): Promise<void> {
292
+ const restoreStart = Date.now();
293
+ const responseBody = await response.text();
294
+ const responseData = JSON.parse(responseBody);
295
+
296
+ // Restore placeholders in response
297
+ const restoredData = restore(responseData, mappingTable);
298
+
299
+ // Log restoration event
300
+ if (mappingTable.size > 0) {
301
+ logRestoreEvent({
302
+ requestId,
303
+ backend: "anthropic",
304
+ endpoint: "/v1/messages",
305
+ model,
306
+ mappingTable,
307
+ restorationCount: mappingTable.size,
308
+ durationMs: Date.now() - restoreStart,
309
+ });
310
+ }
311
+
312
+ res.writeHead(200, { "Content-Type": "application/json" });
313
+ res.end(JSON.stringify(restoredData));
314
+ }
315
+
316
+ /**
317
+ * Read request body as string
318
+ */
319
+ function readBody(req: IncomingMessage): Promise<string> {
320
+ return new Promise((resolve, reject) => {
321
+ let body = "";
322
+ req.on("data", (chunk) => {
323
+ body += chunk.toString();
324
+ });
325
+ req.on("end", () => resolve(body));
326
+ req.on("error", reject);
327
+ });
328
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * AI Security Gateway - Google Gemini API handler
3
+ *
4
+ * Handles POST /v1/models/:model:generateContent requests in Gemini's format.
5
+ */
6
+
7
+ import type { IncomingMessage, ServerResponse } from "node:http";
8
+ import type { BackendConfig, MappingTable } from "../types.js";
9
+ import { sanitize } from "../sanitizer.js";
10
+ import { restore } from "../restorer.js";
11
+ import { generateRequestId, logSanitizeEvent, logRestoreEvent } from "../activity.js";
12
+
13
+ /**
14
+ * Handle Gemini API request
15
+ */
16
+ export async function handleGeminiRequest(
17
+ req: IncomingMessage,
18
+ res: ServerResponse,
19
+ backend: BackendConfig,
20
+ modelName: string,
21
+ ): Promise<void> {
22
+ try {
23
+ const requestId = generateRequestId();
24
+ const sanitizeStart = Date.now();
25
+
26
+ // 1. Parse request body
27
+ const body = await readBody(req);
28
+ const requestData = JSON.parse(body);
29
+
30
+ const { contents, tools, generationConfig, ...rest } = requestData;
31
+
32
+ // 2. Sanitize contents (Gemini uses "contents" instead of "messages")
33
+ const { sanitized: sanitizedContents, mappingTable, redactionCount } = sanitize(contents);
34
+
35
+ // Log sanitization event
36
+ if (redactionCount > 0) {
37
+ logSanitizeEvent({
38
+ requestId,
39
+ backend: "gemini",
40
+ endpoint: `/v1/models/${modelName}:generateContent`,
41
+ model: modelName,
42
+ mappingTable,
43
+ redactionCount,
44
+ durationMs: Date.now() - sanitizeStart,
45
+ });
46
+ }
47
+
48
+ // 3. Build sanitized request
49
+ const sanitizedRequest = {
50
+ contents: sanitizedContents,
51
+ ...(tools && { tools }),
52
+ ...(generationConfig && { generationConfig }),
53
+ ...rest,
54
+ };
55
+
56
+ // 4. Forward to Gemini API
57
+ const apiUrl = `${backend.baseUrl}/v1/models/${modelName}:generateContent`;
58
+ const response = await fetch(apiUrl, {
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ "x-goog-api-key": backend.apiKey,
63
+ },
64
+ body: JSON.stringify(sanitizedRequest),
65
+ });
66
+
67
+ if (!response.ok) {
68
+ // Forward error response
69
+ res.writeHead(response.status, { "Content-Type": "application/json" });
70
+ const errorBody = await response.text();
71
+ res.end(errorBody);
72
+ return;
73
+ }
74
+
75
+ // 6. Handle response (Gemini typically doesn't stream in same way)
76
+ const restoreStart = Date.now();
77
+ const responseBody = await response.text();
78
+ const responseData = JSON.parse(responseBody);
79
+
80
+ // Restore placeholders in response
81
+ const restoredData = restore(responseData, mappingTable);
82
+
83
+ // Log restoration event
84
+ if (mappingTable.size > 0) {
85
+ logRestoreEvent({
86
+ requestId,
87
+ backend: "gemini",
88
+ endpoint: `/v1/models/${modelName}:generateContent`,
89
+ model: modelName,
90
+ mappingTable,
91
+ restorationCount: mappingTable.size,
92
+ durationMs: Date.now() - restoreStart,
93
+ });
94
+ }
95
+
96
+ res.writeHead(200, { "Content-Type": "application/json" });
97
+ res.end(JSON.stringify(restoredData));
98
+ } catch (error) {
99
+ console.error("[ai-security-gateway] Gemini handler error:", error);
100
+ res.writeHead(500, { "Content-Type": "application/json" });
101
+ res.end(
102
+ JSON.stringify({
103
+ error: "Internal gateway error",
104
+ message: error instanceof Error ? error.message : String(error),
105
+ }),
106
+ );
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Read request body as string
112
+ */
113
+ function readBody(req: IncomingMessage): Promise<string> {
114
+ return new Promise((resolve, reject) => {
115
+ let body = "";
116
+ req.on("data", (chunk) => {
117
+ body += chunk.toString();
118
+ });
119
+ req.on("end", () => resolve(body));
120
+ req.on("error", reject);
121
+ });
122
+ }