palaryn 0.1.0 → 0.3.2

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 (344) hide show
  1. package/README.md +243 -588
  2. package/dist/sdk/typescript/src/client.js +2 -2
  3. package/dist/sdk/typescript/src/client.js.map +1 -1
  4. package/dist/src/anomaly/detector.d.ts +7 -4
  5. package/dist/src/anomaly/detector.d.ts.map +1 -1
  6. package/dist/src/anomaly/detector.js +22 -12
  7. package/dist/src/anomaly/detector.js.map +1 -1
  8. package/dist/src/audit/logger.d.ts +10 -0
  9. package/dist/src/audit/logger.d.ts.map +1 -1
  10. package/dist/src/audit/logger.js +52 -38
  11. package/dist/src/audit/logger.js.map +1 -1
  12. package/dist/src/auth/routes.d.ts.map +1 -1
  13. package/dist/src/auth/routes.js +35 -0
  14. package/dist/src/auth/routes.js.map +1 -1
  15. package/dist/src/budget/manager.d.ts +5 -0
  16. package/dist/src/budget/manager.d.ts.map +1 -1
  17. package/dist/src/budget/manager.js +32 -0
  18. package/dist/src/budget/manager.js.map +1 -1
  19. package/dist/src/budget/model-pricing.d.ts +20 -0
  20. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  21. package/dist/src/budget/model-pricing.js +107 -0
  22. package/dist/src/budget/model-pricing.js.map +1 -0
  23. package/dist/src/budget/usage-extractor.d.ts +3 -1
  24. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  25. package/dist/src/budget/usage-extractor.js +47 -3
  26. package/dist/src/budget/usage-extractor.js.map +1 -1
  27. package/dist/src/config/defaults.d.ts.map +1 -1
  28. package/dist/src/config/defaults.js +65 -13
  29. package/dist/src/config/defaults.js.map +1 -1
  30. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  31. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  32. package/dist/src/dlp/tool-patterns.js +34 -0
  33. package/dist/src/dlp/tool-patterns.js.map +1 -0
  34. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  35. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  36. package/dist/src/executor/filesystem-executor.js +192 -0
  37. package/dist/src/executor/filesystem-executor.js.map +1 -0
  38. package/dist/src/executor/http-executor.d.ts.map +1 -1
  39. package/dist/src/executor/http-executor.js +22 -2
  40. package/dist/src/executor/http-executor.js.map +1 -1
  41. package/dist/src/executor/index.d.ts +4 -0
  42. package/dist/src/executor/index.d.ts.map +1 -1
  43. package/dist/src/executor/index.js +9 -1
  44. package/dist/src/executor/index.js.map +1 -1
  45. package/dist/src/executor/shell-executor.d.ts +22 -0
  46. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  47. package/dist/src/executor/shell-executor.js +119 -0
  48. package/dist/src/executor/shell-executor.js.map +1 -0
  49. package/dist/src/executor/sql-executor.d.ts +29 -0
  50. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  51. package/dist/src/executor/sql-executor.js +114 -0
  52. package/dist/src/executor/sql-executor.js.map +1 -0
  53. package/dist/src/executor/websocket-executor.d.ts +26 -0
  54. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  55. package/dist/src/executor/websocket-executor.js +205 -0
  56. package/dist/src/executor/websocket-executor.js.map +1 -0
  57. package/dist/src/interceptor/index.d.ts +2 -0
  58. package/dist/src/interceptor/index.d.ts.map +1 -0
  59. package/dist/src/interceptor/index.js +6 -0
  60. package/dist/src/interceptor/index.js.map +1 -0
  61. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  62. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  63. package/dist/src/interceptor/provider-interceptor.js +302 -0
  64. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  65. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  66. package/dist/src/mcp/auth-verifier.js +3 -2
  67. package/dist/src/mcp/auth-verifier.js.map +1 -1
  68. package/dist/src/mcp/bridge.d.ts +14 -10
  69. package/dist/src/mcp/bridge.d.ts.map +1 -1
  70. package/dist/src/mcp/bridge.js +51 -227
  71. package/dist/src/mcp/bridge.js.map +1 -1
  72. package/dist/src/mcp/http-transport.d.ts +2 -0
  73. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  74. package/dist/src/mcp/http-transport.js +117 -66
  75. package/dist/src/mcp/http-transport.js.map +1 -1
  76. package/dist/src/mcp/internal-auth.d.ts +13 -0
  77. package/dist/src/mcp/internal-auth.d.ts.map +1 -0
  78. package/dist/src/mcp/internal-auth.js +12 -0
  79. package/dist/src/mcp/internal-auth.js.map +1 -0
  80. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  81. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  82. package/dist/src/mcp/tool-definitions.js +491 -0
  83. package/dist/src/mcp/tool-definitions.js.map +1 -0
  84. package/dist/src/middleware/auth.js.map +1 -1
  85. package/dist/src/middleware/session.js.map +1 -1
  86. package/dist/src/middleware/validate.d.ts +8 -0
  87. package/dist/src/middleware/validate.d.ts.map +1 -1
  88. package/dist/src/middleware/validate.js +45 -0
  89. package/dist/src/middleware/validate.js.map +1 -1
  90. package/dist/src/policy/engine.d.ts +4 -0
  91. package/dist/src/policy/engine.d.ts.map +1 -1
  92. package/dist/src/policy/engine.js +117 -0
  93. package/dist/src/policy/engine.js.map +1 -1
  94. package/dist/src/saas/routes.d.ts.map +1 -1
  95. package/dist/src/saas/routes.js +355 -22
  96. package/dist/src/saas/routes.js.map +1 -1
  97. package/dist/src/server/app.d.ts.map +1 -1
  98. package/dist/src/server/app.js +24 -3
  99. package/dist/src/server/app.js.map +1 -1
  100. package/dist/src/server/gateway.d.ts.map +1 -1
  101. package/dist/src/server/gateway.js +17 -0
  102. package/dist/src/server/gateway.js.map +1 -1
  103. package/dist/src/server/index.d.ts.map +1 -1
  104. package/dist/src/server/index.js +18 -0
  105. package/dist/src/server/index.js.map +1 -1
  106. package/dist/src/storage/interfaces.d.ts +14 -3
  107. package/dist/src/storage/interfaces.d.ts.map +1 -1
  108. package/dist/src/storage/memory.d.ts +2 -0
  109. package/dist/src/storage/memory.d.ts.map +1 -1
  110. package/dist/src/storage/memory.js +6 -0
  111. package/dist/src/storage/memory.js.map +1 -1
  112. package/dist/src/storage/postgres.d.ts +5 -0
  113. package/dist/src/storage/postgres.d.ts.map +1 -1
  114. package/dist/src/storage/postgres.js +16 -0
  115. package/dist/src/storage/postgres.js.map +1 -1
  116. package/dist/src/storage/redis.d.ts +10 -0
  117. package/dist/src/storage/redis.d.ts.map +1 -1
  118. package/dist/src/storage/redis.js +65 -0
  119. package/dist/src/storage/redis.js.map +1 -1
  120. package/dist/src/types/budget.d.ts +4 -0
  121. package/dist/src/types/budget.d.ts.map +1 -1
  122. package/dist/src/types/config.d.ts +58 -0
  123. package/dist/src/types/config.d.ts.map +1 -1
  124. package/dist/src/types/events.d.ts +1 -0
  125. package/dist/src/types/events.d.ts.map +1 -1
  126. package/dist/src/types/policy.d.ts +11 -1
  127. package/dist/src/types/policy.d.ts.map +1 -1
  128. package/dist/src/types/tool-result.d.ts +11 -0
  129. package/dist/src/types/tool-result.d.ts.map +1 -1
  130. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  131. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  132. package/dist/tests/unit/app-routes.test.js +715 -0
  133. package/dist/tests/unit/app-routes.test.js.map +1 -0
  134. package/dist/tests/unit/audit-logger.test.js +105 -0
  135. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  136. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  137. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  138. package/dist/tests/unit/auth-providers.test.js +279 -0
  139. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  140. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  141. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  142. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  143. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  144. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  145. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  146. package/dist/tests/unit/auth-verifier.test.js +505 -0
  147. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  148. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  149. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  150. package/dist/tests/unit/billing-routes.test.js +432 -0
  151. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  152. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  153. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  154. package/dist/tests/unit/config-defaults.test.js +119 -0
  155. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  156. package/dist/tests/unit/defaults.test.js +0 -10
  157. package/dist/tests/unit/defaults.test.js.map +1 -1
  158. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  159. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  160. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  161. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  162. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  163. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  164. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  165. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  166. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  167. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  168. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  169. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  170. package/dist/tests/unit/logger.test.d.ts +2 -0
  171. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  172. package/dist/tests/unit/logger.test.js +97 -0
  173. package/dist/tests/unit/logger.test.js.map +1 -0
  174. package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
  175. package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
  176. package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
  177. package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
  178. package/dist/tests/unit/metrics.test.js +102 -0
  179. package/dist/tests/unit/metrics.test.js.map +1 -1
  180. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  181. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  182. package/dist/tests/unit/model-pricing.test.js +87 -0
  183. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  184. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  185. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  186. package/dist/tests/unit/oauth-stores.test.js +260 -0
  187. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  188. package/dist/tests/unit/policy-engine.test.js +466 -0
  189. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  190. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  191. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  192. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  193. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  194. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  195. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  196. package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
  197. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  198. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  199. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  200. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  201. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  202. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  203. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  204. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  205. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  206. package/dist/tests/unit/saas-routes.test.js +3 -3
  207. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  208. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  209. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  210. package/dist/tests/unit/shell-executor.test.js +145 -0
  211. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  212. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  213. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  214. package/dist/tests/unit/sql-executor.test.js +177 -0
  215. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  216. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  217. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  218. package/dist/tests/unit/stream-proxy.test.js +147 -0
  219. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  220. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  221. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  222. package/dist/tests/unit/tool-definitions.test.js +184 -0
  223. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  224. package/dist/tests/unit/usage-extractor.test.js +140 -0
  225. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  226. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  227. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  228. package/dist/tests/unit/webhook-handler.test.js +453 -0
  229. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  230. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  231. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  232. package/dist/tests/unit/webhook-routes.test.js +69 -0
  233. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  234. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  235. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  236. package/dist/tests/unit/websocket-executor.test.js +121 -0
  237. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  238. package/package.json +8 -2
  239. package/policy-packs/demo_fail.yaml +41 -0
  240. package/policy-packs/full_tools.yaml +136 -0
  241. package/src/admin/index.ts +1 -0
  242. package/src/admin/routes.ts +509 -0
  243. package/src/admin/templates.ts +572 -0
  244. package/src/anomaly/detector.ts +730 -0
  245. package/src/anomaly/index.ts +1 -0
  246. package/src/approval/manager.ts +569 -0
  247. package/src/approval/webhook.ts +133 -0
  248. package/src/audit/logger.ts +490 -0
  249. package/src/auth/index.ts +5 -0
  250. package/src/auth/password.ts +21 -0
  251. package/src/auth/pkce.ts +22 -0
  252. package/src/auth/providers.ts +208 -0
  253. package/src/auth/routes.ts +561 -0
  254. package/src/auth/session.ts +84 -0
  255. package/src/billing/index.ts +6 -0
  256. package/src/billing/plan-enforcer.ts +135 -0
  257. package/src/billing/routes.ts +229 -0
  258. package/src/billing/stripe-client.ts +58 -0
  259. package/src/billing/webhook-handler.ts +182 -0
  260. package/src/billing/webhook-routes.ts +28 -0
  261. package/src/budget/manager.ts +679 -0
  262. package/src/budget/model-pricing.ts +119 -0
  263. package/src/budget/usage-extractor.ts +214 -0
  264. package/src/cli.ts +91 -0
  265. package/src/config/defaults.ts +261 -0
  266. package/src/config/validate.ts +88 -0
  267. package/src/dlp/composite-scanner.ts +213 -0
  268. package/src/dlp/index.ts +9 -0
  269. package/src/dlp/interfaces.ts +34 -0
  270. package/src/dlp/patterns.ts +30 -0
  271. package/src/dlp/prompt-injection-backend.ts +181 -0
  272. package/src/dlp/prompt-injection-patterns.ts +302 -0
  273. package/src/dlp/regex-backend.ts +181 -0
  274. package/src/dlp/scanner.ts +502 -0
  275. package/src/dlp/text-normalizer.ts +225 -0
  276. package/src/dlp/tool-patterns.ts +35 -0
  277. package/src/dlp/trufflehog-backend.ts +190 -0
  278. package/src/executor/filesystem-executor.ts +196 -0
  279. package/src/executor/http-executor.ts +349 -0
  280. package/src/executor/index.ts +9 -0
  281. package/src/executor/interfaces.ts +11 -0
  282. package/src/executor/noop-executor.ts +23 -0
  283. package/src/executor/registry.ts +64 -0
  284. package/src/executor/shell-executor.ts +148 -0
  285. package/src/executor/slack-executor.ts +176 -0
  286. package/src/executor/sql-executor.ts +146 -0
  287. package/src/executor/websocket-executor.ts +211 -0
  288. package/src/index.ts +24 -0
  289. package/src/interceptor/index.ts +1 -0
  290. package/src/interceptor/provider-interceptor.ts +315 -0
  291. package/src/mcp/auth-verifier.ts +152 -0
  292. package/src/mcp/bridge.ts +703 -0
  293. package/src/mcp/http-transport.ts +698 -0
  294. package/src/mcp/index.ts +9 -0
  295. package/src/mcp/internal-auth.ts +14 -0
  296. package/src/mcp/oauth-pages.ts +139 -0
  297. package/src/mcp/oauth-postgres-stores.ts +278 -0
  298. package/src/mcp/oauth-provider.ts +536 -0
  299. package/src/mcp/oauth-stores.ts +202 -0
  300. package/src/mcp/server.ts +55 -0
  301. package/src/mcp/tool-definitions.ts +562 -0
  302. package/src/metrics/collector.ts +357 -0
  303. package/src/metrics/index.ts +1 -0
  304. package/src/middleware/auth.ts +814 -0
  305. package/src/middleware/session.ts +85 -0
  306. package/src/middleware/validate.ts +130 -0
  307. package/src/policy/engine.ts +815 -0
  308. package/src/policy/index.ts +2 -0
  309. package/src/policy/opa-engine.ts +829 -0
  310. package/src/proxy/forward-proxy.ts +649 -0
  311. package/src/proxy/index.ts +1 -0
  312. package/src/ratelimit/limiter.ts +196 -0
  313. package/src/replay/engine.ts +142 -0
  314. package/src/replay/index.ts +1 -0
  315. package/src/saas/index.ts +1 -0
  316. package/src/saas/routes.ts +2178 -0
  317. package/src/server/app.ts +985 -0
  318. package/src/server/errors.ts +49 -0
  319. package/src/server/gateway.ts +1130 -0
  320. package/src/server/index.ts +307 -0
  321. package/src/server/logger.ts +255 -0
  322. package/src/server/stream-proxy.ts +202 -0
  323. package/src/storage/file-persistence.ts +315 -0
  324. package/src/storage/index.ts +4 -0
  325. package/src/storage/interfaces.ts +287 -0
  326. package/src/storage/memory.ts +686 -0
  327. package/src/storage/postgres.ts +1831 -0
  328. package/src/storage/redis.ts +835 -0
  329. package/src/tracing/index.ts +1 -0
  330. package/src/tracing/provider.ts +100 -0
  331. package/src/trust/calculator.ts +141 -0
  332. package/src/trust/index.ts +7 -0
  333. package/src/types/budget.ts +36 -0
  334. package/src/types/config.ts +278 -0
  335. package/src/types/events.ts +41 -0
  336. package/src/types/express.d.ts +14 -0
  337. package/src/types/index.ts +7 -0
  338. package/src/types/policy.ts +83 -0
  339. package/src/types/stripe-config.ts +11 -0
  340. package/src/types/subscription.ts +59 -0
  341. package/src/types/tool-call.ts +47 -0
  342. package/src/types/tool-result.ts +82 -0
  343. package/src/types/user.ts +125 -0
  344. package/tsconfig.json +24 -0
@@ -0,0 +1,509 @@
1
+ import express from 'express';
2
+ import * as crypto from 'crypto';
3
+ import * as fs from 'fs';
4
+ import * as yaml from 'js-yaml';
5
+ import { randomUUID } from 'crypto';
6
+ import { Gateway } from '../server/gateway';
7
+ import { GatewayConfig } from '../types/config';
8
+ import {
9
+ DashboardStats,
10
+ BudgetInfo,
11
+ CostSummary,
12
+ ApiKeyInfo,
13
+ dashboardTemplate,
14
+ approvalsTemplate,
15
+ policiesTemplate,
16
+ budgetsTemplate,
17
+ traceListTemplate,
18
+ traceDetailTemplate,
19
+ apiKeysTemplate,
20
+ layoutTemplate,
21
+ escapeHtml,
22
+ } from './templates';
23
+
24
+ // CSRF token store: token -> expiry timestamp (ms)
25
+ const csrfTokens = new Map<string, number>();
26
+
27
+ const CSRF_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
28
+ const MAX_CSRF_TOKENS = 10000;
29
+
30
+ /** Generate a single-use CSRF token with 1-hour expiry */
31
+ function generateCSRFToken(): string {
32
+ // Prune expired tokens on each generation call
33
+ const now = Date.now();
34
+ for (const [token, expiry] of csrfTokens) {
35
+ if (expiry < now) {
36
+ csrfTokens.delete(token);
37
+ }
38
+ }
39
+
40
+ // Hard cap: if still too many tokens, evict oldest
41
+ if (csrfTokens.size >= MAX_CSRF_TOKENS) {
42
+ const firstKey = csrfTokens.keys().next().value;
43
+ if (firstKey) csrfTokens.delete(firstKey);
44
+ }
45
+
46
+ const token = crypto.randomBytes(32).toString('hex');
47
+ csrfTokens.set(token, now + CSRF_TOKEN_TTL_MS);
48
+ return token;
49
+ }
50
+
51
+ /** Validate and consume a single-use CSRF token. Returns true if valid. */
52
+ function validateCSRFToken(token: string): boolean {
53
+ if (!token) return false;
54
+ const expiry = csrfTokens.get(token);
55
+ if (expiry === undefined) return false;
56
+
57
+ // Delete the token (single-use)
58
+ csrfTokens.delete(token);
59
+
60
+ // Check if expired
61
+ return Date.now() < expiry;
62
+ }
63
+
64
+ export function createAdminRouter(gateway: Gateway, config: GatewayConfig): express.Router {
65
+ const router = express.Router();
66
+
67
+ // Parse URL-encoded form bodies for POST routes
68
+ router.use(express.urlencoded({ extended: true }));
69
+
70
+ // ---------- GET /admin -- Dashboard overview ----------
71
+ router.get('/', (req, res) => {
72
+ try {
73
+ const auditLogger = gateway.getAuditLogger();
74
+ const allEvents = auditLogger.getAllEvents();
75
+ const pendingApprovals = gateway.getPendingApprovals();
76
+ const policy = gateway.getCurrentPolicy();
77
+
78
+ // Gather unique workspace IDs from events
79
+ const workspaceIds = new Set<string>();
80
+ for (const event of allEvents) {
81
+ if (event.workspace_id) {
82
+ workspaceIds.add(event.workspace_id);
83
+ }
84
+ }
85
+
86
+ const stats: DashboardStats = {
87
+ total_requests: allEvents.filter(e => e.event_type === 'TOOL_CALL_RECEIVED').length,
88
+ pending_approvals: pendingApprovals.length,
89
+ active_workspaces: workspaceIds.size,
90
+ policy_rules: policy.rules ? policy.rules.length : 0,
91
+ recent_events: allEvents.slice(-20).reverse(),
92
+ };
93
+
94
+ res.send(dashboardTemplate(stats));
95
+ } catch (err) {
96
+ const message = err instanceof Error ? err.message : 'Unknown error';
97
+ res.status(500).send(layoutTemplate('Error', 'dashboard',
98
+ `<div class="alert alert-error">Failed to load dashboard: ${escapeHtml(message)}</div>`));
99
+ }
100
+ });
101
+
102
+ // ---------- GET /admin/approvals -- Pending approvals list ----------
103
+ router.get('/approvals', (req, res) => {
104
+ try {
105
+ const approvals = gateway.getPendingApprovals();
106
+ const csrfToken = generateCSRFToken();
107
+ res.send(approvalsTemplate(approvals, csrfToken));
108
+ } catch (err) {
109
+ const message = err instanceof Error ? err.message : 'Unknown error';
110
+ res.status(500).send(layoutTemplate('Error', 'approvals',
111
+ `<div class="alert alert-error">Failed to load approvals: ${escapeHtml(message)}</div>`));
112
+ }
113
+ });
114
+
115
+ // ---------- POST /admin/approvals/:id/approve -- Approve an approval ----------
116
+ router.post('/approvals/:id/approve', async (req, res) => {
117
+ if (!validateCSRFToken(req.body._csrf)) {
118
+ res.status(403).send('Invalid or missing CSRF token');
119
+ return;
120
+ }
121
+
122
+ const approvalId = req.params.id;
123
+ const approvalManager = gateway.getApprovalManager();
124
+
125
+ try {
126
+ await approvalManager.resolveById(approvalId, 'admin', true);
127
+ } catch {
128
+ // Approval may have expired or already been resolved
129
+ }
130
+
131
+ res.redirect('/admin/approvals');
132
+ });
133
+
134
+ // ---------- POST /admin/approvals/:id/deny -- Deny an approval ----------
135
+ router.post('/approvals/:id/deny', async (req, res) => {
136
+ if (!validateCSRFToken(req.body._csrf)) {
137
+ res.status(403).send('Invalid or missing CSRF token');
138
+ return;
139
+ }
140
+
141
+ const approvalId = req.params.id;
142
+ const approvalManager = gateway.getApprovalManager();
143
+
144
+ try {
145
+ await approvalManager.resolveById(approvalId, 'admin', false, 'Denied via admin panel');
146
+ } catch {
147
+ // Approval may have expired or already been resolved
148
+ }
149
+
150
+ res.redirect('/admin/approvals');
151
+ });
152
+
153
+ // ---------- GET /admin/policies -- Policy viewer + YAML editor ----------
154
+ router.get('/policies', (req, res) => {
155
+ try {
156
+ const policy = gateway.getCurrentPolicy();
157
+ const csrfToken = generateCSRFToken();
158
+
159
+ // Read raw YAML from disk to pre-fill the editor
160
+ let policyYaml = '';
161
+ try {
162
+ policyYaml = fs.readFileSync(gateway.getPolicyPackPath(), 'utf-8');
163
+ } catch {
164
+ // If file can't be read, editor will be empty
165
+ }
166
+
167
+ // Handle success/error flash from POST redirect
168
+ const applied = typeof req.query.applied === 'string' ? req.query.applied : undefined;
169
+ const msg = typeof req.query.msg === 'string' ? req.query.msg : undefined;
170
+ const reloaded = typeof req.query.reloaded === 'string' ? req.query.reloaded : undefined;
171
+ const applyResult = applied !== undefined
172
+ ? { success: applied === '1', message: msg || (applied === '1' ? 'Policy applied successfully' : 'Failed to apply policy') }
173
+ : reloaded !== undefined
174
+ ? { success: reloaded === '1', message: msg || (reloaded === '1' ? 'Policy reloaded from disk' : 'Failed to reload policy') }
175
+ : undefined;
176
+
177
+ res.send(policiesTemplate(policy, undefined, csrfToken, policyYaml, applyResult));
178
+ } catch (err) {
179
+ const message = err instanceof Error ? err.message : 'Unknown error';
180
+ res.status(500).send(layoutTemplate('Error', 'policies',
181
+ `<div class="alert alert-error">Failed to load policies: ${escapeHtml(message)}</div>`));
182
+ }
183
+ });
184
+
185
+ // ---------- POST /admin/policies/apply -- Save YAML to disk & hot-reload ----------
186
+ router.post('/policies/apply', (req, res) => {
187
+ if (!validateCSRFToken(req.body._csrf)) {
188
+ res.status(403).send('Invalid or missing CSRF token');
189
+ return;
190
+ }
191
+
192
+ const policyYamlText = req.body.policy_yaml;
193
+
194
+ // Validate: not empty
195
+ if (!policyYamlText || typeof policyYamlText !== 'string' || policyYamlText.trim().length === 0) {
196
+ res.redirect('/admin/policies?applied=0&msg=' + encodeURIComponent('Policy YAML cannot be empty'));
197
+ return;
198
+ }
199
+
200
+ // Validate: parseable YAML
201
+ let parsed: any;
202
+ try {
203
+ parsed = yaml.load(policyYamlText);
204
+ } catch (err) {
205
+ const message = err instanceof Error ? err.message : 'Invalid YAML';
206
+ res.redirect('/admin/policies?applied=0&msg=' + encodeURIComponent('Invalid YAML: ' + message));
207
+ return;
208
+ }
209
+
210
+ // Validate: valid policy structure
211
+ const validation = gateway.validatePolicy(parsed);
212
+ if (!validation.valid) {
213
+ res.redirect('/admin/policies?applied=0&msg=' + encodeURIComponent('Validation errors: ' + validation.errors.join('; ')));
214
+ return;
215
+ }
216
+
217
+ // Write to disk
218
+ try {
219
+ fs.writeFileSync(gateway.getPolicyPackPath(), policyYamlText, 'utf-8');
220
+ } catch (err) {
221
+ const message = err instanceof Error ? err.message : 'Write failed';
222
+ res.redirect('/admin/policies?applied=0&msg=' + encodeURIComponent('Failed to write file: ' + message));
223
+ return;
224
+ }
225
+
226
+ // Hot-reload
227
+ const result = gateway.reloadPolicy();
228
+ if (result.success) {
229
+ res.redirect('/admin/policies?applied=1&msg=' + encodeURIComponent(`Policy reloaded successfully (${result.ruleCount} rules active)`));
230
+ } else {
231
+ res.redirect('/admin/policies?applied=0&msg=' + encodeURIComponent('Reload failed: ' + (result.error || 'unknown error')));
232
+ }
233
+ });
234
+
235
+ // ---------- POST /admin/policies/reload -- Hot-reload policy from disk ----------
236
+ router.post('/policies/reload', (req, res) => {
237
+ if (!validateCSRFToken(req.body._csrf)) {
238
+ res.status(403).send('Invalid or missing CSRF token');
239
+ return;
240
+ }
241
+
242
+ const result = gateway.reloadPolicy();
243
+ if (result.success) {
244
+ res.redirect('/admin/policies?reloaded=1&msg=' + encodeURIComponent(`Policy reloaded from disk (${result.ruleCount} rules active)`));
245
+ } else {
246
+ res.redirect('/admin/policies?reloaded=0&msg=' + encodeURIComponent('Reload failed: ' + (result.error || 'unknown error')));
247
+ }
248
+ });
249
+
250
+ // ---------- POST /admin/policies/validate -- Validate a policy ----------
251
+ router.post('/policies/validate', (req, res) => {
252
+ if (!validateCSRFToken(req.body._csrf)) {
253
+ res.status(403).send('Invalid or missing CSRF token');
254
+ return;
255
+ }
256
+
257
+ const policy = gateway.getCurrentPolicy();
258
+ const csrfToken = generateCSRFToken();
259
+ let validationResult: { valid: boolean; errors: string[] };
260
+
261
+ try {
262
+ const policyJson = req.body.policy_json;
263
+ const parsed = JSON.parse(policyJson);
264
+ validationResult = gateway.validatePolicy(parsed);
265
+ } catch (err) {
266
+ const message = err instanceof Error ? err.message : 'Invalid JSON';
267
+ validationResult = { valid: false, errors: [message] };
268
+ }
269
+
270
+ // Read current YAML for the editor
271
+ let policyYaml = '';
272
+ try {
273
+ policyYaml = fs.readFileSync(gateway.getPolicyPackPath(), 'utf-8');
274
+ } catch {
275
+ // If file can't be read, editor will be empty
276
+ }
277
+
278
+ res.send(policiesTemplate(policy, validationResult, csrfToken, policyYaml));
279
+ });
280
+
281
+ // ---------- GET /admin/budgets -- Budget overview ----------
282
+ router.get('/budgets', (req, res) => {
283
+ try {
284
+ const budgetManager = gateway.getBudgetManager();
285
+ const budgetConfig = budgetManager.getConfig();
286
+ const spending = budgetManager.getSpendingSummary();
287
+ const budgets: BudgetInfo[] = [];
288
+
289
+ // Show configured budget limits with actual spending
290
+ budgets.push({
291
+ key: 'task_default',
292
+ type: 'task',
293
+ label: 'Task Budget (default)',
294
+ spent: spending.task_total,
295
+ limit: budgetConfig.task_budget_usd,
296
+ });
297
+
298
+ if (budgetConfig.user_daily_budget_usd !== undefined) {
299
+ budgets.push({
300
+ key: 'user_daily',
301
+ type: 'user_daily',
302
+ label: 'User Daily Budget',
303
+ spent: spending.user_daily_total,
304
+ limit: budgetConfig.user_daily_budget_usd,
305
+ });
306
+ }
307
+
308
+ if (budgetConfig.user_monthly_budget_usd !== undefined) {
309
+ budgets.push({
310
+ key: 'user_monthly',
311
+ type: 'user_monthly',
312
+ label: 'User Monthly Budget',
313
+ spent: spending.user_monthly_total,
314
+ limit: budgetConfig.user_monthly_budget_usd,
315
+ });
316
+ }
317
+
318
+ if (budgetConfig.workspace_daily_budget_usd !== undefined) {
319
+ budgets.push({
320
+ key: 'workspace_daily',
321
+ type: 'workspace_daily',
322
+ label: 'Workspace Daily Budget',
323
+ spent: spending.workspace_daily_total,
324
+ limit: budgetConfig.workspace_daily_budget_usd,
325
+ });
326
+ }
327
+
328
+ if (budgetConfig.workspace_monthly_budget_usd !== undefined) {
329
+ budgets.push({
330
+ key: 'workspace_monthly',
331
+ type: 'workspace_monthly',
332
+ label: 'Workspace Monthly Budget',
333
+ spent: spending.workspace_monthly_total,
334
+ limit: budgetConfig.workspace_monthly_budget_usd,
335
+ });
336
+ }
337
+
338
+ // Build cost summary from audit events
339
+ const auditLogger = gateway.getAuditLogger();
340
+ const allEvents = auditLogger.getAllEvents();
341
+ let estimatedTotal = 0;
342
+ let actualTotal = 0;
343
+ const toolCosts = new Map<string, number>();
344
+
345
+ for (const event of allEvents) {
346
+ if (event.event_type === 'BUDGET_CHECKED' && event.metadata) {
347
+ const estimated = typeof event.metadata.estimated_cost_usd === 'number' ? event.metadata.estimated_cost_usd : 0;
348
+ estimatedTotal += estimated;
349
+ }
350
+ if (event.event_type === 'USAGE_REPORTED' && event.metadata) {
351
+ const actual = typeof event.metadata.actual_cost_usd === 'number' ? event.metadata.actual_cost_usd : 0;
352
+ actualTotal += actual;
353
+ }
354
+ if (event.event_type === 'TOOL_EXECUTED') {
355
+ const toolName = event.tool_name || 'unknown';
356
+ toolCosts.set(toolName, (toolCosts.get(toolName) || 0) + 1);
357
+ }
358
+ }
359
+
360
+ const topTools = Array.from(toolCosts.entries())
361
+ .map(([tool_name, count]) => ({ tool_name, cost: count * 0.001 })) // approximate from count
362
+ .sort((a, b) => b.cost - a.cost)
363
+ .slice(0, 5);
364
+
365
+ const costSummary: CostSummary = {
366
+ estimated_total: estimatedTotal || spending.task_total,
367
+ actual_total: actualTotal,
368
+ top_tools: topTools,
369
+ };
370
+
371
+ res.send(budgetsTemplate(budgets, costSummary));
372
+ } catch (err) {
373
+ const message = err instanceof Error ? err.message : 'Unknown error';
374
+ res.status(500).send(layoutTemplate('Error', 'budgets',
375
+ `<div class="alert alert-error">Failed to load budgets: ${escapeHtml(message)}</div>`));
376
+ }
377
+ });
378
+
379
+ // ---------- GET /admin/traces -- Trace explorer ----------
380
+ router.get('/traces', (req, res) => {
381
+ try {
382
+ const taskIdQuery = typeof req.query.task_id === 'string' ? req.query.task_id : undefined;
383
+
384
+ // If a task_id is provided via query, redirect to detail page
385
+ if (taskIdQuery) {
386
+ res.redirect(`/admin/traces/${taskIdQuery}`);
387
+ return;
388
+ }
389
+
390
+ const page = Math.max(1, parseInt(req.query.page as string) || 1);
391
+ const limit = Math.min(Math.max(1, parseInt(req.query.limit as string) || 50), 200);
392
+
393
+ // Build a summary of recent task IDs from audit events
394
+ const auditLogger = gateway.getAuditLogger();
395
+ const allEvents = auditLogger.getAllEvents();
396
+
397
+ const taskMap = new Map<string, { event_count: number; first_seen: string }>();
398
+ for (const event of allEvents) {
399
+ const existing = taskMap.get(event.task_id);
400
+ if (existing) {
401
+ existing.event_count++;
402
+ } else {
403
+ taskMap.set(event.task_id, { event_count: 1, first_seen: event.timestamp });
404
+ }
405
+ }
406
+
407
+ const allTraces = Array.from(taskMap.entries())
408
+ .map(([task_id, info]) => ({ task_id, ...info }))
409
+ .reverse();
410
+
411
+ const total = allTraces.length;
412
+ const totalPages = Math.max(1, Math.ceil(total / limit));
413
+ const offset = (page - 1) * limit;
414
+ const traces = allTraces.slice(offset, offset + limit);
415
+
416
+ res.send(traceListTemplate(traces, { page, limit, total, totalPages }));
417
+ } catch (err) {
418
+ const message = err instanceof Error ? err.message : 'Unknown error';
419
+ res.status(500).send(layoutTemplate('Error', 'traces',
420
+ `<div class="alert alert-error">Failed to load traces: ${escapeHtml(message)}</div>`));
421
+ }
422
+ });
423
+
424
+ // ---------- GET /admin/traces/:task_id -- Trace detail ----------
425
+ router.get('/traces/:task_id', (req, res) => {
426
+ try {
427
+ const taskId = req.params.task_id;
428
+ const events = gateway.getTaskTrace(taskId);
429
+ res.send(traceDetailTemplate(taskId, events));
430
+ } catch (err) {
431
+ const message = err instanceof Error ? err.message : 'Unknown error';
432
+ res.status(500).send(layoutTemplate('Error', 'traces',
433
+ `<div class="alert alert-error">Failed to load trace detail: ${escapeHtml(message)}</div>`));
434
+ }
435
+ });
436
+
437
+ // ---------- GET /admin/api-keys -- API key management ----------
438
+ router.get('/api-keys', (req, res) => {
439
+ try {
440
+ const keys = getApiKeysFromConfig(config);
441
+ const csrfToken = generateCSRFToken();
442
+ const createdKey = typeof req.query.created === 'string' ? req.query.created : undefined;
443
+ res.send(apiKeysTemplate(keys, csrfToken, createdKey));
444
+ } catch (err) {
445
+ const message = err instanceof Error ? err.message : 'Unknown error';
446
+ res.status(500).send(layoutTemplate('Error', 'api-keys',
447
+ `<div class="alert alert-error">Failed to load API keys: ${escapeHtml(message)}</div>`));
448
+ }
449
+ });
450
+
451
+ // ---------- POST /admin/api-keys -- Create new API key ----------
452
+ router.post('/api-keys', (req, res) => {
453
+ if (!validateCSRFToken(req.body._csrf)) {
454
+ res.status(403).send('Invalid or missing CSRF token');
455
+ return;
456
+ }
457
+
458
+ const workspaceId = req.body.workspace_id || 'ws_default';
459
+ const description = req.body.description || '';
460
+
461
+ const newKey = `key-${randomUUID().replace(/-/g, '').substring(0, 16)}`;
462
+
463
+ config.auth.api_keys[newKey] = {
464
+ workspace_id: workspaceId,
465
+ description: description,
466
+ created_at: new Date().toISOString(),
467
+ };
468
+
469
+ res.redirect('/admin/api-keys?created=' + encodeURIComponent(newKey));
470
+ });
471
+
472
+ // ---------- POST /admin/api-keys/:key/revoke -- Revoke an API key ----------
473
+ router.post('/api-keys/:key/revoke', (req, res) => {
474
+ if (!validateCSRFToken(req.body._csrf)) {
475
+ res.status(403).send('Invalid or missing CSRF token');
476
+ return;
477
+ }
478
+
479
+ const key = req.params.key;
480
+
481
+ if (config.auth.api_keys[key]) {
482
+ config.auth.api_keys[key].revoked = true;
483
+ }
484
+
485
+ res.redirect('/admin/api-keys');
486
+ });
487
+
488
+ return router;
489
+ }
490
+
491
+ /** Extract API key info from the gateway config */
492
+ function getApiKeysFromConfig(config: GatewayConfig): ApiKeyInfo[] {
493
+ const keys: ApiKeyInfo[] = [];
494
+
495
+ for (const [key, keyConfig] of Object.entries(config.auth.api_keys)) {
496
+ keys.push({
497
+ key,
498
+ workspace_id: keyConfig.workspace_id,
499
+ description: keyConfig.description,
500
+ roles: keyConfig.roles,
501
+ created_at: keyConfig.created_at,
502
+ last_used_at: keyConfig.last_used_at,
503
+ revoked: keyConfig.revoked,
504
+ expires_at: keyConfig.expires_at,
505
+ });
506
+ }
507
+
508
+ return keys;
509
+ }