palaryn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (607) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +716 -0
  3. package/dist/sdk/typescript/src/client.d.ts +71 -0
  4. package/dist/sdk/typescript/src/client.d.ts.map +1 -0
  5. package/dist/sdk/typescript/src/client.js +176 -0
  6. package/dist/sdk/typescript/src/client.js.map +1 -0
  7. package/dist/sdk/typescript/src/errors.d.ts +50 -0
  8. package/dist/sdk/typescript/src/errors.d.ts.map +1 -0
  9. package/dist/sdk/typescript/src/errors.js +103 -0
  10. package/dist/sdk/typescript/src/errors.js.map +1 -0
  11. package/dist/sdk/typescript/src/index.d.ts +4 -0
  12. package/dist/sdk/typescript/src/index.d.ts.map +1 -0
  13. package/dist/sdk/typescript/src/index.js +15 -0
  14. package/dist/sdk/typescript/src/index.js.map +1 -0
  15. package/dist/sdk/typescript/src/types.d.ts +101 -0
  16. package/dist/sdk/typescript/src/types.d.ts.map +1 -0
  17. package/dist/sdk/typescript/src/types.js +6 -0
  18. package/dist/sdk/typescript/src/types.js.map +1 -0
  19. package/dist/src/admin/index.d.ts +2 -0
  20. package/dist/src/admin/index.d.ts.map +1 -0
  21. package/dist/src/admin/index.js +6 -0
  22. package/dist/src/admin/index.js.map +1 -0
  23. package/dist/src/admin/routes.d.ts +5 -0
  24. package/dist/src/admin/routes.d.ts.map +1 -0
  25. package/dist/src/admin/routes.js +471 -0
  26. package/dist/src/admin/routes.js.map +1 -0
  27. package/dist/src/admin/templates.d.ts +51 -0
  28. package/dist/src/admin/templates.d.ts.map +1 -0
  29. package/dist/src/admin/templates.js +500 -0
  30. package/dist/src/admin/templates.js.map +1 -0
  31. package/dist/src/anomaly/detector.d.ts +141 -0
  32. package/dist/src/anomaly/detector.d.ts.map +1 -0
  33. package/dist/src/anomaly/detector.js +554 -0
  34. package/dist/src/anomaly/detector.js.map +1 -0
  35. package/dist/src/anomaly/index.d.ts +2 -0
  36. package/dist/src/anomaly/index.d.ts.map +1 -0
  37. package/dist/src/anomaly/index.js +7 -0
  38. package/dist/src/anomaly/index.js.map +1 -0
  39. package/dist/src/approval/manager.d.ts +147 -0
  40. package/dist/src/approval/manager.d.ts.map +1 -0
  41. package/dist/src/approval/manager.js +511 -0
  42. package/dist/src/approval/manager.js.map +1 -0
  43. package/dist/src/approval/webhook.d.ts +36 -0
  44. package/dist/src/approval/webhook.d.ts.map +1 -0
  45. package/dist/src/approval/webhook.js +135 -0
  46. package/dist/src/approval/webhook.js.map +1 -0
  47. package/dist/src/audit/logger.d.ts +70 -0
  48. package/dist/src/audit/logger.d.ts.map +1 -0
  49. package/dist/src/audit/logger.js +440 -0
  50. package/dist/src/audit/logger.js.map +1 -0
  51. package/dist/src/auth/index.d.ts +6 -0
  52. package/dist/src/auth/index.d.ts.map +1 -0
  53. package/dist/src/auth/index.js +22 -0
  54. package/dist/src/auth/index.js.map +1 -0
  55. package/dist/src/auth/password.d.ts +3 -0
  56. package/dist/src/auth/password.d.ts.map +1 -0
  57. package/dist/src/auth/password.js +25 -0
  58. package/dist/src/auth/password.js.map +1 -0
  59. package/dist/src/auth/pkce.d.ts +13 -0
  60. package/dist/src/auth/pkce.d.ts.map +1 -0
  61. package/dist/src/auth/pkce.js +58 -0
  62. package/dist/src/auth/pkce.js.map +1 -0
  63. package/dist/src/auth/providers.d.ts +28 -0
  64. package/dist/src/auth/providers.d.ts.map +1 -0
  65. package/dist/src/auth/providers.js +198 -0
  66. package/dist/src/auth/providers.js.map +1 -0
  67. package/dist/src/auth/routes.d.ts +14 -0
  68. package/dist/src/auth/routes.d.ts.map +1 -0
  69. package/dist/src/auth/routes.js +431 -0
  70. package/dist/src/auth/routes.js.map +1 -0
  71. package/dist/src/auth/session.d.ts +24 -0
  72. package/dist/src/auth/session.d.ts.map +1 -0
  73. package/dist/src/auth/session.js +105 -0
  74. package/dist/src/auth/session.js.map +1 -0
  75. package/dist/src/billing/index.d.ts +7 -0
  76. package/dist/src/billing/index.d.ts.map +1 -0
  77. package/dist/src/billing/index.js +14 -0
  78. package/dist/src/billing/index.js.map +1 -0
  79. package/dist/src/billing/plan-enforcer.d.ts +44 -0
  80. package/dist/src/billing/plan-enforcer.d.ts.map +1 -0
  81. package/dist/src/billing/plan-enforcer.js +110 -0
  82. package/dist/src/billing/plan-enforcer.js.map +1 -0
  83. package/dist/src/billing/routes.d.ts +15 -0
  84. package/dist/src/billing/routes.d.ts.map +1 -0
  85. package/dist/src/billing/routes.js +193 -0
  86. package/dist/src/billing/routes.js.map +1 -0
  87. package/dist/src/billing/stripe-client.d.ts +14 -0
  88. package/dist/src/billing/stripe-client.d.ts.map +1 -0
  89. package/dist/src/billing/stripe-client.js +51 -0
  90. package/dist/src/billing/stripe-client.js.map +1 -0
  91. package/dist/src/billing/webhook-handler.d.ts +19 -0
  92. package/dist/src/billing/webhook-handler.d.ts.map +1 -0
  93. package/dist/src/billing/webhook-handler.js +169 -0
  94. package/dist/src/billing/webhook-handler.js.map +1 -0
  95. package/dist/src/billing/webhook-routes.d.ts +5 -0
  96. package/dist/src/billing/webhook-routes.d.ts.map +1 -0
  97. package/dist/src/billing/webhook-routes.js +30 -0
  98. package/dist/src/billing/webhook-routes.js.map +1 -0
  99. package/dist/src/budget/manager.d.ts +95 -0
  100. package/dist/src/budget/manager.d.ts.map +1 -0
  101. package/dist/src/budget/manager.js +547 -0
  102. package/dist/src/budget/manager.js.map +1 -0
  103. package/dist/src/budget/usage-extractor.d.ts +38 -0
  104. package/dist/src/budget/usage-extractor.d.ts.map +1 -0
  105. package/dist/src/budget/usage-extractor.js +165 -0
  106. package/dist/src/budget/usage-extractor.js.map +1 -0
  107. package/dist/src/cli.d.ts +3 -0
  108. package/dist/src/cli.d.ts.map +1 -0
  109. package/dist/src/cli.js +115 -0
  110. package/dist/src/cli.js.map +1 -0
  111. package/dist/src/config/defaults.d.ts +3 -0
  112. package/dist/src/config/defaults.d.ts.map +1 -0
  113. package/dist/src/config/defaults.js +243 -0
  114. package/dist/src/config/defaults.js.map +1 -0
  115. package/dist/src/config/validate.d.ts +15 -0
  116. package/dist/src/config/validate.d.ts.map +1 -0
  117. package/dist/src/config/validate.js +105 -0
  118. package/dist/src/config/validate.js.map +1 -0
  119. package/dist/src/dlp/composite-scanner.d.ts +47 -0
  120. package/dist/src/dlp/composite-scanner.d.ts.map +1 -0
  121. package/dist/src/dlp/composite-scanner.js +186 -0
  122. package/dist/src/dlp/composite-scanner.js.map +1 -0
  123. package/dist/src/dlp/index.d.ts +10 -0
  124. package/dist/src/dlp/index.d.ts.map +1 -0
  125. package/dist/src/dlp/index.js +26 -0
  126. package/dist/src/dlp/index.js.map +1 -0
  127. package/dist/src/dlp/interfaces.d.ts +33 -0
  128. package/dist/src/dlp/interfaces.d.ts.map +1 -0
  129. package/dist/src/dlp/interfaces.js +3 -0
  130. package/dist/src/dlp/interfaces.js.map +1 -0
  131. package/dist/src/dlp/patterns.d.ts +9 -0
  132. package/dist/src/dlp/patterns.d.ts.map +1 -0
  133. package/dist/src/dlp/patterns.js +25 -0
  134. package/dist/src/dlp/patterns.js.map +1 -0
  135. package/dist/src/dlp/prompt-injection-backend.d.ts +68 -0
  136. package/dist/src/dlp/prompt-injection-backend.d.ts.map +1 -0
  137. package/dist/src/dlp/prompt-injection-backend.js +148 -0
  138. package/dist/src/dlp/prompt-injection-backend.js.map +1 -0
  139. package/dist/src/dlp/prompt-injection-patterns.d.ts +32 -0
  140. package/dist/src/dlp/prompt-injection-patterns.d.ts.map +1 -0
  141. package/dist/src/dlp/prompt-injection-patterns.js +290 -0
  142. package/dist/src/dlp/prompt-injection-patterns.js.map +1 -0
  143. package/dist/src/dlp/regex-backend.d.ts +32 -0
  144. package/dist/src/dlp/regex-backend.d.ts.map +1 -0
  145. package/dist/src/dlp/regex-backend.js +153 -0
  146. package/dist/src/dlp/regex-backend.js.map +1 -0
  147. package/dist/src/dlp/scanner.d.ts +122 -0
  148. package/dist/src/dlp/scanner.d.ts.map +1 -0
  149. package/dist/src/dlp/scanner.js +444 -0
  150. package/dist/src/dlp/scanner.js.map +1 -0
  151. package/dist/src/dlp/text-normalizer.d.ts +41 -0
  152. package/dist/src/dlp/text-normalizer.d.ts.map +1 -0
  153. package/dist/src/dlp/text-normalizer.js +203 -0
  154. package/dist/src/dlp/text-normalizer.js.map +1 -0
  155. package/dist/src/dlp/trufflehog-backend.d.ts +64 -0
  156. package/dist/src/dlp/trufflehog-backend.d.ts.map +1 -0
  157. package/dist/src/dlp/trufflehog-backend.js +151 -0
  158. package/dist/src/dlp/trufflehog-backend.js.map +1 -0
  159. package/dist/src/executor/http-executor.d.ts +25 -0
  160. package/dist/src/executor/http-executor.d.ts.map +1 -0
  161. package/dist/src/executor/http-executor.js +333 -0
  162. package/dist/src/executor/http-executor.js.map +1 -0
  163. package/dist/src/executor/index.d.ts +6 -0
  164. package/dist/src/executor/index.d.ts.map +1 -0
  165. package/dist/src/executor/index.js +12 -0
  166. package/dist/src/executor/index.js.map +1 -0
  167. package/dist/src/executor/interfaces.d.ts +11 -0
  168. package/dist/src/executor/interfaces.d.ts.map +1 -0
  169. package/dist/src/executor/interfaces.js +3 -0
  170. package/dist/src/executor/interfaces.js.map +1 -0
  171. package/dist/src/executor/noop-executor.d.ts +13 -0
  172. package/dist/src/executor/noop-executor.d.ts.map +1 -0
  173. package/dist/src/executor/noop-executor.js +21 -0
  174. package/dist/src/executor/noop-executor.js.map +1 -0
  175. package/dist/src/executor/registry.d.ts +30 -0
  176. package/dist/src/executor/registry.d.ts.map +1 -0
  177. package/dist/src/executor/registry.js +62 -0
  178. package/dist/src/executor/registry.js.map +1 -0
  179. package/dist/src/executor/slack-executor.d.ts +24 -0
  180. package/dist/src/executor/slack-executor.d.ts.map +1 -0
  181. package/dist/src/executor/slack-executor.js +147 -0
  182. package/dist/src/executor/slack-executor.js.map +1 -0
  183. package/dist/src/index.d.ts +25 -0
  184. package/dist/src/index.d.ts.map +1 -0
  185. package/dist/src/index.js +74 -0
  186. package/dist/src/index.js.map +1 -0
  187. package/dist/src/mcp/auth-verifier.d.ts +23 -0
  188. package/dist/src/mcp/auth-verifier.d.ts.map +1 -0
  189. package/dist/src/mcp/auth-verifier.js +162 -0
  190. package/dist/src/mcp/auth-verifier.js.map +1 -0
  191. package/dist/src/mcp/bridge.d.ts +132 -0
  192. package/dist/src/mcp/bridge.d.ts.map +1 -0
  193. package/dist/src/mcp/bridge.js +734 -0
  194. package/dist/src/mcp/bridge.js.map +1 -0
  195. package/dist/src/mcp/http-transport.d.ts +32 -0
  196. package/dist/src/mcp/http-transport.d.ts.map +1 -0
  197. package/dist/src/mcp/http-transport.js +538 -0
  198. package/dist/src/mcp/http-transport.js.map +1 -0
  199. package/dist/src/mcp/index.d.ts +10 -0
  200. package/dist/src/mcp/index.d.ts.map +1 -0
  201. package/dist/src/mcp/index.js +17 -0
  202. package/dist/src/mcp/index.js.map +1 -0
  203. package/dist/src/mcp/oauth-pages.d.ts +23 -0
  204. package/dist/src/mcp/oauth-pages.d.ts.map +1 -0
  205. package/dist/src/mcp/oauth-pages.js +121 -0
  206. package/dist/src/mcp/oauth-pages.js.map +1 -0
  207. package/dist/src/mcp/oauth-postgres-stores.d.ts +55 -0
  208. package/dist/src/mcp/oauth-postgres-stores.d.ts.map +1 -0
  209. package/dist/src/mcp/oauth-postgres-stores.js +226 -0
  210. package/dist/src/mcp/oauth-postgres-stores.js.map +1 -0
  211. package/dist/src/mcp/oauth-provider.d.ts +95 -0
  212. package/dist/src/mcp/oauth-provider.d.ts.map +1 -0
  213. package/dist/src/mcp/oauth-provider.js +360 -0
  214. package/dist/src/mcp/oauth-provider.js.map +1 -0
  215. package/dist/src/mcp/oauth-stores.d.ts +62 -0
  216. package/dist/src/mcp/oauth-stores.d.ts.map +1 -0
  217. package/dist/src/mcp/oauth-stores.js +154 -0
  218. package/dist/src/mcp/oauth-stores.js.map +1 -0
  219. package/dist/src/mcp/server.d.ts +18 -0
  220. package/dist/src/mcp/server.d.ts.map +1 -0
  221. package/dist/src/mcp/server.js +51 -0
  222. package/dist/src/mcp/server.js.map +1 -0
  223. package/dist/src/metrics/collector.d.ts +106 -0
  224. package/dist/src/metrics/collector.d.ts.map +1 -0
  225. package/dist/src/metrics/collector.js +311 -0
  226. package/dist/src/metrics/collector.js.map +1 -0
  227. package/dist/src/metrics/index.d.ts +2 -0
  228. package/dist/src/metrics/index.d.ts.map +1 -0
  229. package/dist/src/metrics/index.js +6 -0
  230. package/dist/src/metrics/index.js.map +1 -0
  231. package/dist/src/middleware/auth.d.ts +77 -0
  232. package/dist/src/middleware/auth.d.ts.map +1 -0
  233. package/dist/src/middleware/auth.js +720 -0
  234. package/dist/src/middleware/auth.js.map +1 -0
  235. package/dist/src/middleware/session.d.ts +18 -0
  236. package/dist/src/middleware/session.d.ts.map +1 -0
  237. package/dist/src/middleware/session.js +67 -0
  238. package/dist/src/middleware/session.js.map +1 -0
  239. package/dist/src/middleware/validate.d.ts +3 -0
  240. package/dist/src/middleware/validate.d.ts.map +1 -0
  241. package/dist/src/middleware/validate.js +85 -0
  242. package/dist/src/middleware/validate.js.map +1 -0
  243. package/dist/src/policy/engine.d.ts +107 -0
  244. package/dist/src/policy/engine.d.ts.map +1 -0
  245. package/dist/src/policy/engine.js +646 -0
  246. package/dist/src/policy/engine.js.map +1 -0
  247. package/dist/src/policy/index.d.ts +3 -0
  248. package/dist/src/policy/index.d.ts.map +1 -0
  249. package/dist/src/policy/index.js +8 -0
  250. package/dist/src/policy/index.js.map +1 -0
  251. package/dist/src/policy/opa-engine.d.ts +176 -0
  252. package/dist/src/policy/opa-engine.d.ts.map +1 -0
  253. package/dist/src/policy/opa-engine.js +790 -0
  254. package/dist/src/policy/opa-engine.js.map +1 -0
  255. package/dist/src/proxy/forward-proxy.d.ts +30 -0
  256. package/dist/src/proxy/forward-proxy.d.ts.map +1 -0
  257. package/dist/src/proxy/forward-proxy.js +580 -0
  258. package/dist/src/proxy/forward-proxy.js.map +1 -0
  259. package/dist/src/proxy/index.d.ts +2 -0
  260. package/dist/src/proxy/index.d.ts.map +1 -0
  261. package/dist/src/proxy/index.js +8 -0
  262. package/dist/src/proxy/index.js.map +1 -0
  263. package/dist/src/ratelimit/limiter.d.ts +45 -0
  264. package/dist/src/ratelimit/limiter.d.ts.map +1 -0
  265. package/dist/src/ratelimit/limiter.js +158 -0
  266. package/dist/src/ratelimit/limiter.js.map +1 -0
  267. package/dist/src/replay/engine.d.ts +40 -0
  268. package/dist/src/replay/engine.d.ts.map +1 -0
  269. package/dist/src/replay/engine.js +106 -0
  270. package/dist/src/replay/engine.js.map +1 -0
  271. package/dist/src/replay/index.d.ts +2 -0
  272. package/dist/src/replay/index.d.ts.map +1 -0
  273. package/dist/src/replay/index.js +6 -0
  274. package/dist/src/replay/index.js.map +1 -0
  275. package/dist/src/saas/index.d.ts +2 -0
  276. package/dist/src/saas/index.d.ts.map +1 -0
  277. package/dist/src/saas/index.js +18 -0
  278. package/dist/src/saas/index.js.map +1 -0
  279. package/dist/src/saas/routes.d.ts +18 -0
  280. package/dist/src/saas/routes.d.ts.map +1 -0
  281. package/dist/src/saas/routes.js +1566 -0
  282. package/dist/src/saas/routes.js.map +1 -0
  283. package/dist/src/server/app.d.ts +44 -0
  284. package/dist/src/server/app.d.ts.map +1 -0
  285. package/dist/src/server/app.js +854 -0
  286. package/dist/src/server/app.js.map +1 -0
  287. package/dist/src/server/errors.d.ts +32 -0
  288. package/dist/src/server/errors.d.ts.map +1 -0
  289. package/dist/src/server/errors.js +39 -0
  290. package/dist/src/server/errors.js.map +1 -0
  291. package/dist/src/server/gateway.d.ts +165 -0
  292. package/dist/src/server/gateway.d.ts.map +1 -0
  293. package/dist/src/server/gateway.js +964 -0
  294. package/dist/src/server/gateway.js.map +1 -0
  295. package/dist/src/server/index.d.ts +2 -0
  296. package/dist/src/server/index.d.ts.map +1 -0
  297. package/dist/src/server/index.js +295 -0
  298. package/dist/src/server/index.js.map +1 -0
  299. package/dist/src/server/logger.d.ts +33 -0
  300. package/dist/src/server/logger.d.ts.map +1 -0
  301. package/dist/src/server/logger.js +230 -0
  302. package/dist/src/server/logger.js.map +1 -0
  303. package/dist/src/server/stream-proxy.d.ts +32 -0
  304. package/dist/src/server/stream-proxy.d.ts.map +1 -0
  305. package/dist/src/server/stream-proxy.js +184 -0
  306. package/dist/src/server/stream-proxy.js.map +1 -0
  307. package/dist/src/storage/file-persistence.d.ts +48 -0
  308. package/dist/src/storage/file-persistence.d.ts.map +1 -0
  309. package/dist/src/storage/file-persistence.js +280 -0
  310. package/dist/src/storage/file-persistence.js.map +1 -0
  311. package/dist/src/storage/index.d.ts +5 -0
  312. package/dist/src/storage/index.d.ts.map +1 -0
  313. package/dist/src/storage/index.js +21 -0
  314. package/dist/src/storage/index.js.map +1 -0
  315. package/dist/src/storage/interfaces.d.ts +237 -0
  316. package/dist/src/storage/interfaces.d.ts.map +1 -0
  317. package/dist/src/storage/interfaces.js +3 -0
  318. package/dist/src/storage/interfaces.js.map +1 -0
  319. package/dist/src/storage/memory.d.ts +162 -0
  320. package/dist/src/storage/memory.d.ts.map +1 -0
  321. package/dist/src/storage/memory.js +603 -0
  322. package/dist/src/storage/memory.js.map +1 -0
  323. package/dist/src/storage/postgres.d.ts +267 -0
  324. package/dist/src/storage/postgres.d.ts.map +1 -0
  325. package/dist/src/storage/postgres.js +1555 -0
  326. package/dist/src/storage/postgres.js.map +1 -0
  327. package/dist/src/storage/redis.d.ts +202 -0
  328. package/dist/src/storage/redis.d.ts.map +1 -0
  329. package/dist/src/storage/redis.js +629 -0
  330. package/dist/src/storage/redis.js.map +1 -0
  331. package/dist/src/tracing/index.d.ts +2 -0
  332. package/dist/src/tracing/index.d.ts.map +1 -0
  333. package/dist/src/tracing/index.js +6 -0
  334. package/dist/src/tracing/index.js.map +1 -0
  335. package/dist/src/tracing/provider.d.ts +43 -0
  336. package/dist/src/tracing/provider.d.ts.map +1 -0
  337. package/dist/src/tracing/provider.js +74 -0
  338. package/dist/src/tracing/provider.js.map +1 -0
  339. package/dist/src/trust/calculator.d.ts +54 -0
  340. package/dist/src/trust/calculator.d.ts.map +1 -0
  341. package/dist/src/trust/calculator.js +102 -0
  342. package/dist/src/trust/calculator.js.map +1 -0
  343. package/dist/src/trust/index.d.ts +2 -0
  344. package/dist/src/trust/index.d.ts.map +1 -0
  345. package/dist/src/trust/index.js +7 -0
  346. package/dist/src/trust/index.js.map +1 -0
  347. package/dist/src/types/budget.d.ts +30 -0
  348. package/dist/src/types/budget.d.ts.map +1 -0
  349. package/dist/src/types/budget.js +3 -0
  350. package/dist/src/types/budget.js.map +1 -0
  351. package/dist/src/types/config.d.ts +176 -0
  352. package/dist/src/types/config.d.ts.map +1 -0
  353. package/dist/src/types/config.js +3 -0
  354. package/dist/src/types/config.js.map +1 -0
  355. package/dist/src/types/events.d.ts +24 -0
  356. package/dist/src/types/events.d.ts.map +1 -0
  357. package/dist/src/types/events.js +3 -0
  358. package/dist/src/types/events.js.map +1 -0
  359. package/dist/src/types/index.d.ts +8 -0
  360. package/dist/src/types/index.d.ts.map +1 -0
  361. package/dist/src/types/index.js +24 -0
  362. package/dist/src/types/index.js.map +1 -0
  363. package/dist/src/types/policy.d.ts +60 -0
  364. package/dist/src/types/policy.d.ts.map +1 -0
  365. package/dist/src/types/policy.js +3 -0
  366. package/dist/src/types/policy.js.map +1 -0
  367. package/dist/src/types/stripe-config.d.ts +12 -0
  368. package/dist/src/types/stripe-config.d.ts.map +1 -0
  369. package/dist/src/types/stripe-config.js +3 -0
  370. package/dist/src/types/stripe-config.js.map +1 -0
  371. package/dist/src/types/subscription.d.ts +24 -0
  372. package/dist/src/types/subscription.d.ts.map +1 -0
  373. package/dist/src/types/subscription.js +38 -0
  374. package/dist/src/types/subscription.js.map +1 -0
  375. package/dist/src/types/tool-call.d.ts +42 -0
  376. package/dist/src/types/tool-call.d.ts.map +1 -0
  377. package/dist/src/types/tool-call.js +3 -0
  378. package/dist/src/types/tool-call.js.map +1 -0
  379. package/dist/src/types/tool-result.d.ts +58 -0
  380. package/dist/src/types/tool-result.d.ts.map +1 -0
  381. package/dist/src/types/tool-result.js +3 -0
  382. package/dist/src/types/tool-result.js.map +1 -0
  383. package/dist/src/types/user.d.ts +101 -0
  384. package/dist/src/types/user.d.ts.map +1 -0
  385. package/dist/src/types/user.js +6 -0
  386. package/dist/src/types/user.js.map +1 -0
  387. package/dist/tests/integration/api.test.d.ts +2 -0
  388. package/dist/tests/integration/api.test.d.ts.map +1 -0
  389. package/dist/tests/integration/api.test.js +1199 -0
  390. package/dist/tests/integration/api.test.js.map +1 -0
  391. package/dist/tests/integration/proxy.test.d.ts +2 -0
  392. package/dist/tests/integration/proxy.test.d.ts.map +1 -0
  393. package/dist/tests/integration/proxy.test.js +251 -0
  394. package/dist/tests/integration/proxy.test.js.map +1 -0
  395. package/dist/tests/integration/storage.test.d.ts +16 -0
  396. package/dist/tests/integration/storage.test.d.ts.map +1 -0
  397. package/dist/tests/integration/storage.test.js +826 -0
  398. package/dist/tests/integration/storage.test.js.map +1 -0
  399. package/dist/tests/unit/admin.test.d.ts +2 -0
  400. package/dist/tests/unit/admin.test.d.ts.map +1 -0
  401. package/dist/tests/unit/admin.test.js +698 -0
  402. package/dist/tests/unit/admin.test.js.map +1 -0
  403. package/dist/tests/unit/anomaly-detector.test.d.ts +2 -0
  404. package/dist/tests/unit/anomaly-detector.test.d.ts.map +1 -0
  405. package/dist/tests/unit/anomaly-detector.test.js +903 -0
  406. package/dist/tests/unit/anomaly-detector.test.js.map +1 -0
  407. package/dist/tests/unit/approval-manager.test.d.ts +2 -0
  408. package/dist/tests/unit/approval-manager.test.d.ts.map +1 -0
  409. package/dist/tests/unit/approval-manager.test.js +528 -0
  410. package/dist/tests/unit/approval-manager.test.js.map +1 -0
  411. package/dist/tests/unit/approval-webhook.test.d.ts +2 -0
  412. package/dist/tests/unit/approval-webhook.test.d.ts.map +1 -0
  413. package/dist/tests/unit/approval-webhook.test.js +355 -0
  414. package/dist/tests/unit/approval-webhook.test.js.map +1 -0
  415. package/dist/tests/unit/audit-logger.test.d.ts +2 -0
  416. package/dist/tests/unit/audit-logger.test.d.ts.map +1 -0
  417. package/dist/tests/unit/audit-logger.test.js +635 -0
  418. package/dist/tests/unit/audit-logger.test.js.map +1 -0
  419. package/dist/tests/unit/auth-routes.test.d.ts +2 -0
  420. package/dist/tests/unit/auth-routes.test.d.ts.map +1 -0
  421. package/dist/tests/unit/auth-routes.test.js +281 -0
  422. package/dist/tests/unit/auth-routes.test.js.map +1 -0
  423. package/dist/tests/unit/auth.test.d.ts +2 -0
  424. package/dist/tests/unit/auth.test.d.ts.map +1 -0
  425. package/dist/tests/unit/auth.test.js +1382 -0
  426. package/dist/tests/unit/auth.test.js.map +1 -0
  427. package/dist/tests/unit/billing.test.d.ts +2 -0
  428. package/dist/tests/unit/billing.test.d.ts.map +1 -0
  429. package/dist/tests/unit/billing.test.js +579 -0
  430. package/dist/tests/unit/billing.test.js.map +1 -0
  431. package/dist/tests/unit/budget-manager.test.d.ts +2 -0
  432. package/dist/tests/unit/budget-manager.test.d.ts.map +1 -0
  433. package/dist/tests/unit/budget-manager.test.js +778 -0
  434. package/dist/tests/unit/budget-manager.test.js.map +1 -0
  435. package/dist/tests/unit/budget-race.test.d.ts +2 -0
  436. package/dist/tests/unit/budget-race.test.d.ts.map +1 -0
  437. package/dist/tests/unit/budget-race.test.js +58 -0
  438. package/dist/tests/unit/budget-race.test.js.map +1 -0
  439. package/dist/tests/unit/cli.test.d.ts +2 -0
  440. package/dist/tests/unit/cli.test.d.ts.map +1 -0
  441. package/dist/tests/unit/cli.test.js +93 -0
  442. package/dist/tests/unit/cli.test.js.map +1 -0
  443. package/dist/tests/unit/concurrency.test.d.ts +2 -0
  444. package/dist/tests/unit/concurrency.test.d.ts.map +1 -0
  445. package/dist/tests/unit/concurrency.test.js +1270 -0
  446. package/dist/tests/unit/concurrency.test.js.map +1 -0
  447. package/dist/tests/unit/config-validate.test.d.ts +2 -0
  448. package/dist/tests/unit/config-validate.test.d.ts.map +1 -0
  449. package/dist/tests/unit/config-validate.test.js +230 -0
  450. package/dist/tests/unit/config-validate.test.js.map +1 -0
  451. package/dist/tests/unit/defaults.test.d.ts +2 -0
  452. package/dist/tests/unit/defaults.test.d.ts.map +1 -0
  453. package/dist/tests/unit/defaults.test.js +364 -0
  454. package/dist/tests/unit/defaults.test.js.map +1 -0
  455. package/dist/tests/unit/dlp-backends.test.d.ts +2 -0
  456. package/dist/tests/unit/dlp-backends.test.d.ts.map +1 -0
  457. package/dist/tests/unit/dlp-backends.test.js +563 -0
  458. package/dist/tests/unit/dlp-backends.test.js.map +1 -0
  459. package/dist/tests/unit/dlp-scanner.test.d.ts +2 -0
  460. package/dist/tests/unit/dlp-scanner.test.d.ts.map +1 -0
  461. package/dist/tests/unit/dlp-scanner.test.js +739 -0
  462. package/dist/tests/unit/dlp-scanner.test.js.map +1 -0
  463. package/dist/tests/unit/error-responses.test.d.ts +2 -0
  464. package/dist/tests/unit/error-responses.test.d.ts.map +1 -0
  465. package/dist/tests/unit/error-responses.test.js +101 -0
  466. package/dist/tests/unit/error-responses.test.js.map +1 -0
  467. package/dist/tests/unit/executor-registry.test.d.ts +2 -0
  468. package/dist/tests/unit/executor-registry.test.d.ts.map +1 -0
  469. package/dist/tests/unit/executor-registry.test.js +390 -0
  470. package/dist/tests/unit/executor-registry.test.js.map +1 -0
  471. package/dist/tests/unit/forward-proxy.test.d.ts +2 -0
  472. package/dist/tests/unit/forward-proxy.test.d.ts.map +1 -0
  473. package/dist/tests/unit/forward-proxy.test.js +621 -0
  474. package/dist/tests/unit/forward-proxy.test.js.map +1 -0
  475. package/dist/tests/unit/gateway-features.test.d.ts +2 -0
  476. package/dist/tests/unit/gateway-features.test.d.ts.map +1 -0
  477. package/dist/tests/unit/gateway-features.test.js +753 -0
  478. package/dist/tests/unit/gateway-features.test.js.map +1 -0
  479. package/dist/tests/unit/http-executor.test.d.ts +2 -0
  480. package/dist/tests/unit/http-executor.test.d.ts.map +1 -0
  481. package/dist/tests/unit/http-executor.test.js +310 -0
  482. package/dist/tests/unit/http-executor.test.js.map +1 -0
  483. package/dist/tests/unit/mcp-bridge.test.d.ts +2 -0
  484. package/dist/tests/unit/mcp-bridge.test.d.ts.map +1 -0
  485. package/dist/tests/unit/mcp-bridge.test.js +1136 -0
  486. package/dist/tests/unit/mcp-bridge.test.js.map +1 -0
  487. package/dist/tests/unit/mcp-http-transport.test.d.ts +2 -0
  488. package/dist/tests/unit/mcp-http-transport.test.d.ts.map +1 -0
  489. package/dist/tests/unit/mcp-http-transport.test.js +899 -0
  490. package/dist/tests/unit/mcp-http-transport.test.js.map +1 -0
  491. package/dist/tests/unit/mcp-oauth.test.d.ts +2 -0
  492. package/dist/tests/unit/mcp-oauth.test.d.ts.map +1 -0
  493. package/dist/tests/unit/mcp-oauth.test.js +759 -0
  494. package/dist/tests/unit/mcp-oauth.test.js.map +1 -0
  495. package/dist/tests/unit/mcp-server.test.d.ts +15 -0
  496. package/dist/tests/unit/mcp-server.test.d.ts.map +1 -0
  497. package/dist/tests/unit/mcp-server.test.js +158 -0
  498. package/dist/tests/unit/mcp-server.test.js.map +1 -0
  499. package/dist/tests/unit/metrics.test.d.ts +2 -0
  500. package/dist/tests/unit/metrics.test.d.ts.map +1 -0
  501. package/dist/tests/unit/metrics.test.js +208 -0
  502. package/dist/tests/unit/metrics.test.js.map +1 -0
  503. package/dist/tests/unit/oauth.test.d.ts +2 -0
  504. package/dist/tests/unit/oauth.test.d.ts.map +1 -0
  505. package/dist/tests/unit/oauth.test.js +281 -0
  506. package/dist/tests/unit/oauth.test.js.map +1 -0
  507. package/dist/tests/unit/opa-circuit-breaker.test.d.ts +2 -0
  508. package/dist/tests/unit/opa-circuit-breaker.test.d.ts.map +1 -0
  509. package/dist/tests/unit/opa-circuit-breaker.test.js +297 -0
  510. package/dist/tests/unit/opa-circuit-breaker.test.js.map +1 -0
  511. package/dist/tests/unit/opa-engine.test.d.ts +2 -0
  512. package/dist/tests/unit/opa-engine.test.d.ts.map +1 -0
  513. package/dist/tests/unit/opa-engine.test.js +1813 -0
  514. package/dist/tests/unit/opa-engine.test.js.map +1 -0
  515. package/dist/tests/unit/pipeline-timing.test.d.ts +2 -0
  516. package/dist/tests/unit/pipeline-timing.test.d.ts.map +1 -0
  517. package/dist/tests/unit/pipeline-timing.test.js +528 -0
  518. package/dist/tests/unit/pipeline-timing.test.js.map +1 -0
  519. package/dist/tests/unit/policy-engine.test.d.ts +2 -0
  520. package/dist/tests/unit/policy-engine.test.d.ts.map +1 -0
  521. package/dist/tests/unit/policy-engine.test.js +1345 -0
  522. package/dist/tests/unit/policy-engine.test.js.map +1 -0
  523. package/dist/tests/unit/policy-store.test.d.ts +2 -0
  524. package/dist/tests/unit/policy-store.test.d.ts.map +1 -0
  525. package/dist/tests/unit/policy-store.test.js +60 -0
  526. package/dist/tests/unit/policy-store.test.js.map +1 -0
  527. package/dist/tests/unit/postgres-storage.test.d.ts +2 -0
  528. package/dist/tests/unit/postgres-storage.test.d.ts.map +1 -0
  529. package/dist/tests/unit/postgres-storage.test.js +614 -0
  530. package/dist/tests/unit/postgres-storage.test.js.map +1 -0
  531. package/dist/tests/unit/prompt-injection-backend.test.d.ts +2 -0
  532. package/dist/tests/unit/prompt-injection-backend.test.d.ts.map +1 -0
  533. package/dist/tests/unit/prompt-injection-backend.test.js +621 -0
  534. package/dist/tests/unit/prompt-injection-backend.test.js.map +1 -0
  535. package/dist/tests/unit/proxy-hardening.test.d.ts +2 -0
  536. package/dist/tests/unit/proxy-hardening.test.d.ts.map +1 -0
  537. package/dist/tests/unit/proxy-hardening.test.js +166 -0
  538. package/dist/tests/unit/proxy-hardening.test.js.map +1 -0
  539. package/dist/tests/unit/rate-limiter.test.d.ts +2 -0
  540. package/dist/tests/unit/rate-limiter.test.d.ts.map +1 -0
  541. package/dist/tests/unit/rate-limiter.test.js +443 -0
  542. package/dist/tests/unit/rate-limiter.test.js.map +1 -0
  543. package/dist/tests/unit/redis-storage.test.d.ts +2 -0
  544. package/dist/tests/unit/redis-storage.test.d.ts.map +1 -0
  545. package/dist/tests/unit/redis-storage.test.js +766 -0
  546. package/dist/tests/unit/redis-storage.test.js.map +1 -0
  547. package/dist/tests/unit/replay-engine.test.d.ts +2 -0
  548. package/dist/tests/unit/replay-engine.test.d.ts.map +1 -0
  549. package/dist/tests/unit/replay-engine.test.js +371 -0
  550. package/dist/tests/unit/replay-engine.test.js.map +1 -0
  551. package/dist/tests/unit/saas-routes.test.d.ts +2 -0
  552. package/dist/tests/unit/saas-routes.test.d.ts.map +1 -0
  553. package/dist/tests/unit/saas-routes.test.js +1399 -0
  554. package/dist/tests/unit/saas-routes.test.js.map +1 -0
  555. package/dist/tests/unit/session.test.d.ts +2 -0
  556. package/dist/tests/unit/session.test.d.ts.map +1 -0
  557. package/dist/tests/unit/session.test.js +532 -0
  558. package/dist/tests/unit/session.test.js.map +1 -0
  559. package/dist/tests/unit/slack-executor.test.d.ts +2 -0
  560. package/dist/tests/unit/slack-executor.test.d.ts.map +1 -0
  561. package/dist/tests/unit/slack-executor.test.js +209 -0
  562. package/dist/tests/unit/slack-executor.test.js.map +1 -0
  563. package/dist/tests/unit/storage-hardening.test.d.ts +2 -0
  564. package/dist/tests/unit/storage-hardening.test.d.ts.map +1 -0
  565. package/dist/tests/unit/storage-hardening.test.js +165 -0
  566. package/dist/tests/unit/storage-hardening.test.js.map +1 -0
  567. package/dist/tests/unit/storage.test.d.ts +2 -0
  568. package/dist/tests/unit/storage.test.d.ts.map +1 -0
  569. package/dist/tests/unit/storage.test.js +698 -0
  570. package/dist/tests/unit/storage.test.js.map +1 -0
  571. package/dist/tests/unit/text-normalizer.test.d.ts +2 -0
  572. package/dist/tests/unit/text-normalizer.test.d.ts.map +1 -0
  573. package/dist/tests/unit/text-normalizer.test.js +229 -0
  574. package/dist/tests/unit/text-normalizer.test.js.map +1 -0
  575. package/dist/tests/unit/tracing.test.d.ts +2 -0
  576. package/dist/tests/unit/tracing.test.d.ts.map +1 -0
  577. package/dist/tests/unit/tracing.test.js +611 -0
  578. package/dist/tests/unit/tracing.test.js.map +1 -0
  579. package/dist/tests/unit/trust-calculator.test.d.ts +2 -0
  580. package/dist/tests/unit/trust-calculator.test.d.ts.map +1 -0
  581. package/dist/tests/unit/trust-calculator.test.js +497 -0
  582. package/dist/tests/unit/trust-calculator.test.js.map +1 -0
  583. package/dist/tests/unit/ts-sdk.test.d.ts +2 -0
  584. package/dist/tests/unit/ts-sdk.test.d.ts.map +1 -0
  585. package/dist/tests/unit/ts-sdk.test.js +421 -0
  586. package/dist/tests/unit/ts-sdk.test.js.map +1 -0
  587. package/dist/tests/unit/usage-extractor-llm.test.d.ts +2 -0
  588. package/dist/tests/unit/usage-extractor-llm.test.d.ts.map +1 -0
  589. package/dist/tests/unit/usage-extractor-llm.test.js +139 -0
  590. package/dist/tests/unit/usage-extractor-llm.test.js.map +1 -0
  591. package/dist/tests/unit/usage-extractor.test.d.ts +2 -0
  592. package/dist/tests/unit/usage-extractor.test.d.ts.map +1 -0
  593. package/dist/tests/unit/usage-extractor.test.js +271 -0
  594. package/dist/tests/unit/usage-extractor.test.js.map +1 -0
  595. package/dist/tests/unit/user-stores.test.d.ts +2 -0
  596. package/dist/tests/unit/user-stores.test.d.ts.map +1 -0
  597. package/dist/tests/unit/user-stores.test.js +687 -0
  598. package/dist/tests/unit/user-stores.test.js.map +1 -0
  599. package/dist/tests/unit/validate.test.d.ts +2 -0
  600. package/dist/tests/unit/validate.test.d.ts.map +1 -0
  601. package/dist/tests/unit/validate.test.js +545 -0
  602. package/dist/tests/unit/validate.test.js.map +1 -0
  603. package/package.json +86 -0
  604. package/policy-packs/README.md +42 -0
  605. package/policy-packs/default.yaml +46 -0
  606. package/policy-packs/dev_fast.yaml +54 -0
  607. package/policy-packs/prod_strict.yaml +83 -0
@@ -0,0 +1,1566 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createSaaSRouter = createSaaSRouter;
37
+ const express_1 = require("express");
38
+ const crypto = __importStar(require("crypto"));
39
+ const crypto_1 = require("crypto");
40
+ const memory_1 = require("../storage/memory");
41
+ const calculator_1 = require("../trust/calculator");
42
+ const engine_1 = require("../replay/engine");
43
+ const plan_enforcer_1 = require("../billing/plan-enforcer");
44
+ /** Extract a route param as string (Express 5 returns string | string[]). */
45
+ function param(req, name) {
46
+ const val = req.params[name];
47
+ return Array.isArray(val) ? val[0] : val;
48
+ }
49
+ /**
50
+ * Require session authentication for all SaaS routes.
51
+ */
52
+ function requireSession(req, res) {
53
+ if (!req.sessionUser) {
54
+ res.status(401).json({ error: 'Session authentication required' });
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ function createSaaSRouter(deps) {
60
+ const router = (0, express_1.Router)();
61
+ const { config, userStore, workspaceStore, workspaceMemberStore, userApiKeyStore, sessionStore, gateway } = deps;
62
+ // ---------------------------------------------------------------------------
63
+ // User Profile
64
+ // ---------------------------------------------------------------------------
65
+ router.get('/user/profile', (req, res) => {
66
+ if (!requireSession(req, res))
67
+ return;
68
+ const user = req.sessionUser;
69
+ res.json({
70
+ id: user.id,
71
+ email: user.email,
72
+ display_name: user.display_name,
73
+ avatar_url: user.avatar_url,
74
+ status: user.status,
75
+ onboarding_completed: user.onboarding_completed,
76
+ created_at: user.created_at,
77
+ });
78
+ });
79
+ router.put('/user/profile', (req, res) => {
80
+ if (!requireSession(req, res))
81
+ return;
82
+ const user = req.sessionUser;
83
+ const { display_name } = req.body;
84
+ if (!display_name || typeof display_name !== 'string' || display_name.trim().length === 0) {
85
+ res.status(400).json({ error: 'display_name is required' });
86
+ return;
87
+ }
88
+ if (display_name.length > 100) {
89
+ res.status(400).json({ error: 'display_name must be 100 characters or less' });
90
+ return;
91
+ }
92
+ const updated = userStore.update(user.id, {
93
+ display_name: display_name.trim(),
94
+ updated_at: new Date().toISOString(),
95
+ });
96
+ res.json(updated);
97
+ });
98
+ router.put('/user/onboarding', (req, res) => {
99
+ if (!requireSession(req, res))
100
+ return;
101
+ const user = req.sessionUser;
102
+ const updated = userStore.update(user.id, {
103
+ onboarding_completed: true,
104
+ updated_at: new Date().toISOString(),
105
+ });
106
+ res.json(updated);
107
+ });
108
+ // ---------------------------------------------------------------------------
109
+ // Workspaces
110
+ // ---------------------------------------------------------------------------
111
+ router.get('/workspaces', (req, res) => {
112
+ if (!requireSession(req, res))
113
+ return;
114
+ const user = req.sessionUser;
115
+ const memberships = workspaceMemberStore.getByUser(user.id);
116
+ const workspaces = memberships.map(m => {
117
+ const ws = workspaceStore.getById(m.workspace_id);
118
+ return ws ? { ...ws, role: m.role } : null;
119
+ }).filter(Boolean);
120
+ res.json({ workspaces });
121
+ });
122
+ router.post('/workspaces', (req, res) => {
123
+ if (!requireSession(req, res))
124
+ return;
125
+ const user = req.sessionUser;
126
+ const { name, slug } = req.body;
127
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
128
+ res.status(400).json({ error: 'name is required' });
129
+ return;
130
+ }
131
+ // Enforce workspace limit based on user's highest plan
132
+ const userWorkspaces = workspaceMemberStore.getByUser(user.id)
133
+ .filter(m => m.role === 'owner')
134
+ .map(m => workspaceStore.getById(m.workspace_id))
135
+ .filter(Boolean);
136
+ // Determine the highest plan the user owns
137
+ const planPriority = { free: 0, pro: 1, business: 2, enterprise: 3 };
138
+ let highestPlan = 'free';
139
+ for (const ws of userWorkspaces) {
140
+ const p = (ws.plan || 'free');
141
+ if ((planPriority[p] || 0) > (planPriority[highestPlan] || 0)) {
142
+ highestPlan = p;
143
+ }
144
+ }
145
+ const wsLimitCheck = plan_enforcer_1.PlanEnforcer.checkWorkspaceLimit(highestPlan, userWorkspaces.length);
146
+ if (!wsLimitCheck.allowed) {
147
+ res.status(403).json({ error: wsLimitCheck.reason });
148
+ return;
149
+ }
150
+ // Generate slug from name if not provided
151
+ const wsSlug = (slug || name).toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').slice(0, 63);
152
+ if (workspaceStore.getBySlug(wsSlug)) {
153
+ res.status(409).json({ error: 'Workspace slug already taken' });
154
+ return;
155
+ }
156
+ const now = new Date().toISOString();
157
+ const workspaceId = (0, crypto_1.randomUUID)();
158
+ workspaceStore.create({
159
+ id: workspaceId,
160
+ name: name.trim(),
161
+ slug: wsSlug,
162
+ owner_user_id: user.id,
163
+ plan: 'free',
164
+ settings: {},
165
+ created_at: now,
166
+ updated_at: now,
167
+ });
168
+ workspaceMemberStore.create({
169
+ id: (0, crypto_1.randomUUID)(),
170
+ workspace_id: workspaceId,
171
+ user_id: user.id,
172
+ role: 'owner',
173
+ joined_at: now,
174
+ });
175
+ // Update session to point to this workspace
176
+ const sessionData = req.sessionData;
177
+ if (sessionData) {
178
+ sessionStore.update(sessionData.id, { workspace_id: workspaceId });
179
+ }
180
+ const workspace = workspaceStore.getById(workspaceId);
181
+ res.status(201).json(workspace);
182
+ });
183
+ router.get('/workspaces/:id', (req, res) => {
184
+ if (!requireSession(req, res))
185
+ return;
186
+ const user = req.sessionUser;
187
+ const workspaceId = param(req, 'id');
188
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
189
+ if (!membership) {
190
+ res.status(403).json({ error: 'Not a member of this workspace' });
191
+ return;
192
+ }
193
+ const workspace = workspaceStore.getById(workspaceId);
194
+ if (!workspace) {
195
+ res.status(404).json({ error: 'Workspace not found' });
196
+ return;
197
+ }
198
+ res.json({ ...workspace, role: membership.role });
199
+ });
200
+ // ---------------------------------------------------------------------------
201
+ // Workspace Stats (Dashboard)
202
+ // ---------------------------------------------------------------------------
203
+ router.get('/workspaces/:id/stats', (req, res) => {
204
+ if (!requireSession(req, res))
205
+ return;
206
+ const user = req.sessionUser;
207
+ const workspaceId = param(req, 'id');
208
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
209
+ if (!membership) {
210
+ res.status(403).json({ error: 'Not a member of this workspace' });
211
+ return;
212
+ }
213
+ // Gather stats from the gateway's audit store
214
+ // Match events by workspace UUID or slug (Android clients send slug as workspace_id)
215
+ const workspace = workspaceStore.getById(workspaceId);
216
+ const wsSlug = workspace?.slug;
217
+ const allEvents = gateway.getAuditLogger().getAllEvents();
218
+ const wsEvents = allEvents.filter(e => e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug));
219
+ const recentEvents = wsEvents.filter(e => {
220
+ const age = Date.now() - new Date(e.timestamp).getTime();
221
+ return age < 86400000; // Last 24 hours
222
+ });
223
+ const members = workspaceMemberStore.getByWorkspace(workspaceId);
224
+ const apiKeys = userApiKeyStore.getByWorkspace(workspaceId);
225
+ res.json({
226
+ total_requests: wsEvents.length,
227
+ requests_24h: recentEvents.length,
228
+ members: members.length,
229
+ api_keys: apiKeys.filter(k => !k.revoked).length,
230
+ recent_events: recentEvents.slice(-10).reverse(),
231
+ });
232
+ });
233
+ // ---------------------------------------------------------------------------
234
+ // Events (filterable, sortable, paginated)
235
+ // ---------------------------------------------------------------------------
236
+ router.get('/workspaces/:id/events', (req, res) => {
237
+ if (!requireSession(req, res))
238
+ return;
239
+ const user = req.sessionUser;
240
+ const workspaceId = param(req, 'id');
241
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
242
+ if (!membership) {
243
+ res.status(403).json({ error: 'Not a member of this workspace' });
244
+ return;
245
+ }
246
+ const workspace = workspaceStore.getById(workspaceId);
247
+ const wsSlug = workspace?.slug;
248
+ const allEvents = gateway.getAuditLogger().getAllEvents();
249
+ // Filter to this workspace (strict match — no ws_default fallback)
250
+ let events = allEvents.filter(e => e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug));
251
+ // Query params
252
+ const eventType = req.query.event_type;
253
+ const toolName = req.query.tool;
254
+ const actorId = req.query.actor;
255
+ const status = req.query.status;
256
+ const search = req.query.q;
257
+ const since = req.query.since;
258
+ const sort = req.query.sort || 'desc';
259
+ const limit = Math.min(parseInt(req.query.limit) || 50, 200);
260
+ const offset = parseInt(req.query.offset) || 0;
261
+ // Apply filters
262
+ if (eventType) {
263
+ const types = eventType.split(',');
264
+ events = events.filter(e => types.includes(e.event_type));
265
+ }
266
+ if (toolName) {
267
+ events = events.filter(e => e.tool_name?.includes(toolName));
268
+ }
269
+ if (actorId) {
270
+ events = events.filter(e => e.actor_id?.includes(actorId));
271
+ }
272
+ if (status) {
273
+ const statuses = status.split(',');
274
+ events = events.filter(e => {
275
+ const s = e.metadata?.status || e.metadata?.decision || '';
276
+ return statuses.some(st => s.includes(st));
277
+ });
278
+ }
279
+ if (search) {
280
+ const q = search.toLowerCase();
281
+ events = events.filter(e => e.tool_name?.toLowerCase().includes(q) ||
282
+ e.actor_id?.toLowerCase().includes(q) ||
283
+ e.task_id?.toLowerCase().includes(q) ||
284
+ e.event_type?.toLowerCase().includes(q));
285
+ }
286
+ if (since) {
287
+ events = events.filter(e => e.timestamp >= since);
288
+ }
289
+ // Sort
290
+ events.sort((a, b) => {
291
+ const cmp = a.timestamp.localeCompare(b.timestamp);
292
+ return sort === 'asc' ? cmp : -cmp;
293
+ });
294
+ const total = events.length;
295
+ const paged = events.slice(offset, offset + limit);
296
+ res.json({ events: paged, total, offset, limit });
297
+ });
298
+ // ---------------------------------------------------------------------------
299
+ // API Keys
300
+ // ---------------------------------------------------------------------------
301
+ router.get('/workspaces/:id/api-keys', (req, res) => {
302
+ if (!requireSession(req, res))
303
+ return;
304
+ const user = req.sessionUser;
305
+ const workspaceId = param(req, 'id');
306
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
307
+ if (!membership) {
308
+ res.status(403).json({ error: 'Not a member of this workspace' });
309
+ return;
310
+ }
311
+ const keys = userApiKeyStore.getByWorkspace(workspaceId);
312
+ // Never return the hash, only metadata
313
+ res.json({
314
+ api_keys: keys.map(k => ({
315
+ id: k.id,
316
+ key_prefix: k.key_prefix,
317
+ name: k.name,
318
+ roles: k.roles,
319
+ tags: k.tags || [],
320
+ revoked: k.revoked,
321
+ last_used_at: k.last_used_at,
322
+ created_at: k.created_at,
323
+ })),
324
+ });
325
+ });
326
+ router.post('/workspaces/:id/api-keys', (req, res) => {
327
+ if (!requireSession(req, res))
328
+ return;
329
+ const user = req.sessionUser;
330
+ const workspaceId = param(req, 'id');
331
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
332
+ if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
333
+ res.status(403).json({ error: 'Only workspace owners and admins can create API keys' });
334
+ return;
335
+ }
336
+ const { name, roles, tags } = req.body;
337
+ if (!name || typeof name !== 'string') {
338
+ res.status(400).json({ error: 'name is required' });
339
+ return;
340
+ }
341
+ // Validate tags
342
+ const parsedTags = Array.isArray(tags)
343
+ ? tags.filter((t) => typeof t === 'string' && t.length > 0).slice(0, 20)
344
+ : [];
345
+ // Enforce API key limit based on workspace plan
346
+ const wsForKeyLimit = workspaceStore.getById(workspaceId);
347
+ if (wsForKeyLimit) {
348
+ const plan = (wsForKeyLimit.plan || 'free');
349
+ const currentKeys = userApiKeyStore.getByWorkspace(workspaceId).filter(k => !k.revoked);
350
+ const keyLimitCheck = plan_enforcer_1.PlanEnforcer.checkApiKeyLimit(plan, currentKeys.length);
351
+ if (!keyLimitCheck.allowed) {
352
+ res.status(403).json({ error: keyLimitCheck.reason });
353
+ return;
354
+ }
355
+ }
356
+ // Generate a random API key (pn_<32 random hex chars>) with salted hash
357
+ const rawKey = `pn_${crypto.randomBytes(32).toString('hex')}`;
358
+ const salt = crypto.randomBytes(16).toString('hex');
359
+ const hash = crypto.createHash('sha256').update(salt + rawKey).digest('hex');
360
+ const keyHash = `${salt}:${hash}`; // salted format: "salt:hash"
361
+ const keyPrefix = rawKey.slice(0, 11); // "pn_" + first 8 hex
362
+ const apiKey = {
363
+ id: (0, crypto_1.randomUUID)(),
364
+ key_hash: keyHash,
365
+ key_prefix: keyPrefix,
366
+ user_id: user.id,
367
+ workspace_id: workspaceId,
368
+ name: name.trim(),
369
+ roles: Array.isArray(roles) ? roles : ['agent'],
370
+ tags: parsedTags,
371
+ revoked: false,
372
+ created_at: new Date().toISOString(),
373
+ };
374
+ userApiKeyStore.create(apiKey);
375
+ // Return the plaintext key only once
376
+ res.status(201).json({
377
+ id: apiKey.id,
378
+ key: rawKey,
379
+ key_prefix: keyPrefix,
380
+ name: apiKey.name,
381
+ roles: apiKey.roles,
382
+ tags: apiKey.tags,
383
+ workspace_id: workspaceId,
384
+ created_at: apiKey.created_at,
385
+ });
386
+ });
387
+ router.delete('/workspaces/:id/api-keys/:keyId', (req, res) => {
388
+ if (!requireSession(req, res))
389
+ return;
390
+ const user = req.sessionUser;
391
+ const workspaceId = param(req, 'id');
392
+ const keyId = param(req, 'keyId');
393
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
394
+ if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
395
+ res.status(403).json({ error: 'Only workspace owners and admins can revoke API keys' });
396
+ return;
397
+ }
398
+ const key = userApiKeyStore.getById(keyId);
399
+ if (!key || key.workspace_id !== workspaceId) {
400
+ res.status(404).json({ error: 'API key not found' });
401
+ return;
402
+ }
403
+ userApiKeyStore.update(keyId, { revoked: true });
404
+ res.json({ status: 'ok', id: keyId });
405
+ });
406
+ router.patch('/workspaces/:id/api-keys/:keyId', (req, res) => {
407
+ if (!requireSession(req, res))
408
+ return;
409
+ const user = req.sessionUser;
410
+ const workspaceId = param(req, 'id');
411
+ const keyId = param(req, 'keyId');
412
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
413
+ if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
414
+ res.status(403).json({ error: 'Only workspace owners and admins can update API keys' });
415
+ return;
416
+ }
417
+ const key = userApiKeyStore.getById(keyId);
418
+ if (!key || key.workspace_id !== workspaceId) {
419
+ res.status(404).json({ error: 'API key not found' });
420
+ return;
421
+ }
422
+ const updates = {};
423
+ if (req.body.name !== undefined && typeof req.body.name === 'string') {
424
+ updates.name = req.body.name.trim();
425
+ }
426
+ if (Array.isArray(req.body.tags)) {
427
+ updates.tags = req.body.tags.filter((t) => typeof t === 'string' && t.length > 0).slice(0, 20);
428
+ }
429
+ if (Object.keys(updates).length === 0) {
430
+ res.status(400).json({ error: 'No valid fields to update (supported: name, tags)' });
431
+ return;
432
+ }
433
+ const updated = userApiKeyStore.update(keyId, updates);
434
+ if (!updated) {
435
+ res.status(500).json({ error: 'Failed to update API key' });
436
+ return;
437
+ }
438
+ res.json({
439
+ id: updated.id,
440
+ key_prefix: updated.key_prefix,
441
+ name: updated.name,
442
+ roles: updated.roles,
443
+ tags: updated.tags || [],
444
+ revoked: updated.revoked,
445
+ last_used_at: updated.last_used_at,
446
+ created_at: updated.created_at,
447
+ });
448
+ });
449
+ // ---------------------------------------------------------------------------
450
+ // Traces
451
+ // ---------------------------------------------------------------------------
452
+ router.get('/workspaces/:id/traces', (req, res) => {
453
+ if (!requireSession(req, res))
454
+ return;
455
+ const user = req.sessionUser;
456
+ const workspaceId = param(req, 'id');
457
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
458
+ if (!membership) {
459
+ res.status(403).json({ error: 'Not a member of this workspace' });
460
+ return;
461
+ }
462
+ const limit = Math.min(parseInt(req.query.limit) || 50, 200);
463
+ const offset = parseInt(req.query.offset) || 0;
464
+ const statusFilter = req.query.status;
465
+ const toolFilter = req.query.tool;
466
+ const eventTypeFilter = req.query.event_type;
467
+ const allEvents = gateway.getAuditLogger().getAllEvents();
468
+ // Match events by workspace UUID or slug (Android clients send slug as workspace_id)
469
+ const wsForTraces = workspaceStore.getById(workspaceId);
470
+ const slugForTraces = wsForTraces?.slug;
471
+ let wsEvents = allEvents
472
+ .filter(e => e.workspace_id === workspaceId || (slugForTraces && e.workspace_id === slugForTraces));
473
+ // Apply server-side filters
474
+ if (statusFilter) {
475
+ const sf = statusFilter.toLowerCase();
476
+ wsEvents = wsEvents.filter(e => {
477
+ const decision = (e.metadata?.decision || '').toLowerCase();
478
+ const status = (e.metadata?.status || '').toLowerCase();
479
+ return decision.includes(sf) || status.includes(sf) || e.event_type.toLowerCase().includes(sf);
480
+ });
481
+ }
482
+ if (toolFilter) {
483
+ const tf = toolFilter.toLowerCase();
484
+ wsEvents = wsEvents.filter(e => (e.tool_name || '').toLowerCase().includes(tf));
485
+ }
486
+ if (eventTypeFilter) {
487
+ const ef = eventTypeFilter.toLowerCase();
488
+ wsEvents = wsEvents.filter(e => e.event_type.toLowerCase().includes(ef));
489
+ }
490
+ wsEvents.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
491
+ res.json({
492
+ traces: wsEvents.slice(offset, offset + limit),
493
+ total: wsEvents.length,
494
+ limit,
495
+ offset,
496
+ });
497
+ });
498
+ // ---------------------------------------------------------------------------
499
+ // Budgets
500
+ // ---------------------------------------------------------------------------
501
+ router.get('/workspaces/:id/budgets', (req, res) => {
502
+ if (!requireSession(req, res))
503
+ return;
504
+ const user = req.sessionUser;
505
+ const workspaceId = param(req, 'id');
506
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
507
+ if (!membership) {
508
+ res.status(403).json({ error: 'Not a member of this workspace' });
509
+ return;
510
+ }
511
+ const spending = gateway.getBudgetManager().getSpendingSummary();
512
+ // Use workspace-specific budget config if available, otherwise global
513
+ const budgetConfigStore = gateway.getBudgetConfigStore();
514
+ const wsConfig = budgetConfigStore?.getByWorkspaceId(workspaceId);
515
+ const effectiveConfig = wsConfig ? { ...config.budget, ...wsConfig } : config.budget;
516
+ const is_custom = !!wsConfig;
517
+ res.json({
518
+ workspace_id: workspaceId,
519
+ config: effectiveConfig,
520
+ spending,
521
+ is_custom,
522
+ });
523
+ });
524
+ // ---------------------------------------------------------------------------
525
+ // Approvals (proxy to gateway's ApprovalManager)
526
+ // ---------------------------------------------------------------------------
527
+ router.get('/workspaces/:id/approvals', (req, res) => {
528
+ if (!requireSession(req, res))
529
+ return;
530
+ const user = req.sessionUser;
531
+ const workspaceId = param(req, 'id');
532
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
533
+ if (!membership) {
534
+ res.status(403).json({ error: 'Not a member of this workspace' });
535
+ return;
536
+ }
537
+ const pending = gateway.getPendingApprovals(workspaceId);
538
+ // Strip the JWT token — frontend only sees approval metadata
539
+ const approvals = pending.map(a => ({
540
+ approval_id: a.approval_id,
541
+ tool_call_id: a.tool_call_id,
542
+ task_id: a.task_id,
543
+ workspace_id: a.workspace_id,
544
+ actor_id: a.actor_id,
545
+ tool_name: a.tool_name,
546
+ tool_capability: a.tool_capability,
547
+ args_summary: a.args_summary,
548
+ scope: a.scope,
549
+ reason: a.reason,
550
+ status: a.status,
551
+ created_at: a.created_at,
552
+ expires_at: a.expires_at,
553
+ }));
554
+ res.json({ approvals });
555
+ });
556
+ router.post('/workspaces/:id/approvals/:approvalId/approve', async (req, res) => {
557
+ if (!requireSession(req, res))
558
+ return;
559
+ const user = req.sessionUser;
560
+ const workspaceId = param(req, 'id');
561
+ const approvalId = param(req, 'approvalId');
562
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
563
+ if (!membership) {
564
+ res.status(403).json({ error: 'Not a member of this workspace' });
565
+ return;
566
+ }
567
+ // Verify approval exists and belongs to this workspace
568
+ const approval = gateway.getApprovalManager().getApproval(approvalId);
569
+ if (!approval) {
570
+ res.status(404).json({ error: 'Approval not found' });
571
+ return;
572
+ }
573
+ if (approval.workspace_id !== workspaceId) {
574
+ res.status(404).json({ error: 'Approval not found' });
575
+ return;
576
+ }
577
+ try {
578
+ await gateway.getApprovalManager().resolveById(approvalId, user.id, true);
579
+ }
580
+ catch (err) {
581
+ const message = err instanceof Error ? err.message : 'Approval failed';
582
+ res.status(400).json({ error: message });
583
+ return;
584
+ }
585
+ res.json({ status: 'approved', approval_id: approvalId });
586
+ });
587
+ router.post('/workspaces/:id/approvals/:approvalId/deny', async (req, res) => {
588
+ if (!requireSession(req, res))
589
+ return;
590
+ const user = req.sessionUser;
591
+ const workspaceId = param(req, 'id');
592
+ const approvalId = param(req, 'approvalId');
593
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
594
+ if (!membership) {
595
+ res.status(403).json({ error: 'Not a member of this workspace' });
596
+ return;
597
+ }
598
+ const approvalRecord = gateway.getApprovalManager().getApproval(approvalId);
599
+ if (!approvalRecord) {
600
+ res.status(404).json({ error: 'Approval not found' });
601
+ return;
602
+ }
603
+ if (approvalRecord.workspace_id !== workspaceId) {
604
+ res.status(404).json({ error: 'Approval not found' });
605
+ return;
606
+ }
607
+ const reason = req.body?.reason || 'Denied via SaaS dashboard';
608
+ try {
609
+ await gateway.getApprovalManager().resolveById(approvalId, user.id, false, reason);
610
+ }
611
+ catch (err) {
612
+ const message = err instanceof Error ? err.message : 'Denial failed';
613
+ res.status(400).json({ error: message });
614
+ return;
615
+ }
616
+ res.json({ status: 'denied', approval_id: approvalId });
617
+ });
618
+ // ---------------------------------------------------------------------------
619
+ // Policies (workspace-aware CRUD)
620
+ // ---------------------------------------------------------------------------
621
+ // Ensure a PolicyStore is available on the gateway
622
+ if (!gateway.getPolicyStore()) {
623
+ gateway.setStores({ policyStore: deps.policyStore || new memory_1.InMemoryPolicyStore() });
624
+ }
625
+ const policyStore = gateway.getPolicyStore();
626
+ router.get('/workspaces/:id/policies', (req, res) => {
627
+ if (!requireSession(req, res))
628
+ return;
629
+ const user = req.sessionUser;
630
+ const workspaceId = param(req, 'id');
631
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
632
+ if (!membership) {
633
+ res.status(403).json({ error: 'Not a member of this workspace' });
634
+ return;
635
+ }
636
+ const { policy, is_custom } = gateway.getWorkspacePolicy(workspaceId);
637
+ res.json({ policy, is_custom });
638
+ });
639
+ router.put('/workspaces/:id/policies', (req, res) => {
640
+ if (!requireSession(req, res))
641
+ return;
642
+ const user = req.sessionUser;
643
+ const workspaceId = param(req, 'id');
644
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
645
+ if (!membership) {
646
+ res.status(403).json({ error: 'Not a member of this workspace' });
647
+ return;
648
+ }
649
+ if (!['owner', 'admin'].includes(membership.role)) {
650
+ res.status(403).json({ error: 'Only workspace owners and admins can modify policies' });
651
+ return;
652
+ }
653
+ const body = req.body;
654
+ const validation = gateway.validatePolicy(body);
655
+ if (!validation.valid) {
656
+ res.status(400).json({ valid: false, errors: validation.errors });
657
+ return;
658
+ }
659
+ policyStore.set(workspaceId, body);
660
+ gateway.getAuditLogger().log({
661
+ event_type: 'POLICY_UPDATED',
662
+ tool_call_id: '',
663
+ task_id: '',
664
+ workspace_id: workspaceId,
665
+ actor_id: user.id,
666
+ tool_name: '',
667
+ metadata: { policy_name: body.name, policy_version: body.version, updated_by: user.id },
668
+ });
669
+ res.json({ policy: body, is_custom: true });
670
+ });
671
+ router.delete('/workspaces/:id/policies', (req, res) => {
672
+ if (!requireSession(req, res))
673
+ return;
674
+ const user = req.sessionUser;
675
+ const workspaceId = param(req, 'id');
676
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
677
+ if (!membership) {
678
+ res.status(403).json({ error: 'Not a member of this workspace' });
679
+ return;
680
+ }
681
+ if (!['owner', 'admin'].includes(membership.role)) {
682
+ res.status(403).json({ error: 'Only workspace owners and admins can modify policies' });
683
+ return;
684
+ }
685
+ policyStore.delete(workspaceId);
686
+ gateway.getAuditLogger().log({
687
+ event_type: 'POLICY_RESET',
688
+ tool_call_id: '',
689
+ task_id: '',
690
+ workspace_id: workspaceId,
691
+ actor_id: user.id,
692
+ tool_name: '',
693
+ metadata: { reset_by: user.id },
694
+ });
695
+ const policy = gateway.getCurrentPolicy();
696
+ res.json({ status: 'reset', policy, is_custom: false });
697
+ });
698
+ router.post('/workspaces/:id/policies/validate', (req, res) => {
699
+ if (!requireSession(req, res))
700
+ return;
701
+ const user = req.sessionUser;
702
+ const workspaceId = param(req, 'id');
703
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
704
+ if (!membership) {
705
+ res.status(403).json({ error: 'Not a member of this workspace' });
706
+ return;
707
+ }
708
+ const body = req.body;
709
+ const validation = gateway.validatePolicy(body);
710
+ res.json(validation);
711
+ });
712
+ // ---------------------------------------------------------------------------
713
+ // Policy Rule Generation (LLM-powered)
714
+ // ---------------------------------------------------------------------------
715
+ router.post('/workspaces/:id/policies/generate-rule', async (req, res) => {
716
+ if (!requireSession(req, res))
717
+ return;
718
+ const user = req.sessionUser;
719
+ const workspaceId = param(req, 'id');
720
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
721
+ if (!membership) {
722
+ res.status(403).json({ error: 'Not a member of this workspace' });
723
+ return;
724
+ }
725
+ const apiKey = process.env.PALARYN_LLM_API_KEY;
726
+ if (!apiKey) {
727
+ res.status(503).json({ error: 'AI rule generation is not configured. Set PALARYN_LLM_API_KEY environment variable.' });
728
+ return;
729
+ }
730
+ const { description } = req.body;
731
+ if (!description || typeof description !== 'string' || description.trim().length === 0) {
732
+ res.status(400).json({ error: 'description is required' });
733
+ return;
734
+ }
735
+ const trimmed = description.trim().slice(0, 500);
736
+ const systemPrompt = `You are a policy rule generator for Palaryn, an AI agent gateway. Given a natural language description, generate a single JSON PolicyRule object.
737
+
738
+ The PolicyRule schema:
739
+ {
740
+ "name": string, // Short kebab-case name for the rule
741
+ "description": string, // Human-readable description
742
+ "effect": "ALLOW" | "DENY" | "REQUIRE_APPROVAL" | "TRANSFORM",
743
+ "priority": number, // Lower = higher precedence (1-100)
744
+ "conditions": {
745
+ "tools": string[], // Tool names: "http.request", "slack.post_message", etc.
746
+ "tool_match": string, // Regex pattern for tool name matching
747
+ "capabilities": string[], // "read", "write", "delete", "admin"
748
+ "actors": string[], // Actor/agent IDs
749
+ "actor_types": string[], // "agent", "user", "system"
750
+ "domains": string[], // URL domains: "api.github.com", "*.googleapis.com"
751
+ "domain_blocklist": string[],// Blocked domains
752
+ "methods": string[], // HTTP methods: "GET", "POST", "PUT", "DELETE", "PATCH"
753
+ "labels": string[], // Context labels
754
+ "platforms": string[], // Source platforms: "langgraph", "claude_code", "n8n", "custom"
755
+ "workspace_ids": string[] // Specific workspace IDs
756
+ },
757
+ "approval": { // Only when effect is REQUIRE_APPROVAL
758
+ "scope": string,
759
+ "ttl_seconds": number,
760
+ "reason": string
761
+ }
762
+ }
763
+
764
+ Rules:
765
+ - Only include condition fields that are relevant to the description. Omit empty arrays.
766
+ - Use "DENY" for blocking, "ALLOW" for permitting, "REQUIRE_APPROVAL" when human approval is needed.
767
+ - For domain wildcards use "*.example.com" format.
768
+ - IMPORTANT: Always pair capabilities with matching methods. read → ["GET","HEAD","OPTIONS"], write → ["POST","PUT","PATCH"], delete → ["DELETE"], admin → all methods.
769
+ - When the description says "read-only", set capabilities to ["read"] AND methods to ["GET","HEAD","OPTIONS"].
770
+ - Return ONLY the JSON object, no markdown fences, no explanation.
771
+
772
+ Examples:
773
+ Input: "Allow GET requests to GitHub and Google APIs"
774
+ Output: {"name":"allow-get-github-google","description":"Allow GET requests to GitHub and Google APIs","effect":"ALLOW","priority":10,"conditions":{"tools":["http.request"],"capabilities":["read"],"methods":["GET"],"domains":["api.github.com","*.googleapis.com"]}}
775
+
776
+ Input: "Block all delete operations"
777
+ Output: {"name":"block-delete-ops","description":"Block all delete operations","effect":"DENY","priority":5,"conditions":{"capabilities":["delete"],"methods":["DELETE"]}}
778
+
779
+ Input: "Allow read-only access"
780
+ Output: {"name":"allow-read-only","description":"Allow read-only access","effect":"ALLOW","priority":10,"conditions":{"capabilities":["read"],"methods":["GET","HEAD","OPTIONS"]}}
781
+
782
+ Input: "Require approval for write operations to Slack"
783
+ Output: {"name":"approve-slack-writes","description":"Require approval for write operations to Slack","effect":"REQUIRE_APPROVAL","priority":15,"conditions":{"capabilities":["write"],"methods":["POST","PUT","PATCH"],"domains":["api.slack.com"]},"approval":{"scope":"team_lead","ttl_seconds":3600,"reason":"Write operations to Slack require approval"}}`;
784
+ try {
785
+ const controller = new AbortController();
786
+ const timeout = setTimeout(() => controller.abort(), 15000);
787
+ const llmRes = await fetch('https://api.anthropic.com/v1/messages', {
788
+ method: 'POST',
789
+ headers: {
790
+ 'Content-Type': 'application/json',
791
+ 'x-api-key': apiKey,
792
+ 'anthropic-version': '2023-06-01',
793
+ },
794
+ body: JSON.stringify({
795
+ model: 'claude-sonnet-4-5-20250929',
796
+ max_tokens: 1024,
797
+ system: systemPrompt,
798
+ messages: [{ role: 'user', content: trimmed }],
799
+ }),
800
+ signal: controller.signal,
801
+ });
802
+ clearTimeout(timeout);
803
+ if (!llmRes.ok) {
804
+ const errBody = await llmRes.text();
805
+ console.error('[generate-rule] LLM API error:', llmRes.status, errBody);
806
+ res.status(502).json({ error: 'Failed to generate rule. LLM API returned an error.' });
807
+ return;
808
+ }
809
+ const llmData = await llmRes.json();
810
+ const text = llmData.content?.[0]?.text || '';
811
+ // Strip markdown code fences if present
812
+ const cleaned = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
813
+ let rule;
814
+ try {
815
+ rule = JSON.parse(cleaned);
816
+ }
817
+ catch {
818
+ console.error('[generate-rule] Failed to parse LLM output:', cleaned);
819
+ res.status(500).json({ error: 'Failed to parse generated rule. Please try rephrasing your description.' });
820
+ return;
821
+ }
822
+ // Normalize effect to uppercase
823
+ if (rule.effect && typeof rule.effect === 'string') {
824
+ rule.effect = rule.effect.toUpperCase();
825
+ }
826
+ // Ensure required fields exist
827
+ if (!rule.name)
828
+ rule.name = 'generated-rule';
829
+ if (!rule.conditions)
830
+ rule.conditions = {};
831
+ if (!rule.effect)
832
+ rule.effect = 'DENY';
833
+ // Strip unknown top-level fields
834
+ const validKeys = ['name', 'description', 'effect', 'priority', 'conditions', 'transformations', 'approval'];
835
+ for (const key of Object.keys(rule)) {
836
+ if (!validKeys.includes(key))
837
+ delete rule[key];
838
+ }
839
+ res.json({ rule });
840
+ }
841
+ catch (err) {
842
+ if (err.name === 'AbortError') {
843
+ res.status(504).json({ error: 'Rule generation timed out. Please try again.' });
844
+ return;
845
+ }
846
+ console.error('[generate-rule] Unexpected error:', err);
847
+ res.status(500).json({ error: 'Failed to generate rule. Please try again.' });
848
+ }
849
+ });
850
+ // ---------------------------------------------------------------------------
851
+ // Security (DLP detections dashboard)
852
+ // ---------------------------------------------------------------------------
853
+ router.get('/workspaces/:id/security', (req, res) => {
854
+ if (!requireSession(req, res))
855
+ return;
856
+ const user = req.sessionUser;
857
+ const workspaceId = param(req, 'id');
858
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
859
+ if (!membership) {
860
+ res.status(403).json({ error: 'Not a member of this workspace' });
861
+ return;
862
+ }
863
+ const limit = Math.min(parseInt(req.query.limit) || 50, 200);
864
+ const offset = parseInt(req.query.offset) || 0;
865
+ const allDlpEvents = gateway.getAuditLogger().getEventsByType('DLP_SCANNED');
866
+ const workspace = workspaceStore.getById(workspaceId);
867
+ const wsSlug = workspace?.slug;
868
+ const wsEvents = allDlpEvents
869
+ .filter(e => e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug))
870
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp));
871
+ // Compute severity stats
872
+ let high = 0, medium = 0, low = 0;
873
+ const patternSet = new Set();
874
+ for (const e of wsEvents) {
875
+ const severity = e.metadata?.severity;
876
+ if (severity === 'high')
877
+ high++;
878
+ else if (severity === 'medium')
879
+ medium++;
880
+ else
881
+ low++;
882
+ const detected = e.metadata?.detected;
883
+ if (Array.isArray(detected)) {
884
+ for (const p of detected)
885
+ patternSet.add(p);
886
+ }
887
+ }
888
+ res.json({
889
+ events: wsEvents.slice(offset, offset + limit),
890
+ total: wsEvents.length,
891
+ limit,
892
+ offset,
893
+ stats: {
894
+ total: wsEvents.length,
895
+ high,
896
+ medium,
897
+ low,
898
+ unique_patterns: patternSet.size,
899
+ },
900
+ });
901
+ });
902
+ // ---------------------------------------------------------------------------
903
+ // Workspace Switch
904
+ // ---------------------------------------------------------------------------
905
+ router.post('/workspaces/:id/switch', (req, res) => {
906
+ if (!requireSession(req, res))
907
+ return;
908
+ const user = req.sessionUser;
909
+ const workspaceId = param(req, 'id');
910
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
911
+ if (!membership) {
912
+ res.status(403).json({ error: 'Not a member of this workspace' });
913
+ return;
914
+ }
915
+ const workspace = workspaceStore.getById(workspaceId);
916
+ if (!workspace) {
917
+ res.status(404).json({ error: 'Workspace not found' });
918
+ return;
919
+ }
920
+ // Update session to point to this workspace
921
+ const sessionData = req.sessionData;
922
+ if (sessionData) {
923
+ sessionStore.update(sessionData.id, { workspace_id: workspaceId });
924
+ }
925
+ res.json({ ...workspace, role: membership.role });
926
+ });
927
+ // ---------------------------------------------------------------------------
928
+ // Members
929
+ // ---------------------------------------------------------------------------
930
+ router.get('/workspaces/:id/members', (req, res) => {
931
+ if (!requireSession(req, res))
932
+ return;
933
+ const user = req.sessionUser;
934
+ const workspaceId = param(req, 'id');
935
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
936
+ if (!membership) {
937
+ res.status(403).json({ error: 'Not a member of this workspace' });
938
+ return;
939
+ }
940
+ const members = workspaceMemberStore.getByWorkspace(workspaceId);
941
+ const enriched = members.map(m => {
942
+ const u = userStore.getById(m.user_id);
943
+ return {
944
+ id: m.id,
945
+ user_id: m.user_id,
946
+ role: m.role,
947
+ joined_at: m.joined_at,
948
+ email: u?.email || '',
949
+ display_name: u?.display_name || '',
950
+ };
951
+ });
952
+ res.json({ members: enriched, viewer_role: membership.role });
953
+ });
954
+ router.put('/workspaces/:id/members/:memberId', (req, res) => {
955
+ if (!requireSession(req, res))
956
+ return;
957
+ const user = req.sessionUser;
958
+ const workspaceId = param(req, 'id');
959
+ const memberId = param(req, 'memberId');
960
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
961
+ if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
962
+ res.status(403).json({ error: 'Only workspace owners and admins can change roles' });
963
+ return;
964
+ }
965
+ const target = workspaceMemberStore.getById(memberId);
966
+ if (!target || target.workspace_id !== workspaceId) {
967
+ res.status(404).json({ error: 'Member not found' });
968
+ return;
969
+ }
970
+ // Cannot change own role
971
+ if (target.user_id === user.id) {
972
+ res.status(400).json({ error: 'Cannot change your own role' });
973
+ return;
974
+ }
975
+ const { role } = req.body;
976
+ const validRoles = ['owner', 'admin', 'member', 'viewer'];
977
+ if (!role || !validRoles.includes(role)) {
978
+ res.status(400).json({ error: `role must be one of: ${validRoles.join(', ')}` });
979
+ return;
980
+ }
981
+ const updated = workspaceMemberStore.update(memberId, { role });
982
+ res.json(updated);
983
+ });
984
+ router.delete('/workspaces/:id/members/:memberId', (req, res) => {
985
+ if (!requireSession(req, res))
986
+ return;
987
+ const user = req.sessionUser;
988
+ const workspaceId = param(req, 'id');
989
+ const memberId = param(req, 'memberId');
990
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
991
+ if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
992
+ res.status(403).json({ error: 'Only workspace owners and admins can remove members' });
993
+ return;
994
+ }
995
+ const target = workspaceMemberStore.getById(memberId);
996
+ if (!target || target.workspace_id !== workspaceId) {
997
+ res.status(404).json({ error: 'Member not found' });
998
+ return;
999
+ }
1000
+ // Cannot remove yourself
1001
+ if (target.user_id === user.id) {
1002
+ res.status(400).json({ error: 'Cannot remove yourself' });
1003
+ return;
1004
+ }
1005
+ // Prevent removing the last owner
1006
+ if (target.role === 'owner') {
1007
+ const owners = workspaceMemberStore.getByWorkspace(workspaceId).filter(m => m.role === 'owner');
1008
+ if (owners.length <= 1) {
1009
+ res.status(400).json({ error: 'Cannot remove the last owner' });
1010
+ return;
1011
+ }
1012
+ }
1013
+ workspaceMemberStore.delete(memberId);
1014
+ res.json({ status: 'ok', id: memberId });
1015
+ });
1016
+ router.post('/workspaces/:id/members', (req, res) => {
1017
+ if (!requireSession(req, res))
1018
+ return;
1019
+ const user = req.sessionUser;
1020
+ const workspaceId = param(req, 'id');
1021
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1022
+ if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
1023
+ res.status(403).json({ error: 'Only workspace owners and admins can add members' });
1024
+ return;
1025
+ }
1026
+ const { email, role } = req.body;
1027
+ if (!email || typeof email !== 'string') {
1028
+ res.status(400).json({ error: 'email is required' });
1029
+ return;
1030
+ }
1031
+ // Enforce member limit based on workspace plan
1032
+ const wsForLimit = workspaceStore.getById(workspaceId);
1033
+ if (wsForLimit) {
1034
+ const plan = (wsForLimit.plan || 'free');
1035
+ const currentMembers = workspaceMemberStore.getByWorkspace(workspaceId);
1036
+ const memberLimitCheck = plan_enforcer_1.PlanEnforcer.checkMemberLimit(plan, currentMembers.length);
1037
+ if (!memberLimitCheck.allowed) {
1038
+ res.status(403).json({ error: memberLimitCheck.reason });
1039
+ return;
1040
+ }
1041
+ }
1042
+ const validRoles = ['admin', 'member', 'viewer'];
1043
+ const memberRole = validRoles.includes(role) ? role : 'member';
1044
+ const targetUser = userStore.getByEmail(email.trim().toLowerCase());
1045
+ if (!targetUser) {
1046
+ res.status(404).json({ error: 'User not found' });
1047
+ return;
1048
+ }
1049
+ // Check if already a member
1050
+ const existing = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, targetUser.id);
1051
+ if (existing) {
1052
+ res.status(409).json({ error: 'User is already a member of this workspace' });
1053
+ return;
1054
+ }
1055
+ const newMember = {
1056
+ id: (0, crypto_1.randomUUID)(),
1057
+ workspace_id: workspaceId,
1058
+ user_id: targetUser.id,
1059
+ role: memberRole,
1060
+ joined_at: new Date().toISOString(),
1061
+ };
1062
+ workspaceMemberStore.create(newMember);
1063
+ const enriched = {
1064
+ id: newMember.id,
1065
+ user_id: targetUser.id,
1066
+ role: newMember.role,
1067
+ joined_at: newMember.joined_at,
1068
+ email: targetUser.email,
1069
+ display_name: targetUser.display_name,
1070
+ };
1071
+ res.status(201).json(enriched);
1072
+ });
1073
+ // ---------------------------------------------------------------------------
1074
+ // Trace Detail (single task)
1075
+ // ---------------------------------------------------------------------------
1076
+ router.get('/workspaces/:id/traces/:taskId', (req, res) => {
1077
+ if (!requireSession(req, res))
1078
+ return;
1079
+ const user = req.sessionUser;
1080
+ const workspaceId = param(req, 'id');
1081
+ const taskId = param(req, 'taskId');
1082
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1083
+ if (!membership) {
1084
+ res.status(403).json({ error: 'Not a member of this workspace' });
1085
+ return;
1086
+ }
1087
+ const events = gateway.getTaskTrace(taskId);
1088
+ // Filter to events belonging to this workspace
1089
+ const workspace = workspaceStore.getById(workspaceId);
1090
+ const wsSlug = workspace?.slug;
1091
+ const filteredEvents = events.filter(e => e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug));
1092
+ // Cap at 200 events to prevent UI overload
1093
+ res.json({
1094
+ task_id: taskId,
1095
+ events: filteredEvents.slice(0, 200),
1096
+ total: filteredEvents.length,
1097
+ });
1098
+ });
1099
+ // ---------------------------------------------------------------------------
1100
+ // Dashboard Stats (aggregated metrics for dashboard widgets)
1101
+ // ---------------------------------------------------------------------------
1102
+ router.get('/workspaces/:id/dashboard/stats', (req, res) => {
1103
+ if (!requireSession(req, res))
1104
+ return;
1105
+ const user = req.sessionUser;
1106
+ const workspaceId = param(req, 'id');
1107
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1108
+ if (!membership) {
1109
+ res.status(403).json({ error: 'Not a member of this workspace' });
1110
+ return;
1111
+ }
1112
+ const workspace = workspaceStore.getById(workspaceId);
1113
+ const wsSlug = workspace?.slug;
1114
+ // Use the audit logger's getEventStats for aggregated metrics
1115
+ // Pass both workspace UUID and slug to match events (no ws_default — strict isolation)
1116
+ const stats = gateway.getAuditLogger().getEventStats(workspaceId, 24);
1117
+ const slugStats = wsSlug ? gateway.getAuditLogger().getEventStats(wsSlug, 24) : null;
1118
+ // Merge stats from UUID and slug matches only
1119
+ const allStats = [stats, slugStats].filter((s) => s !== null);
1120
+ const mergeSum = (fn) => allStats.reduce((acc, s) => acc + fn(s), 0);
1121
+ const metrics = {
1122
+ requests_per_minute: Math.round(mergeSum(s => s.requests_per_minute) * 10) / 10,
1123
+ blocked_24h: mergeSum(s => s.blocked_count),
1124
+ avg_latency_ms: Math.round(stats.avg_duration_ms || slugStats?.avg_duration_ms || 0),
1125
+ active_agents: mergeSum(s => s.active_agents),
1126
+ pending_approvals: gateway.getPendingApprovals(workspaceId).length,
1127
+ budget_burn_percent: 0,
1128
+ };
1129
+ // Compute budget burn
1130
+ const spending = gateway.getBudgetManager().getSpendingSummary();
1131
+ const budgetConfig = config.budget;
1132
+ if (budgetConfig.workspace_monthly_budget_usd && budgetConfig.workspace_monthly_budget_usd > 0) {
1133
+ metrics.budget_burn_percent = Math.round((spending.workspace_monthly_total / budgetConfig.workspace_monthly_budget_usd) * 100);
1134
+ }
1135
+ // Shield score: compute from policy breakdown (across all matching workspace IDs)
1136
+ const totalDecisions = allStats.reduce((acc, s) => acc + Object.values(s.policy_breakdown).reduce((sum, n) => sum + n, 0), 0);
1137
+ const allowCount = allStats.reduce((acc, s) => acc + (s.policy_breakdown['allow'] || 0), 0);
1138
+ const transformCount = allStats.reduce((acc, s) => acc + (s.policy_breakdown['transform'] || 0), 0);
1139
+ const approvalCount = allStats.reduce((acc, s) => acc + (s.policy_breakdown['require_approval'] || 0), 0);
1140
+ const denyCount = allStats.reduce((acc, s) => acc + (s.policy_breakdown['deny'] || 0), 0);
1141
+ const shield_score = {
1142
+ score: totalDecisions > 0 ? Math.round(((allowCount + transformCount) / totalDecisions) * 100) : 100,
1143
+ breakdown: {
1144
+ allowed_percent: totalDecisions > 0 ? Math.round((allowCount / totalDecisions) * 100) : 100,
1145
+ transformed_percent: totalDecisions > 0 ? Math.round((transformCount / totalDecisions) * 100) : 0,
1146
+ approval_percent: totalDecisions > 0 ? Math.round((approvalCount / totalDecisions) * 100) : 0,
1147
+ blocked_percent: totalDecisions > 0 ? Math.round((denyCount / totalDecisions) * 100) : 0,
1148
+ },
1149
+ };
1150
+ // Pipeline throughput from stats (merged across all matching workspace IDs)
1151
+ const pipeline_throughput = stats.pipeline_throughput.map(stage => ({
1152
+ ...stage,
1153
+ passed: allStats.reduce((acc, s) => acc + (s.pipeline_throughput.find(t => t.stage === stage.stage)?.passed || 0), 0),
1154
+ failed: allStats.reduce((acc, s) => acc + (s.pipeline_throughput.find(t => t.stage === stage.stage)?.failed || 0), 0),
1155
+ }));
1156
+ res.json({
1157
+ metrics,
1158
+ shield_score,
1159
+ pipeline_throughput,
1160
+ });
1161
+ });
1162
+ // ---------------------------------------------------------------------------
1163
+ // Anomaly Baseline (for anomaly radar widget)
1164
+ // ---------------------------------------------------------------------------
1165
+ router.get('/workspaces/:id/anomalies/baseline', (req, res) => {
1166
+ if (!requireSession(req, res))
1167
+ return;
1168
+ const user = req.sessionUser;
1169
+ const workspaceId = param(req, 'id');
1170
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1171
+ if (!membership) {
1172
+ res.status(403).json({ error: 'Not a member of this workspace' });
1173
+ return;
1174
+ }
1175
+ const detector = gateway.getAnomalyDetector();
1176
+ if (!detector) {
1177
+ res.json({ current: {}, baseline: {}, alerts: [] });
1178
+ return;
1179
+ }
1180
+ const report = detector.getBaselineReport();
1181
+ res.json(report);
1182
+ });
1183
+ // ---------------------------------------------------------------------------
1184
+ // Agent Trust Score
1185
+ // ---------------------------------------------------------------------------
1186
+ router.get('/workspaces/:id/agents/:actorId/trust', (req, res) => {
1187
+ if (!requireSession(req, res))
1188
+ return;
1189
+ const user = req.sessionUser;
1190
+ const workspaceId = param(req, 'id');
1191
+ const actorId = param(req, 'actorId');
1192
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1193
+ if (!membership) {
1194
+ res.status(403).json({ error: 'Not a member of this workspace' });
1195
+ return;
1196
+ }
1197
+ const detector = gateway.getAnomalyDetector();
1198
+ if (!detector) {
1199
+ res.json({ actor_id: actorId, score: 100, risk_level: 'low', breakdown: {}, calculated_at: new Date().toISOString() });
1200
+ return;
1201
+ }
1202
+ const calculator = new calculator_1.TrustScoreCalculator(detector, gateway.getAuditLogger(), gateway.getBudgetManager());
1203
+ res.json(calculator.calculate(actorId));
1204
+ });
1205
+ router.get('/workspaces/:id/agents/trust-leaderboard', (req, res) => {
1206
+ if (!requireSession(req, res))
1207
+ return;
1208
+ const user = req.sessionUser;
1209
+ const workspaceId = param(req, 'id');
1210
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1211
+ if (!membership) {
1212
+ res.status(403).json({ error: 'Not a member of this workspace' });
1213
+ return;
1214
+ }
1215
+ // Get distinct actor IDs from workspace events
1216
+ const workspace = workspaceStore.getById(workspaceId);
1217
+ const wsSlug = workspace?.slug;
1218
+ const allEvents = gateway.getAuditLogger().getAllEvents();
1219
+ const actorIds = new Set();
1220
+ for (const e of allEvents) {
1221
+ if (e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug)) {
1222
+ if (e.actor_id)
1223
+ actorIds.add(e.actor_id);
1224
+ }
1225
+ }
1226
+ const detector = gateway.getAnomalyDetector();
1227
+ if (!detector || actorIds.size === 0) {
1228
+ res.json({ agents: [] });
1229
+ return;
1230
+ }
1231
+ const calculator = new calculator_1.TrustScoreCalculator(detector, gateway.getAuditLogger(), gateway.getBudgetManager());
1232
+ const leaderboard = calculator.getLeaderboard([...actorIds]);
1233
+ res.json({ agents: leaderboard });
1234
+ });
1235
+ // ---------------------------------------------------------------------------
1236
+ // Session Replay
1237
+ // ---------------------------------------------------------------------------
1238
+ router.post('/workspaces/:id/replay', (req, res) => {
1239
+ if (!requireSession(req, res))
1240
+ return;
1241
+ const user = req.sessionUser;
1242
+ const workspaceId = param(req, 'id');
1243
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1244
+ if (!membership) {
1245
+ res.status(403).json({ error: 'Not a member of this workspace' });
1246
+ return;
1247
+ }
1248
+ const { task_id, policy_pack_path } = req.body;
1249
+ if (!task_id || typeof task_id !== 'string') {
1250
+ res.status(400).json({ error: 'task_id is required' });
1251
+ return;
1252
+ }
1253
+ if (!policy_pack_path || typeof policy_pack_path !== 'string') {
1254
+ res.status(400).json({ error: 'policy_pack_path is required' });
1255
+ return;
1256
+ }
1257
+ const engine = new engine_1.SessionReplayEngine(gateway.getAuditLogger());
1258
+ const result = engine.replay(task_id, policy_pack_path);
1259
+ res.json(result);
1260
+ });
1261
+ // ---------------------------------------------------------------------------
1262
+ // LLM Usage (model/provider breakdown)
1263
+ // ---------------------------------------------------------------------------
1264
+ router.get('/workspaces/:id/llm-usage', (req, res) => {
1265
+ if (!requireSession(req, res))
1266
+ return;
1267
+ const user = req.sessionUser;
1268
+ const workspaceId = param(req, 'id');
1269
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1270
+ if (!membership) {
1271
+ res.status(403).json({ error: 'Not a member of this workspace' });
1272
+ return;
1273
+ }
1274
+ const range = req.query.range || '24h';
1275
+ const rangeMs = {
1276
+ '1h': 3600000,
1277
+ '6h': 21600000,
1278
+ '24h': 86400000,
1279
+ '7d': 604800000,
1280
+ '30d': 2592000000,
1281
+ };
1282
+ const windowMs = rangeMs[range] || rangeMs['24h'];
1283
+ const cutoff = Date.now() - windowMs;
1284
+ const workspace = workspaceStore.getById(workspaceId);
1285
+ const wsSlug = workspace?.slug;
1286
+ const allEvents = gateway.getAuditLogger().getAllEvents();
1287
+ // Filter events that have model metadata and belong to this workspace
1288
+ const wsEvents = allEvents.filter(e => {
1289
+ if (e.workspace_id !== workspaceId && (!wsSlug || e.workspace_id !== wsSlug))
1290
+ return false;
1291
+ if (new Date(e.timestamp).getTime() < cutoff)
1292
+ return false;
1293
+ return e.metadata?.model;
1294
+ });
1295
+ // Aggregate by model
1296
+ const modelMap = new Map();
1297
+ let totalRequests = 0;
1298
+ let totalCostUsd = 0;
1299
+ let totalTokens = 0;
1300
+ let totalLatencyMs = 0;
1301
+ for (const e of wsEvents) {
1302
+ const model = e.metadata.model;
1303
+ const provider = e.metadata.provider || 'unknown';
1304
+ const inputTokens = e.metadata.input_tokens || 0;
1305
+ const outputTokens = e.metadata.output_tokens || 0;
1306
+ const costUsd = e.metadata.cost_usd || 0;
1307
+ const durationMs = e.metadata.duration_ms || 0;
1308
+ let entry = modelMap.get(model);
1309
+ if (!entry) {
1310
+ entry = { model, provider, requests: 0, input_tokens: 0, output_tokens: 0, cost_usd: 0, total_latency_ms: 0 };
1311
+ modelMap.set(model, entry);
1312
+ }
1313
+ entry.requests++;
1314
+ entry.input_tokens += inputTokens;
1315
+ entry.output_tokens += outputTokens;
1316
+ entry.cost_usd += costUsd;
1317
+ entry.total_latency_ms += durationMs;
1318
+ totalRequests++;
1319
+ totalCostUsd += costUsd;
1320
+ totalTokens += inputTokens + outputTokens;
1321
+ totalLatencyMs += durationMs;
1322
+ }
1323
+ const byModel = Array.from(modelMap.values()).map(m => ({
1324
+ model: m.model,
1325
+ provider: m.provider,
1326
+ requests: m.requests,
1327
+ input_tokens: m.input_tokens,
1328
+ output_tokens: m.output_tokens,
1329
+ cost_usd: m.cost_usd,
1330
+ avg_latency_ms: m.requests > 0 ? Math.round(m.total_latency_ms / m.requests) : 0,
1331
+ })).sort((a, b) => b.requests - a.requests);
1332
+ // Build time series (bucket events into intervals)
1333
+ const bucketCount = Math.min(24, Math.max(6, Math.floor(windowMs / 3600000)));
1334
+ const bucketMs = windowMs / bucketCount;
1335
+ const timeSeries = [];
1336
+ for (let i = 0; i < bucketCount; i++) {
1337
+ const bucketStart = cutoff + i * bucketMs;
1338
+ const bucketEnd = bucketStart + bucketMs;
1339
+ let requests = 0;
1340
+ let cost = 0;
1341
+ let tokens = 0;
1342
+ for (const e of wsEvents) {
1343
+ const t = new Date(e.timestamp).getTime();
1344
+ if (t >= bucketStart && t < bucketEnd) {
1345
+ requests++;
1346
+ cost += e.metadata.cost_usd || 0;
1347
+ tokens += (e.metadata.input_tokens || 0) + (e.metadata.output_tokens || 0);
1348
+ }
1349
+ }
1350
+ timeSeries.push({
1351
+ timestamp: new Date(bucketStart).toISOString(),
1352
+ requests,
1353
+ cost_usd: cost,
1354
+ tokens,
1355
+ });
1356
+ }
1357
+ res.json({
1358
+ summary: {
1359
+ total_requests: totalRequests,
1360
+ total_cost_usd: totalCostUsd,
1361
+ total_tokens: totalTokens,
1362
+ avg_latency_ms: totalRequests > 0 ? Math.round(totalLatencyMs / totalRequests) : 0,
1363
+ },
1364
+ by_model: byModel,
1365
+ time_series: timeSeries,
1366
+ });
1367
+ });
1368
+ // ---------------------------------------------------------------------------
1369
+ // Per-Workspace Rate Limit Configuration
1370
+ // ---------------------------------------------------------------------------
1371
+ // Ensure stores are available on the gateway
1372
+ if (!gateway.getRateLimitConfigStore()) {
1373
+ gateway.setStores({ rateLimitConfigStore: deps.rateLimitConfigStore || new memory_1.InMemoryRateLimitConfigStore() });
1374
+ }
1375
+ const rateLimitConfigStore = gateway.getRateLimitConfigStore();
1376
+ if (!gateway.getBudgetConfigStore()) {
1377
+ gateway.setStores({ budgetConfigStore: deps.budgetConfigStore || new memory_1.InMemoryBudgetConfigStore() });
1378
+ }
1379
+ const budgetConfigStore = gateway.getBudgetConfigStore();
1380
+ router.get('/workspaces/:id/rate-limits', (req, res) => {
1381
+ if (!requireSession(req, res))
1382
+ return;
1383
+ const user = req.sessionUser;
1384
+ const workspaceId = param(req, 'id');
1385
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1386
+ if (!membership) {
1387
+ res.status(403).json({ error: 'Not a member of this workspace' });
1388
+ return;
1389
+ }
1390
+ const wsConfig = rateLimitConfigStore.getByWorkspaceId(workspaceId);
1391
+ const globalConfig = config.rate_limit || { enabled: false, actor_max_per_window: 100, workspace_max_per_window: 500, window_ms: 60000 };
1392
+ const effectiveConfig = wsConfig ? { ...globalConfig, ...wsConfig } : globalConfig;
1393
+ res.json({ config: effectiveConfig, is_custom: !!wsConfig });
1394
+ });
1395
+ router.put('/workspaces/:id/rate-limits', (req, res) => {
1396
+ if (!requireSession(req, res))
1397
+ return;
1398
+ const user = req.sessionUser;
1399
+ const workspaceId = param(req, 'id');
1400
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1401
+ if (!membership) {
1402
+ res.status(403).json({ error: 'Not a member of this workspace' });
1403
+ return;
1404
+ }
1405
+ if (!['owner', 'admin'].includes(membership.role)) {
1406
+ res.status(403).json({ error: 'Only workspace owners and admins can modify rate limits' });
1407
+ return;
1408
+ }
1409
+ const body = req.body;
1410
+ // Validate: all values must be positive numbers if present
1411
+ const errors = [];
1412
+ if (body.actor_max_per_window !== undefined && (typeof body.actor_max_per_window !== 'number' || body.actor_max_per_window <= 0)) {
1413
+ errors.push('actor_max_per_window must be a positive number');
1414
+ }
1415
+ if (body.workspace_max_per_window !== undefined && (typeof body.workspace_max_per_window !== 'number' || body.workspace_max_per_window <= 0)) {
1416
+ errors.push('workspace_max_per_window must be a positive number');
1417
+ }
1418
+ if (body.window_ms !== undefined && (typeof body.window_ms !== 'number' || body.window_ms <= 0)) {
1419
+ errors.push('window_ms must be a positive number');
1420
+ }
1421
+ if (errors.length > 0) {
1422
+ res.status(400).json({ error: 'Validation failed', errors });
1423
+ return;
1424
+ }
1425
+ const clean = {};
1426
+ if (body.actor_max_per_window !== undefined)
1427
+ clean.actor_max_per_window = body.actor_max_per_window;
1428
+ if (body.workspace_max_per_window !== undefined)
1429
+ clean.workspace_max_per_window = body.workspace_max_per_window;
1430
+ if (body.window_ms !== undefined)
1431
+ clean.window_ms = body.window_ms;
1432
+ rateLimitConfigStore.set(workspaceId, clean);
1433
+ gateway.getAuditLogger().log({
1434
+ event_type: 'RATE_LIMIT_CONFIG_UPDATED',
1435
+ tool_call_id: '',
1436
+ task_id: '',
1437
+ workspace_id: workspaceId,
1438
+ actor_id: user.id,
1439
+ tool_name: '',
1440
+ metadata: { config: clean, updated_by: user.id },
1441
+ });
1442
+ const globalConfig = config.rate_limit || { enabled: false, actor_max_per_window: 100, workspace_max_per_window: 500, window_ms: 60000 };
1443
+ res.json({ config: { ...globalConfig, ...clean }, is_custom: true });
1444
+ });
1445
+ router.delete('/workspaces/:id/rate-limits', (req, res) => {
1446
+ if (!requireSession(req, res))
1447
+ return;
1448
+ const user = req.sessionUser;
1449
+ const workspaceId = param(req, 'id');
1450
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1451
+ if (!membership) {
1452
+ res.status(403).json({ error: 'Not a member of this workspace' });
1453
+ return;
1454
+ }
1455
+ if (!['owner', 'admin'].includes(membership.role)) {
1456
+ res.status(403).json({ error: 'Only workspace owners and admins can modify rate limits' });
1457
+ return;
1458
+ }
1459
+ rateLimitConfigStore.delete(workspaceId);
1460
+ gateway.getAuditLogger().log({
1461
+ event_type: 'RATE_LIMIT_CONFIG_RESET',
1462
+ tool_call_id: '',
1463
+ task_id: '',
1464
+ workspace_id: workspaceId,
1465
+ actor_id: user.id,
1466
+ tool_name: '',
1467
+ metadata: { reset_by: user.id },
1468
+ });
1469
+ const globalConfig = config.rate_limit || { enabled: false, actor_max_per_window: 100, workspace_max_per_window: 500, window_ms: 60000 };
1470
+ res.json({ status: 'reset', config: globalConfig, is_custom: false });
1471
+ });
1472
+ // ---------------------------------------------------------------------------
1473
+ // Per-Workspace Budget Configuration
1474
+ // ---------------------------------------------------------------------------
1475
+ router.get('/workspaces/:id/budget-config', (req, res) => {
1476
+ if (!requireSession(req, res))
1477
+ return;
1478
+ const user = req.sessionUser;
1479
+ const workspaceId = param(req, 'id');
1480
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1481
+ if (!membership) {
1482
+ res.status(403).json({ error: 'Not a member of this workspace' });
1483
+ return;
1484
+ }
1485
+ const wsConfig = budgetConfigStore.getByWorkspaceId(workspaceId);
1486
+ const effectiveConfig = wsConfig ? { ...config.budget, ...wsConfig } : config.budget;
1487
+ res.json({ config: effectiveConfig, is_custom: !!wsConfig });
1488
+ });
1489
+ router.put('/workspaces/:id/budget-config', (req, res) => {
1490
+ if (!requireSession(req, res))
1491
+ return;
1492
+ const user = req.sessionUser;
1493
+ const workspaceId = param(req, 'id');
1494
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1495
+ if (!membership) {
1496
+ res.status(403).json({ error: 'Not a member of this workspace' });
1497
+ return;
1498
+ }
1499
+ if (!['owner', 'admin'].includes(membership.role)) {
1500
+ res.status(403).json({ error: 'Only workspace owners and admins can modify budget config' });
1501
+ return;
1502
+ }
1503
+ const body = req.body;
1504
+ // Validate: all values must be positive numbers if present
1505
+ const errors = [];
1506
+ const numFields = [
1507
+ 'task_budget_usd', 'user_daily_budget_usd', 'user_monthly_budget_usd',
1508
+ 'workspace_daily_budget_usd', 'workspace_monthly_budget_usd',
1509
+ 'max_steps_per_task', 'max_retries_per_call', 'max_wall_clock_ms',
1510
+ ];
1511
+ for (const field of numFields) {
1512
+ const val = body[field];
1513
+ if (val !== undefined && (typeof val !== 'number' || val <= 0)) {
1514
+ errors.push(`${field} must be a positive number`);
1515
+ }
1516
+ }
1517
+ if (errors.length > 0) {
1518
+ res.status(400).json({ error: 'Validation failed', errors });
1519
+ return;
1520
+ }
1521
+ const clean = {};
1522
+ for (const field of numFields) {
1523
+ if (body[field] !== undefined)
1524
+ clean[field] = body[field];
1525
+ }
1526
+ budgetConfigStore.set(workspaceId, clean);
1527
+ gateway.getAuditLogger().log({
1528
+ event_type: 'BUDGET_CONFIG_UPDATED',
1529
+ tool_call_id: '',
1530
+ task_id: '',
1531
+ workspace_id: workspaceId,
1532
+ actor_id: user.id,
1533
+ tool_name: '',
1534
+ metadata: { config: clean, updated_by: user.id },
1535
+ });
1536
+ res.json({ config: { ...config.budget, ...clean }, is_custom: true });
1537
+ });
1538
+ router.delete('/workspaces/:id/budget-config', (req, res) => {
1539
+ if (!requireSession(req, res))
1540
+ return;
1541
+ const user = req.sessionUser;
1542
+ const workspaceId = param(req, 'id');
1543
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
1544
+ if (!membership) {
1545
+ res.status(403).json({ error: 'Not a member of this workspace' });
1546
+ return;
1547
+ }
1548
+ if (!['owner', 'admin'].includes(membership.role)) {
1549
+ res.status(403).json({ error: 'Only workspace owners and admins can modify budget config' });
1550
+ return;
1551
+ }
1552
+ budgetConfigStore.delete(workspaceId);
1553
+ gateway.getAuditLogger().log({
1554
+ event_type: 'BUDGET_CONFIG_RESET',
1555
+ tool_call_id: '',
1556
+ task_id: '',
1557
+ workspace_id: workspaceId,
1558
+ actor_id: user.id,
1559
+ tool_name: '',
1560
+ metadata: { reset_by: user.id },
1561
+ });
1562
+ res.json({ status: 'reset', config: config.budget, is_custom: false });
1563
+ });
1564
+ return router;
1565
+ }
1566
+ //# sourceMappingURL=routes.js.map