@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,322 @@
1
+ /**
2
+ * AI Security Gateway - Content Restorer
3
+ *
4
+ * Restores sanitized placeholders back to original values.
5
+ * Handles LLM corruption patterns (missing underscores, case variations).
6
+ *
7
+ * Placeholder format: __PII_<ENTITY_TYPE>_<SERIAL_ID>__
8
+ */
9
+
10
+ import type { MappingTable } from "./types.js";
11
+
12
+ /**
13
+ * Build a map from placeholder patterns to original values
14
+ * Handles variations that LLMs might produce
15
+ */
16
+ function buildRestorationMap(mappingTable: MappingTable): Map<RegExp, string> {
17
+ const restorationMap = new Map<RegExp, string>();
18
+
19
+ for (const [placeholder, originalValue] of mappingTable.entries()) {
20
+ // Extract the core pattern from placeholder like __PII_EMAIL_ADDRESS_00000001__
21
+ const match = placeholder.match(/^__PII_([A-Z_]+)_(\d+)__$/);
22
+ if (!match) {
23
+ // Fallback: exact match only
24
+ restorationMap.set(new RegExp(escapeRegex(placeholder), "g"), originalValue);
25
+ continue;
26
+ }
27
+
28
+ const entityType = match[1];
29
+ const serialId = match[2];
30
+
31
+ // Create flexible pattern that handles LLM corruption:
32
+ // - Missing leading/trailing underscores
33
+ // - Case variations
34
+ // - Extra spaces
35
+ const flexiblePattern = new RegExp(
36
+ `_?_?PII[_\\s]*${entityType}[_\\s]*${serialId}_?_?`,
37
+ "gi"
38
+ );
39
+
40
+ restorationMap.set(flexiblePattern, originalValue);
41
+ }
42
+
43
+ return restorationMap;
44
+ }
45
+
46
+ /**
47
+ * Escape special regex characters
48
+ */
49
+ function escapeRegex(str: string): string {
50
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
51
+ }
52
+
53
+ /**
54
+ * Restore placeholders in a string
55
+ */
56
+ function restoreText(text: string, mappingTable: MappingTable): string {
57
+ if (mappingTable.size === 0) return text;
58
+
59
+ let restored = text;
60
+
61
+ // First pass: exact matches (fastest)
62
+ for (const [placeholder, originalValue] of mappingTable.entries()) {
63
+ if (restored.includes(placeholder)) {
64
+ restored = restored.split(placeholder).join(originalValue);
65
+ }
66
+ }
67
+
68
+ // Second pass: flexible patterns for LLM corruption
69
+ const restorationMap = buildRestorationMap(mappingTable);
70
+ for (const [pattern, originalValue] of restorationMap.entries()) {
71
+ restored = restored.replace(pattern, originalValue);
72
+ }
73
+
74
+ // Third pass: handle "leaked ID suffix" pattern
75
+ // LLM might output: "original_value_00000001" instead of just "original_value"
76
+ for (const [placeholder, originalValue] of mappingTable.entries()) {
77
+ const match = placeholder.match(/^__PII_[A-Z_]+_(\d+)__$/);
78
+ if (match) {
79
+ const serialId = match[1];
80
+ // Pattern: original value followed by underscore and serial ID
81
+ const leakedPattern = new RegExp(
82
+ escapeRegex(originalValue) + `[_\\s]*${serialId}`,
83
+ "g"
84
+ );
85
+ restored = restored.replace(leakedPattern, originalValue);
86
+ }
87
+ }
88
+
89
+ return restored;
90
+ }
91
+
92
+ /**
93
+ * Recursively restore any value (string, object, array)
94
+ */
95
+ function restoreValue(value: unknown, mappingTable: MappingTable): unknown {
96
+ if (typeof value === "string") {
97
+ return restoreText(value, mappingTable);
98
+ }
99
+
100
+ if (Array.isArray(value)) {
101
+ return value.map((item) => restoreValue(item, mappingTable));
102
+ }
103
+
104
+ if (value !== null && typeof value === "object") {
105
+ const restored: Record<string, unknown> = {};
106
+ for (const [key, val] of Object.entries(value)) {
107
+ restored[key] = restoreValue(val, mappingTable);
108
+ }
109
+ return restored;
110
+ }
111
+
112
+ return value;
113
+ }
114
+
115
+ /**
116
+ * Restore any content (object, array, string) using the mapping table
117
+ */
118
+ export function restore(content: unknown, mappingTable: MappingTable): unknown {
119
+ if (mappingTable.size === 0) return content;
120
+ return restoreValue(content, mappingTable);
121
+ }
122
+
123
+ /**
124
+ * Restore a JSON string
125
+ * Useful for SSE streaming where each chunk is a JSON string
126
+ */
127
+ export function restoreJSON(jsonString: string, mappingTable: MappingTable): string {
128
+ if (mappingTable.size === 0) return jsonString;
129
+
130
+ try {
131
+ const parsed = JSON.parse(jsonString);
132
+ const restored = restore(parsed, mappingTable);
133
+ return JSON.stringify(restored);
134
+ } catch {
135
+ // If not valid JSON, treat as plain text
136
+ return restoreText(jsonString, mappingTable);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Restore SSE data line (for streaming responses)
142
+ * Format: "data: {...}\n"
143
+ */
144
+ export function restoreSSELine(line: string, mappingTable: MappingTable): string {
145
+ if (mappingTable.size === 0) return line;
146
+ if (!line.startsWith("data: ")) return line;
147
+
148
+ const dataContent = line.slice(6); // Remove "data: " prefix
149
+ if (dataContent === "[DONE]") return line;
150
+
151
+ try {
152
+ const parsed = JSON.parse(dataContent);
153
+ const restored = restore(parsed, mappingTable);
154
+ return `data: ${JSON.stringify(restored)}`;
155
+ } catch {
156
+ // Fallback to text restoration
157
+ return `data: ${restoreText(dataContent, mappingTable)}`;
158
+ }
159
+ }
160
+
161
+ // =============================================================================
162
+ // Streaming Restoration with Smart Buffering
163
+ // =============================================================================
164
+
165
+ // Max placeholder length: __PII_VERIFICATION_CODE_00000001__ ≈ 40 chars
166
+ const MAX_PLACEHOLDER_LENGTH = 50;
167
+
168
+ // Pattern to match complete placeholders
169
+ const PLACEHOLDER_PATTERN = /__PII_[A-Z_]+_\d{8}__/g;
170
+
171
+ /**
172
+ * StreamRestorer - Stateful streaming restoration with smart buffering
173
+ *
174
+ * Only buffers when `__` is detected (potential placeholder start).
175
+ * Otherwise streams through immediately for best UX.
176
+ */
177
+ export class StreamRestorer {
178
+ private buffer: string = "";
179
+ private mappingTable: MappingTable;
180
+
181
+ constructor(mappingTable: MappingTable) {
182
+ this.mappingTable = mappingTable;
183
+ }
184
+
185
+ /**
186
+ * Process incoming text chunk
187
+ * Returns text that can be safely output (already restored or confirmed non-placeholder)
188
+ */
189
+ process(chunk: string): string {
190
+ // If no mappings, pass through directly
191
+ if (this.mappingTable.size === 0) {
192
+ return chunk;
193
+ }
194
+
195
+ this.buffer += chunk;
196
+ return this.flush();
197
+ }
198
+
199
+ /**
200
+ * Flush what we can safely output
201
+ * Keeps potential incomplete placeholders in buffer
202
+ */
203
+ private flush(): string {
204
+ let output = "";
205
+
206
+ while (this.buffer.length > 0) {
207
+ // Find position of `__` in buffer
208
+ const underscorePos = this.buffer.indexOf("__");
209
+
210
+ if (underscorePos === -1) {
211
+ // No `__` found - check if buffer ends with single `_`
212
+ if (this.buffer.endsWith("_")) {
213
+ // Keep the trailing `_` in case next chunk starts with `_`
214
+ output += this.buffer.slice(0, -1);
215
+ this.buffer = "_";
216
+ } else {
217
+ // Safe to output entire buffer
218
+ output += this.buffer;
219
+ this.buffer = "";
220
+ }
221
+ break;
222
+ }
223
+
224
+ // Output everything before the `__`
225
+ if (underscorePos > 0) {
226
+ output += this.buffer.slice(0, underscorePos);
227
+ this.buffer = this.buffer.slice(underscorePos);
228
+ }
229
+
230
+ // Now buffer starts with `__`
231
+ // Check if we have a complete placeholder
232
+ PLACEHOLDER_PATTERN.lastIndex = 0;
233
+ const match = PLACEHOLDER_PATTERN.exec(this.buffer);
234
+
235
+ if (match && match.index === 0) {
236
+ // Found complete placeholder at start of buffer
237
+ const placeholder = match[0];
238
+ const original = this.mappingTable.get(placeholder);
239
+
240
+ if (original) {
241
+ // Restore and output
242
+ output += original;
243
+ } else {
244
+ // Not in mapping table, output as-is
245
+ output += placeholder;
246
+ }
247
+
248
+ this.buffer = this.buffer.slice(placeholder.length);
249
+ } else {
250
+ // Check if buffer could be an incomplete placeholder
251
+ if (this.couldBePlaceholder(this.buffer)) {
252
+ // Keep buffering - might be incomplete placeholder
253
+ if (this.buffer.length > MAX_PLACEHOLDER_LENGTH) {
254
+ // Too long to be a placeholder - flush the `__` and continue
255
+ output += "__";
256
+ this.buffer = this.buffer.slice(2);
257
+ } else {
258
+ // Wait for more data
259
+ break;
260
+ }
261
+ } else {
262
+ // Definitely not a placeholder - output the `__`
263
+ output += "__";
264
+ this.buffer = this.buffer.slice(2);
265
+ }
266
+ }
267
+ }
268
+
269
+ return output;
270
+ }
271
+
272
+ /**
273
+ * Check if text could be the start of a placeholder
274
+ * Returns true if it matches the beginning of __PII_<TYPE>_<ID>__
275
+ */
276
+ private couldBePlaceholder(text: string): boolean {
277
+ // Must start with __
278
+ if (!text.startsWith("__")) return false;
279
+
280
+ // Check partial patterns
281
+ const partialPatterns = [
282
+ /^__$/,
283
+ /^__P$/,
284
+ /^__PI$/,
285
+ /^__PII$/,
286
+ /^__PII_$/,
287
+ /^__PII_[A-Z_]*$/,
288
+ /^__PII_[A-Z_]+_$/,
289
+ /^__PII_[A-Z_]+_\d*$/,
290
+ /^__PII_[A-Z_]+_\d+_?$/,
291
+ ];
292
+
293
+ return partialPatterns.some(pattern => pattern.test(text));
294
+ }
295
+
296
+ /**
297
+ * Finalize stream - flush any remaining buffer
298
+ * Call this at end of stream to ensure nothing is lost
299
+ */
300
+ finalize(): string {
301
+ if (this.buffer.length === 0) return "";
302
+
303
+ // Try to restore any remaining buffer
304
+ const result = restoreText(this.buffer, this.mappingTable);
305
+ this.buffer = "";
306
+ return result;
307
+ }
308
+
309
+ /**
310
+ * Check if there's pending data in buffer
311
+ */
312
+ hasPendingData(): boolean {
313
+ return this.buffer.length > 0;
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Create a streaming restorer for a response
319
+ */
320
+ export function createStreamRestorer(mappingTable: MappingTable): StreamRestorer {
321
+ return new StreamRestorer(mappingTable);
322
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * AI Security Gateway - Content Sanitizer
3
+ *
4
+ * Sanitizes sensitive data in a single request-response cycle.
5
+ * Placeholder format: __PII_<ENTITY_TYPE>_<SERIAL_ID>__
6
+ */
7
+
8
+ import type { SanitizeResult, MappingTable } from "./types.js";
9
+
10
+ // =============================================================================
11
+ // Entity Types
12
+ // =============================================================================
13
+
14
+ type EntityType =
15
+ | "EMAIL_ADDRESS"
16
+ | "URL_ADDRESS"
17
+ | "PHONE_NUMBER"
18
+ | "BANK_NUMBER"
19
+ | "PRIVATE_KEY"
20
+ | "PASSWORD"
21
+ | "VERIFICATION_CODE"
22
+ | "API_KEY"
23
+ | "IP_ADDRESS"
24
+ | "SSN"
25
+ | "CREDIT_CARD";
26
+
27
+ type EntityPattern = {
28
+ type: EntityType;
29
+ pattern: RegExp;
30
+ score: number;
31
+ captureGroup?: number;
32
+ };
33
+
34
+ // =============================================================================
35
+ // Detection Patterns
36
+ // =============================================================================
37
+
38
+ const ENTITY_PATTERNS: EntityPattern[] = [
39
+ // PEM Private Keys
40
+ {
41
+ type: "PRIVATE_KEY",
42
+ pattern: /-----BEGIN (?:OPENSSH |RSA |EC |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:OPENSSH |RSA |EC |DSA )?PRIVATE KEY-----/g,
43
+ score: 0.95,
44
+ },
45
+ // Email addresses
46
+ {
47
+ type: "EMAIL_ADDRESS",
48
+ pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
49
+ score: 0.90,
50
+ },
51
+ // URLs
52
+ {
53
+ type: "URL_ADDRESS",
54
+ pattern: /https?:\/\/[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+/g,
55
+ score: 0.80,
56
+ },
57
+ // Known API key prefixes
58
+ {
59
+ type: "API_KEY",
60
+ pattern: /\b(?:sk-[A-Za-z0-9]{20,}|sk_(?:live|test)_[A-Za-z0-9]{20,}|pk_(?:live|test)_[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36,}|gho_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,}|AKIA[A-Z0-9]{16}|xox[baprs]-[A-Za-z0-9-]+|SG\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|hf_[A-Za-z0-9]{30,})\b/g,
61
+ score: 0.90,
62
+ },
63
+ // Bearer tokens
64
+ {
65
+ type: "API_KEY",
66
+ pattern: /Bearer\s+[A-Za-z0-9\-_.~+/]{20,}={0,3}/g,
67
+ score: 0.85,
68
+ },
69
+ // Hex private keys (64 hex chars)
70
+ {
71
+ type: "PRIVATE_KEY",
72
+ pattern: /\b[0-9a-fA-F]{64}\b/g,
73
+ score: 0.75,
74
+ },
75
+ // Labeled password patterns
76
+ {
77
+ type: "PASSWORD",
78
+ pattern: /(?:password|passwd|pwd|pass|passcode)\s*[:=]\s*["']?(\S+)["']?/gi,
79
+ score: 0.80,
80
+ captureGroup: 1,
81
+ },
82
+ // Labeled API key patterns
83
+ {
84
+ type: "API_KEY",
85
+ pattern: /(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token)\s*[:=]\s*["']?([A-Za-z0-9\-_.~+/]{16,})["']?/gi,
86
+ score: 0.85,
87
+ captureGroup: 1,
88
+ },
89
+ // Phone numbers
90
+ {
91
+ type: "PHONE_NUMBER",
92
+ pattern: /\+?\d{1,3}[-.\s]?\(?\d{2,4}\)?[-.\s]?\d{3,4}[-.\s]?\d{3,4}/g,
93
+ score: 0.70,
94
+ },
95
+ // Credit card numbers
96
+ {
97
+ type: "CREDIT_CARD",
98
+ pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
99
+ score: 0.85,
100
+ },
101
+ // Bank account numbers
102
+ {
103
+ type: "BANK_NUMBER",
104
+ pattern: /\b\d{12,19}\b/g,
105
+ score: 0.60,
106
+ },
107
+ // SSN
108
+ {
109
+ type: "SSN",
110
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
111
+ score: 0.85,
112
+ },
113
+ // IP addresses
114
+ {
115
+ type: "IP_ADDRESS",
116
+ pattern: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g,
117
+ score: 0.70,
118
+ },
119
+ // Labeled verification codes
120
+ {
121
+ type: "VERIFICATION_CODE",
122
+ pattern: /(?:verification\s*code|verify\s*code|otp|2fa\s*code|auth(?:entication)?\s*code)\s*[:=\-]?\s*([A-Za-z0-9]{4,12})/gi,
123
+ score: 0.80,
124
+ captureGroup: 1,
125
+ },
126
+ ];
127
+
128
+ // =============================================================================
129
+ // Match Collection
130
+ // =============================================================================
131
+
132
+ type MatchWithMeta = {
133
+ originalText: string;
134
+ type: EntityType;
135
+ score: number;
136
+ start: number;
137
+ end: number;
138
+ };
139
+
140
+ function collectMatches(content: string): MatchWithMeta[] {
141
+ const matches: MatchWithMeta[] = [];
142
+
143
+ for (const entity of ENTITY_PATTERNS) {
144
+ entity.pattern.lastIndex = 0;
145
+ let m: RegExpExecArray | null;
146
+
147
+ while ((m = entity.pattern.exec(content)) !== null) {
148
+ let matchedText: string;
149
+ let start: number;
150
+
151
+ if (entity.captureGroup !== undefined && m[entity.captureGroup]) {
152
+ matchedText = m[entity.captureGroup];
153
+ start = m.index + m[0].indexOf(matchedText);
154
+ } else {
155
+ matchedText = m[0];
156
+ start = m.index;
157
+ }
158
+
159
+ matches.push({
160
+ originalText: matchedText,
161
+ type: entity.type,
162
+ score: entity.score,
163
+ start,
164
+ end: start + matchedText.length,
165
+ });
166
+ }
167
+ }
168
+
169
+ return matches;
170
+ }
171
+
172
+ // =============================================================================
173
+ // Span Merging
174
+ // =============================================================================
175
+
176
+ function mergeSpans(matches: MatchWithMeta[]): MatchWithMeta[] {
177
+ if (matches.length === 0) return [];
178
+
179
+ matches.sort((a, b) => {
180
+ if (a.start !== b.start) return a.start - b.start;
181
+ const lenDiff = (b.end - b.start) - (a.end - a.start);
182
+ if (lenDiff !== 0) return lenDiff;
183
+ return b.score - a.score;
184
+ });
185
+
186
+ const merged: MatchWithMeta[] = [];
187
+ let current = matches[0];
188
+
189
+ for (let i = 1; i < matches.length; i++) {
190
+ const next = matches[i];
191
+ if (next.start < current.end) {
192
+ const currentLen = current.end - current.start;
193
+ const nextLen = next.end - next.start;
194
+ if (next.score > current.score || (next.score === current.score && nextLen > currentLen)) {
195
+ current = next;
196
+ }
197
+ } else {
198
+ merged.push(current);
199
+ current = next;
200
+ }
201
+ }
202
+
203
+ merged.push(current);
204
+ return merged;
205
+ }
206
+
207
+ // =============================================================================
208
+ // Text Sanitization
209
+ // =============================================================================
210
+
211
+ function sanitizeText(
212
+ text: string,
213
+ mappingTable: MappingTable,
214
+ typeCounters: Map<EntityType, number>,
215
+ ): string {
216
+ const matches = collectMatches(text);
217
+ if (matches.length === 0) return text;
218
+
219
+ const merged = mergeSpans(matches);
220
+ const textToPlaceholder = new Map<string, string>();
221
+
222
+ for (const match of merged) {
223
+ if (!textToPlaceholder.has(match.originalText)) {
224
+ const counter = (typeCounters.get(match.type) ?? 0) + 1;
225
+ typeCounters.set(match.type, counter);
226
+ const paddedId = counter.toString().padStart(8, "0");
227
+ const placeholder = `__PII_${match.type}_${paddedId}__`;
228
+ textToPlaceholder.set(match.originalText, placeholder);
229
+ mappingTable.set(placeholder, match.originalText);
230
+ }
231
+ }
232
+
233
+ let sanitized = text;
234
+ const sortedMatches = [...merged].sort((a, b) => b.start - a.start);
235
+
236
+ for (const match of sortedMatches) {
237
+ const placeholder = textToPlaceholder.get(match.originalText)!;
238
+ sanitized = sanitized.slice(0, match.start) + placeholder + sanitized.slice(match.end);
239
+ }
240
+
241
+ return sanitized;
242
+ }
243
+
244
+ // =============================================================================
245
+ // Recursive Sanitization
246
+ // =============================================================================
247
+
248
+ function sanitizeValue(
249
+ value: unknown,
250
+ mappingTable: MappingTable,
251
+ typeCounters: Map<EntityType, number>,
252
+ ): unknown {
253
+ if (typeof value === "string") {
254
+ return sanitizeText(value, mappingTable, typeCounters);
255
+ }
256
+
257
+ if (Array.isArray(value)) {
258
+ return value.map((item) => sanitizeValue(item, mappingTable, typeCounters));
259
+ }
260
+
261
+ if (value !== null && typeof value === "object") {
262
+ const sanitized: Record<string, unknown> = {};
263
+ for (const [key, val] of Object.entries(value)) {
264
+ sanitized[key] = sanitizeValue(val, mappingTable, typeCounters);
265
+ }
266
+ return sanitized;
267
+ }
268
+
269
+ return value;
270
+ }
271
+
272
+ // =============================================================================
273
+ // Public API
274
+ // =============================================================================
275
+
276
+ /**
277
+ * Sanitize any content (messages array, object, string)
278
+ * Returns sanitized content and mapping table for restoration
279
+ */
280
+ export function sanitize(content: unknown): SanitizeResult {
281
+ const mappingTable: MappingTable = new Map();
282
+ const typeCounters = new Map<EntityType, number>();
283
+
284
+ const sanitized = sanitizeValue(content, mappingTable, typeCounters);
285
+
286
+ return {
287
+ sanitized,
288
+ mappingTable,
289
+ redactionCount: mappingTable.size,
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Sanitize messages array (common case for LLM APIs)
295
+ */
296
+ export function sanitizeMessages(messages: unknown[]): SanitizeResult {
297
+ return sanitize(messages);
298
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * AI Security Gateway types
3
+ */
4
+
5
+ // Mapping from placeholder to original value
6
+ export type MappingTable = Map<string, string>;
7
+
8
+ // Result of sanitization with mapping table
9
+ export type SanitizeResult = {
10
+ sanitized: any; // Sanitized content (same structure as input)
11
+ mappingTable: MappingTable; // placeholder -> original value
12
+ redactionCount: number; // Total redactions made
13
+ };
14
+
15
+ // API type for backend routing
16
+ export type ApiType = "anthropic" | "openai" | "gemini";
17
+
18
+ // Backend configuration
19
+ export type BackendConfig = {
20
+ baseUrl: string;
21
+ apiKey: string;
22
+ type?: ApiType; // API type (inferred from name if not set)
23
+ pathPrefix?: string; // Gateway path prefix for routing (e.g., "/v1/coding")
24
+ models?: string[]; // Model names this backend handles (e.g., ["gpt-4", "gpt-3.5-turbo"])
25
+ referer?: string; // HTTP-Referer (OpenRouter attribution)
26
+ title?: string; // X-Title (OpenRouter attribution)
27
+ };
28
+
29
+ // Gateway configuration
30
+ export type GatewayConfig = {
31
+ port: number;
32
+ // Flexible backends: any name allowed (e.g., "vllm", "deepseek", "anthropic")
33
+ backends: {
34
+ [name: string]: BackendConfig;
35
+ };
36
+ // Optional: route specific paths to specific backends
37
+ routing?: {
38
+ [path: string]: string; // path -> backend name
39
+ };
40
+ // Optional: default backend for each API type
41
+ defaultBackends?: {
42
+ anthropic?: string; // backend name for /v1/messages
43
+ openai?: string; // backend name for /v1/chat/completions
44
+ gemini?: string; // backend name for /v1/models/:model:generateContent
45
+ };
46
+ };
47
+
48
+ // Entity match
49
+ export type EntityMatch = {
50
+ originalText: string;
51
+ category: string;
52
+ placeholder: string;
53
+ };
54
+
55
+ // Gateway activity event for logging
56
+ export type GatewayActivityEvent = {
57
+ id: string;
58
+ timestamp: string;
59
+ requestId: string;
60
+ type: "sanitize" | "restore";
61
+ direction: "request" | "response";
62
+ backend: string;
63
+ endpoint: string;
64
+ model?: string;
65
+ // Sanitization stats
66
+ redactionCount: number;
67
+ categories: Record<string, number>; // category -> count
68
+ // Timing
69
+ durationMs?: number;
70
+ };
71
+
72
+ // Activity listener callback
73
+ export type ActivityListener = (event: GatewayActivityEvent) => void;
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "types": ["node"],
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*.ts"],
19
+ "exclude": ["node_modules", "dist", "test"]
20
+ }