@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,45 @@
1
+ import type { ServerResponse } from "node:http";
2
+ import type { GatewayConfig } from "../types.js";
3
+ import { findDefaultBackend, getBackendApiType } from "../config.js";
4
+
5
+ export async function handleModelsRequest(
6
+ res: ServerResponse,
7
+ config: GatewayConfig,
8
+ ): Promise<void> {
9
+ try {
10
+ // Find an OpenAI-compatible backend for models listing
11
+ const resolved = findDefaultBackend("openai", config);
12
+ if (!resolved) {
13
+ res.writeHead(500, { "Content-Type": "application/json" });
14
+ res.end(JSON.stringify({ error: "No OpenAI-compatible backend configured" }));
15
+ return;
16
+ }
17
+
18
+ const { backend } = resolved;
19
+ const modelsUrl = `${backend.baseUrl}/v1/models`;
20
+ const headers: Record<string, string> = {
21
+ "Authorization": `Bearer ${backend.apiKey}`,
22
+ };
23
+
24
+ if (backend.referer) {
25
+ headers["HTTP-Referer"] = backend.referer;
26
+ }
27
+ if (backend.title) {
28
+ headers["X-Title"] = backend.title;
29
+ }
30
+
31
+ const response = await fetch(modelsUrl, { headers });
32
+ const body = await response.text();
33
+ res.writeHead(response.status, { "Content-Type": "application/json" });
34
+ res.end(body);
35
+ } catch (error) {
36
+ console.error("[ai-security-gateway] Models request error:", error);
37
+ res.writeHead(500, { "Content-Type": "application/json" });
38
+ res.end(
39
+ JSON.stringify({
40
+ error: "Internal gateway error",
41
+ message: error instanceof Error ? error.message : String(error),
42
+ }),
43
+ );
44
+ }
45
+ }
@@ -0,0 +1,333 @@
1
+ /**
2
+ * AI Security Gateway - OpenAI Chat Completions API handler
3
+ *
4
+ * Handles POST /v1/chat/completions requests in OpenAI's format.
5
+ * Also compatible with OpenAI-compatible APIs (Kimi, DeepSeek, etc.)
6
+ */
7
+
8
+ import type { IncomingMessage, ServerResponse } from "node:http";
9
+ import type { BackendConfig, MappingTable } from "../types.js";
10
+ import { sanitize } from "../sanitizer.js";
11
+ import { restore, createStreamRestorer } from "../restorer.js";
12
+ import { generateRequestId, logSanitizeEvent, logRestoreEvent } from "../activity.js";
13
+
14
+ /**
15
+ * Handle OpenAI API request
16
+ *
17
+ * @param backend - Config for OpenAI-compatible backend
18
+ * @param extraHeaders - Optional additional headers (e.g., OpenRouter attribution)
19
+ */
20
+ export async function handleOpenAIRequest(
21
+ req: IncomingMessage,
22
+ res: ServerResponse,
23
+ backend: BackendConfig,
24
+ extraHeaders?: Record<string, string>,
25
+ ): Promise<void> {
26
+ try {
27
+ const requestId = generateRequestId();
28
+ const sanitizeStart = Date.now();
29
+
30
+ // 1. Parse request body
31
+ const body = await readBody(req);
32
+ const requestData = JSON.parse(body);
33
+
34
+ const {
35
+ model,
36
+ messages,
37
+ tools,
38
+ tool_choice,
39
+ temperature,
40
+ max_tokens,
41
+ stream = false,
42
+ ...rest
43
+ } = requestData;
44
+
45
+ // 2. Sanitize messages
46
+ const { sanitized: sanitizedMessages, mappingTable, redactionCount } = sanitize(messages);
47
+
48
+ // Debug: log what was sanitized
49
+ console.log(`[ai-security-gateway] Sanitized ${redactionCount} items`);
50
+ if (mappingTable.size > 0) {
51
+ for (const [placeholder, original] of mappingTable.entries()) {
52
+ console.log(`[ai-security-gateway] ${placeholder} <- (${original.length} chars)`);
53
+ }
54
+ }
55
+
56
+ // Log sanitization event
57
+ if (redactionCount > 0) {
58
+ logSanitizeEvent({
59
+ requestId,
60
+ backend: "openai",
61
+ endpoint: "/v1/chat/completions",
62
+ model,
63
+ mappingTable,
64
+ redactionCount,
65
+ durationMs: Date.now() - sanitizeStart,
66
+ });
67
+ }
68
+
69
+ // 3. Build sanitized request
70
+ const sanitizedRequest = {
71
+ model,
72
+ messages: sanitizedMessages,
73
+ ...(tools && { tools }),
74
+ ...(tool_choice && { tool_choice }),
75
+ ...(temperature !== undefined && { temperature }),
76
+ ...(max_tokens && { max_tokens }),
77
+ stream,
78
+ ...rest,
79
+ };
80
+
81
+ // 4. Use provided backend config
82
+ // Note: baseUrl already includes the full path prefix (e.g., /v1 or /v1/coding)
83
+ const apiUrl = `${backend.baseUrl}/chat/completions`;
84
+ const headers: Record<string, string> = {
85
+ "Content-Type": "application/json",
86
+ "Authorization": `Bearer ${backend.apiKey}`,
87
+ };
88
+ // Merge extra headers (e.g., OpenRouter attribution headers)
89
+ if (extraHeaders) {
90
+ Object.assign(headers, extraHeaders);
91
+ }
92
+ const response = await fetch(apiUrl, {
93
+ method: "POST",
94
+ headers,
95
+ body: JSON.stringify(sanitizedRequest),
96
+ });
97
+
98
+ if (!response.ok) {
99
+ // Forward error response
100
+ res.writeHead(response.status, { "Content-Type": "application/json" });
101
+ const errorBody = await response.text();
102
+ res.end(errorBody);
103
+ return;
104
+ }
105
+
106
+ // 6. Handle streaming or non-streaming response
107
+ if (stream) {
108
+ await handleOpenAIStream(response, res, mappingTable, requestId, model);
109
+ } else {
110
+ await handleOpenAINonStream(response, res, mappingTable, requestId, model);
111
+ }
112
+ } catch (error) {
113
+ console.error("[ai-security-gateway] OpenAI handler error:", error);
114
+ res.writeHead(500, { "Content-Type": "application/json" });
115
+ res.end(
116
+ JSON.stringify({
117
+ error: "Internal gateway error",
118
+ message: error instanceof Error ? error.message : String(error),
119
+ }),
120
+ );
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Handle streaming response (SSE) with smart placeholder restoration
126
+ *
127
+ * Uses StreamRestorer to detect `__` and buffer potential placeholders.
128
+ * Only buffers when necessary, maintaining streaming UX.
129
+ */
130
+ async function handleOpenAIStream(
131
+ response: Response,
132
+ res: ServerResponse,
133
+ mappingTable: MappingTable,
134
+ requestId: string,
135
+ model?: string,
136
+ ): Promise<void> {
137
+ const restoreStart = Date.now();
138
+
139
+ // Debug: log mapping table
140
+ if (mappingTable.size > 0) {
141
+ console.log(`[ai-security-gateway] Streaming with ${mappingTable.size} placeholders to restore`);
142
+ }
143
+
144
+ // Set SSE headers
145
+ res.writeHead(200, {
146
+ "Content-Type": "text/event-stream",
147
+ "Cache-Control": "no-cache",
148
+ "Connection": "keep-alive",
149
+ });
150
+
151
+ const reader = response.body?.getReader();
152
+ if (!reader) {
153
+ res.end();
154
+ return;
155
+ }
156
+
157
+ const decoder = new TextDecoder();
158
+ let lineBuffer = "";
159
+
160
+ // Create stream restorer for text content
161
+ const streamRestorer = createStreamRestorer(mappingTable);
162
+
163
+ // Buffer for SSE chunks waiting for restoration
164
+ const pendingChunks: Array<{ parsed: OpenAISSEChunk; originalLine: string }> = [];
165
+
166
+ try {
167
+ while (true) {
168
+ const { done, value } = await reader.read();
169
+ if (done) break;
170
+
171
+ // Decode chunk
172
+ lineBuffer += decoder.decode(value, { stream: true });
173
+
174
+ // Process complete lines
175
+ const lines = lineBuffer.split("\n");
176
+ lineBuffer = lines.pop() || ""; // Keep incomplete line in buffer
177
+
178
+ for (const line of lines) {
179
+ if (!line.trim()) {
180
+ // Flush pending chunks before empty line
181
+ flushPendingChunks(pendingChunks, streamRestorer, res);
182
+ res.write("\n");
183
+ continue;
184
+ }
185
+
186
+ if (!line.startsWith("data: ")) {
187
+ res.write(line + "\n");
188
+ continue;
189
+ }
190
+
191
+ const dataContent = line.slice(6);
192
+ if (dataContent === "[DONE]") {
193
+ // Finalize any pending content
194
+ flushPendingChunks(pendingChunks, streamRestorer, res);
195
+ res.write(line + "\n");
196
+ continue;
197
+ }
198
+
199
+ try {
200
+ const parsed = JSON.parse(dataContent) as OpenAISSEChunk;
201
+ const textContent = parsed.choices?.[0]?.delta?.content;
202
+
203
+ if (textContent !== undefined && mappingTable.size > 0) {
204
+ // Process text through stream restorer
205
+ const restored = streamRestorer.process(textContent);
206
+
207
+ if (restored.length > 0) {
208
+ // We have restorable content - flush it
209
+ const restoredChunk = { ...parsed };
210
+ restoredChunk.choices = parsed.choices.map((c, i) =>
211
+ i === 0 ? { ...c, delta: { ...c.delta, content: restored } } : c
212
+ );
213
+ res.write(`data: ${JSON.stringify(restoredChunk)}\n`);
214
+ }
215
+
216
+ // If restorer is buffering, we don't output anything yet
217
+ // Content will be output when buffer is flushed
218
+ } else {
219
+ // No text content or no mappings - pass through
220
+ res.write(line + "\n");
221
+ }
222
+ } catch {
223
+ // Not valid JSON, pass through
224
+ res.write(line + "\n");
225
+ }
226
+ }
227
+ }
228
+
229
+ // Write any remaining line buffer
230
+ if (lineBuffer.trim()) {
231
+ res.write(lineBuffer + "\n");
232
+ }
233
+
234
+ // Finalize stream restorer - flush any remaining buffered content
235
+ const finalContent = streamRestorer.finalize();
236
+ if (finalContent.length > 0) {
237
+ // Create a final chunk with remaining content
238
+ const finalChunk: OpenAISSEChunk = {
239
+ choices: [{ delta: { content: finalContent }, index: 0, finish_reason: null }],
240
+ };
241
+ res.write(`data: ${JSON.stringify(finalChunk)}\n`);
242
+ }
243
+
244
+ // Log restoration event
245
+ if (mappingTable.size > 0) {
246
+ logRestoreEvent({
247
+ requestId,
248
+ backend: "openai",
249
+ endpoint: "/v1/chat/completions",
250
+ model,
251
+ mappingTable,
252
+ restorationCount: mappingTable.size,
253
+ durationMs: Date.now() - restoreStart,
254
+ });
255
+ }
256
+
257
+ res.end();
258
+ } catch (error) {
259
+ console.error("[ai-security-gateway] Stream error:", error);
260
+ res.end();
261
+ }
262
+ }
263
+
264
+ /**
265
+ * OpenAI SSE chunk structure
266
+ */
267
+ interface OpenAISSEChunk {
268
+ choices: Array<{
269
+ delta: { content?: string; role?: string };
270
+ index: number;
271
+ finish_reason: string | null;
272
+ }>;
273
+ [key: string]: unknown;
274
+ }
275
+
276
+ /**
277
+ * Flush pending chunks with restored content
278
+ */
279
+ function flushPendingChunks(
280
+ _pendingChunks: Array<{ parsed: OpenAISSEChunk; originalLine: string }>,
281
+ _streamRestorer: ReturnType<typeof createStreamRestorer>,
282
+ _res: ServerResponse,
283
+ ): void {
284
+ // Currently unused - StreamRestorer handles buffering internally
285
+ }
286
+
287
+ /**
288
+ * Handle non-streaming response
289
+ */
290
+ async function handleOpenAINonStream(
291
+ response: Response,
292
+ res: ServerResponse,
293
+ mappingTable: MappingTable,
294
+ requestId: string,
295
+ model?: string,
296
+ ): Promise<void> {
297
+ const restoreStart = Date.now();
298
+ const responseBody = await response.text();
299
+ const responseData = JSON.parse(responseBody);
300
+
301
+ // Restore placeholders in response
302
+ const restoredData = restore(responseData, mappingTable);
303
+
304
+ // Log restoration event
305
+ if (mappingTable.size > 0) {
306
+ logRestoreEvent({
307
+ requestId,
308
+ backend: "openai",
309
+ endpoint: "/v1/chat/completions",
310
+ model,
311
+ mappingTable,
312
+ restorationCount: mappingTable.size,
313
+ durationMs: Date.now() - restoreStart,
314
+ });
315
+ }
316
+
317
+ res.writeHead(200, { "Content-Type": "application/json" });
318
+ res.end(JSON.stringify(restoredData));
319
+ }
320
+
321
+ /**
322
+ * Read request body as string
323
+ */
324
+ function readBody(req: IncomingMessage): Promise<string> {
325
+ return new Promise((resolve, reject) => {
326
+ let body = "";
327
+ req.on("data", (chunk) => {
328
+ body += chunk.toString();
329
+ });
330
+ req.on("end", () => resolve(body));
331
+ req.on("error", reject);
332
+ });
333
+ }