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,1813 @@
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
+ const http = __importStar(require("http"));
37
+ const opa_engine_1 = require("../../src/policy/opa-engine");
38
+ // ---------------------------------------------------------------------------
39
+ // Helpers
40
+ // ---------------------------------------------------------------------------
41
+ /** Build a minimal valid ToolCall for testing. */
42
+ function buildToolCall(overrides = {}) {
43
+ return {
44
+ tool_call_id: 'tc-opa-001',
45
+ task_id: 'task-opa-001',
46
+ workspace_id: 'ws-default',
47
+ actor: {
48
+ type: 'agent',
49
+ id: 'agent-1',
50
+ ...(overrides.actor || {}),
51
+ },
52
+ source: {
53
+ platform: 'langgraph',
54
+ ...(overrides.source || {}),
55
+ },
56
+ tool: {
57
+ name: 'http.request',
58
+ capability: 'read',
59
+ ...(overrides.tool || {}),
60
+ },
61
+ args: {
62
+ method: 'GET',
63
+ url: 'https://api.github.com/repos',
64
+ ...(overrides.args || {}),
65
+ },
66
+ context: overrides.context,
67
+ constraints: overrides.constraints,
68
+ timestamp: overrides.timestamp,
69
+ };
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Remote OPA evaluation (mock HTTP server)
73
+ // ---------------------------------------------------------------------------
74
+ describe('OPAEngine - Remote evaluation', () => {
75
+ let opaServer;
76
+ let opaPort;
77
+ let serverResponse;
78
+ let lastRequestBody;
79
+ let serverShouldTimeout;
80
+ let serverShouldError;
81
+ let serverShouldReturnInvalidJson;
82
+ beforeAll((done) => {
83
+ opaServer = http.createServer((req, res) => {
84
+ if (serverShouldTimeout) {
85
+ // Do nothing; let the client timeout
86
+ return;
87
+ }
88
+ if (serverShouldError) {
89
+ res.destroy();
90
+ return;
91
+ }
92
+ if (serverShouldReturnInvalidJson) {
93
+ res.writeHead(200, { 'Content-Type': 'application/json' });
94
+ res.end('not valid json{{{');
95
+ return;
96
+ }
97
+ let body = '';
98
+ req.on('data', (chunk) => body += chunk);
99
+ req.on('end', () => {
100
+ lastRequestBody = JSON.parse(body);
101
+ res.writeHead(200, { 'Content-Type': 'application/json' });
102
+ res.end(JSON.stringify(serverResponse));
103
+ });
104
+ });
105
+ opaServer.listen(0, '127.0.0.1', () => {
106
+ opaPort = opaServer.address().port;
107
+ done();
108
+ });
109
+ });
110
+ afterAll((done) => {
111
+ opaServer.close(done);
112
+ });
113
+ beforeEach(() => {
114
+ serverResponse = { result: { decision: 'allow' } };
115
+ lastRequestBody = null;
116
+ serverShouldTimeout = false;
117
+ serverShouldError = false;
118
+ serverShouldReturnInvalidJson = false;
119
+ });
120
+ test('should evaluate allow decision from OPA server', async () => {
121
+ serverResponse = {
122
+ result: {
123
+ decision: 'allow',
124
+ rule_id: 'opa_allow_reads',
125
+ rule_name: 'Allow read operations',
126
+ reasons: ['Read operations are permitted'],
127
+ },
128
+ };
129
+ const engine = new opa_engine_1.OPAEngine({
130
+ enabled: true,
131
+ server_url: `http://127.0.0.1:${opaPort}`,
132
+ });
133
+ const result = await engine.evaluate(buildToolCall());
134
+ expect(result.decision).toBe('allow');
135
+ expect(result.rule_id).toBe('opa_allow_reads');
136
+ expect(result.rule_name).toBe('Allow read operations');
137
+ expect(result.reasons).toEqual(['Read operations are permitted']);
138
+ });
139
+ test('should evaluate deny decision from OPA server', async () => {
140
+ serverResponse = {
141
+ result: {
142
+ decision: 'deny',
143
+ rule_id: 'opa_deny_writes',
144
+ rule_name: 'Deny write operations',
145
+ reasons: ['Write operations are blocked in this environment'],
146
+ },
147
+ };
148
+ const engine = new opa_engine_1.OPAEngine({
149
+ enabled: true,
150
+ server_url: `http://127.0.0.1:${opaPort}`,
151
+ });
152
+ const tc = buildToolCall({ tool: { name: 'http.request', capability: 'write' } });
153
+ const result = await engine.evaluate(tc);
154
+ expect(result.decision).toBe('deny');
155
+ expect(result.rule_id).toBe('opa_deny_writes');
156
+ expect(result.reasons).toContain('Write operations are blocked in this environment');
157
+ });
158
+ test('should evaluate transform decision with transformations', async () => {
159
+ serverResponse = {
160
+ result: {
161
+ decision: 'transform',
162
+ rule_id: 'opa_strip_auth',
163
+ rule_name: 'Strip auth headers',
164
+ reasons: ['Auth headers must be stripped for external calls'],
165
+ transformations: [
166
+ { type: 'strip_header', target: 'Authorization' },
167
+ ],
168
+ },
169
+ };
170
+ const engine = new opa_engine_1.OPAEngine({
171
+ enabled: true,
172
+ server_url: `http://127.0.0.1:${opaPort}`,
173
+ });
174
+ const result = await engine.evaluate(buildToolCall());
175
+ expect(result.decision).toBe('transform');
176
+ expect(result.transformations).toHaveLength(1);
177
+ expect(result.transformations[0]).toEqual({ type: 'strip_header', target: 'Authorization' });
178
+ });
179
+ test('should evaluate require_approval decision with approval info', async () => {
180
+ serverResponse = {
181
+ result: {
182
+ decision: 'require_approval',
183
+ rule_id: 'opa_admin_approval',
184
+ rule_name: 'Admin operations need approval',
185
+ reasons: ['Admin capability requires human approval'],
186
+ approval: { scope: 'security', ttl_seconds: 7200, reason: 'Admin operation detected' },
187
+ },
188
+ };
189
+ const engine = new opa_engine_1.OPAEngine({
190
+ enabled: true,
191
+ server_url: `http://127.0.0.1:${opaPort}`,
192
+ });
193
+ const tc = buildToolCall({ tool: { name: 'http.request', capability: 'admin' } });
194
+ const result = await engine.evaluate(tc);
195
+ expect(result.decision).toBe('require_approval');
196
+ expect(result.approval).toEqual({ scope: 'security', ttl_seconds: 7200, reason: 'Admin operation detected' });
197
+ });
198
+ test('should send correct input format to OPA server', async () => {
199
+ serverResponse = { result: { decision: 'allow' } };
200
+ const engine = new opa_engine_1.OPAEngine({
201
+ enabled: true,
202
+ server_url: `http://127.0.0.1:${opaPort}`,
203
+ });
204
+ const tc = buildToolCall({
205
+ tool: { name: 'http.get', capability: 'read', version: '2.0' },
206
+ context: { purpose: 'test', labels: ['ci'] },
207
+ });
208
+ await engine.evaluate(tc);
209
+ expect(lastRequestBody).toBeDefined();
210
+ expect(lastRequestBody.input).toBeDefined();
211
+ expect(lastRequestBody.input.tool_call_id).toBe('tc-opa-001');
212
+ expect(lastRequestBody.input.task_id).toBe('task-opa-001');
213
+ expect(lastRequestBody.input.tool.name).toBe('http.get');
214
+ expect(lastRequestBody.input.tool.capability).toBe('read');
215
+ expect(lastRequestBody.input.context.labels).toEqual(['ci']);
216
+ });
217
+ test('should use custom policy_path', async () => {
218
+ serverResponse = { result: { decision: 'allow' } };
219
+ const engine = new opa_engine_1.OPAEngine({
220
+ enabled: true,
221
+ server_url: `http://127.0.0.1:${opaPort}`,
222
+ policy_path: 'v1/data/custom/path',
223
+ });
224
+ await engine.evaluate(buildToolCall());
225
+ // If the server received the request successfully, it means the correct path was used
226
+ expect(lastRequestBody).toBeDefined();
227
+ });
228
+ test('should fall back when OPA server times out', async () => {
229
+ serverShouldTimeout = true;
230
+ const engine = new opa_engine_1.OPAEngine({
231
+ enabled: true,
232
+ server_url: `http://127.0.0.1:${opaPort}`,
233
+ timeout_ms: 200,
234
+ fallback_decision: 'deny',
235
+ });
236
+ const result = await engine.evaluate(buildToolCall());
237
+ expect(result.decision).toBe('deny');
238
+ expect(result.rule_id).toBe('opa_fallback');
239
+ expect(result.reasons[0]).toContain('OPA server error');
240
+ });
241
+ test('should fall back when OPA server returns error', async () => {
242
+ serverShouldError = true;
243
+ const engine = new opa_engine_1.OPAEngine({
244
+ enabled: true,
245
+ server_url: `http://127.0.0.1:${opaPort}`,
246
+ fallback_decision: 'deny',
247
+ });
248
+ const result = await engine.evaluate(buildToolCall());
249
+ expect(result.decision).toBe('deny');
250
+ expect(result.rule_id).toBe('opa_fallback');
251
+ expect(result.reasons[0]).toContain('OPA server error');
252
+ });
253
+ test('should fall back when OPA returns invalid JSON', async () => {
254
+ serverShouldReturnInvalidJson = true;
255
+ const engine = new opa_engine_1.OPAEngine({
256
+ enabled: true,
257
+ server_url: `http://127.0.0.1:${opaPort}`,
258
+ fallback_decision: 'deny',
259
+ });
260
+ const result = await engine.evaluate(buildToolCall());
261
+ expect(result.decision).toBe('deny');
262
+ expect(result.rule_id).toBe('opa_fallback');
263
+ expect(result.reasons[0]).toContain('OPA server error');
264
+ });
265
+ test('should fall back when OPA returns empty result', async () => {
266
+ serverResponse = {};
267
+ const engine = new opa_engine_1.OPAEngine({
268
+ enabled: true,
269
+ server_url: `http://127.0.0.1:${opaPort}`,
270
+ fallback_decision: 'deny',
271
+ });
272
+ const result = await engine.evaluate(buildToolCall());
273
+ expect(result.decision).toBe('deny');
274
+ expect(result.rule_id).toBe('opa_fallback');
275
+ expect(result.reasons[0]).toBe('OPA returned empty result');
276
+ });
277
+ test('should fall back when OPA returns result without decision', async () => {
278
+ serverResponse = { result: { rule_id: 'some_rule' } };
279
+ const engine = new opa_engine_1.OPAEngine({
280
+ enabled: true,
281
+ server_url: `http://127.0.0.1:${opaPort}`,
282
+ fallback_decision: 'deny',
283
+ });
284
+ const result = await engine.evaluate(buildToolCall());
285
+ // normalizeDecision handles undefined → fallback
286
+ expect(result.decision).toBe('deny');
287
+ });
288
+ test('should use allow as fallback decision when configured', async () => {
289
+ serverShouldError = true;
290
+ const engine = new opa_engine_1.OPAEngine({
291
+ enabled: true,
292
+ server_url: `http://127.0.0.1:${opaPort}`,
293
+ fallback_decision: 'allow',
294
+ });
295
+ const result = await engine.evaluate(buildToolCall());
296
+ expect(result.decision).toBe('allow');
297
+ expect(result.rule_id).toBe('opa_fallback');
298
+ });
299
+ test('should return OPA server unreachable fallback for invalid URL', async () => {
300
+ const engine = new opa_engine_1.OPAEngine({
301
+ enabled: true,
302
+ server_url: 'http://127.0.0.1:1', // Port 1 is unlikely to have anything listening
303
+ timeout_ms: 500,
304
+ fallback_decision: 'deny',
305
+ });
306
+ const result = await engine.evaluate(buildToolCall());
307
+ expect(result.decision).toBe('deny');
308
+ expect(result.rule_id).toBe('opa_fallback');
309
+ });
310
+ test('should use default values for reasons when OPA provides a string', async () => {
311
+ serverResponse = {
312
+ result: {
313
+ decision: 'deny',
314
+ reasons: 'single reason string',
315
+ },
316
+ };
317
+ const engine = new opa_engine_1.OPAEngine({
318
+ enabled: true,
319
+ server_url: `http://127.0.0.1:${opaPort}`,
320
+ });
321
+ const result = await engine.evaluate(buildToolCall());
322
+ expect(result.reasons).toEqual(['single reason string']);
323
+ });
324
+ test('should provide default reasons when OPA returns none', async () => {
325
+ serverResponse = {
326
+ result: {
327
+ decision: 'allow',
328
+ },
329
+ };
330
+ const engine = new opa_engine_1.OPAEngine({
331
+ enabled: true,
332
+ server_url: `http://127.0.0.1:${opaPort}`,
333
+ });
334
+ const result = await engine.evaluate(buildToolCall());
335
+ expect(result.reasons).toEqual(['OPA decision: allow']);
336
+ });
337
+ test('should provide default rule_id and rule_name when OPA returns none', async () => {
338
+ serverResponse = {
339
+ result: {
340
+ decision: 'allow',
341
+ },
342
+ };
343
+ const engine = new opa_engine_1.OPAEngine({
344
+ enabled: true,
345
+ server_url: `http://127.0.0.1:${opaPort}`,
346
+ });
347
+ const result = await engine.evaluate(buildToolCall());
348
+ expect(result.rule_id).toBe('opa_policy');
349
+ expect(result.rule_name).toBe('OPA policy evaluation');
350
+ });
351
+ });
352
+ // ---------------------------------------------------------------------------
353
+ // Local Rego evaluation
354
+ // ---------------------------------------------------------------------------
355
+ describe('OPAEngine - Local Rego evaluation', () => {
356
+ test('should deny by default with basic deny policy', async () => {
357
+ const policy = `
358
+ package palaryn.policy
359
+ default decision = "deny"
360
+ `;
361
+ const engine = new opa_engine_1.OPAEngine({
362
+ enabled: true,
363
+ rego_policy: policy,
364
+ });
365
+ const result = await engine.evaluate(buildToolCall());
366
+ expect(result.decision).toBe('deny');
367
+ expect(result.rule_id).toBe('opa_local');
368
+ });
369
+ test('should allow by default with basic allow policy', async () => {
370
+ const policy = `
371
+ package palaryn.policy
372
+ default decision = "allow"
373
+ `;
374
+ const engine = new opa_engine_1.OPAEngine({
375
+ enabled: true,
376
+ rego_policy: policy,
377
+ });
378
+ const result = await engine.evaluate(buildToolCall());
379
+ expect(result.decision).toBe('allow');
380
+ });
381
+ test('should evaluate allow rule with capability condition', async () => {
382
+ const policy = `
383
+ package palaryn.policy
384
+ default decision = "deny"
385
+ decision = "allow" {
386
+ input.tool.capability == "read"
387
+ }
388
+ `;
389
+ const engine = new opa_engine_1.OPAEngine({
390
+ enabled: true,
391
+ rego_policy: policy,
392
+ });
393
+ // Read capability should be allowed
394
+ const readResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'read' } }));
395
+ expect(readResult.decision).toBe('allow');
396
+ // Write capability should be denied
397
+ const writeResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'write' } }));
398
+ expect(writeResult.decision).toBe('deny');
399
+ });
400
+ test('should evaluate deny rule for specific tool name', async () => {
401
+ const policy = `
402
+ package palaryn.policy
403
+ default decision = "allow"
404
+ decision = "deny" {
405
+ input.tool.name == "dangerous"
406
+ }
407
+ `;
408
+ const engine = new opa_engine_1.OPAEngine({
409
+ enabled: true,
410
+ rego_policy: policy,
411
+ });
412
+ // Normal tool should be allowed
413
+ const normalResult = await engine.evaluate(buildToolCall());
414
+ expect(normalResult.decision).toBe('allow');
415
+ // Dangerous tool should be denied
416
+ const dangerousResult = await engine.evaluate(buildToolCall({ tool: { name: 'dangerous', capability: 'read' } }));
417
+ expect(dangerousResult.decision).toBe('deny');
418
+ });
419
+ test('should handle multiple rules (last match wins)', async () => {
420
+ const policy = `
421
+ package palaryn.policy
422
+ default decision = "deny"
423
+ decision = "allow" {
424
+ input.tool.capability == "read"
425
+ }
426
+ decision = "deny" {
427
+ input.tool.name == "http.dangerous"
428
+ }
429
+ `;
430
+ const engine = new opa_engine_1.OPAEngine({
431
+ enabled: true,
432
+ rego_policy: policy,
433
+ });
434
+ // A read tool that isn't dangerous should be allowed
435
+ const readResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'read' } }));
436
+ expect(readResult.decision).toBe('allow');
437
+ // A dangerous tool with read capability -- both rules match, last wins (deny)
438
+ const dangerousRead = await engine.evaluate(buildToolCall({ tool: { name: 'http.dangerous', capability: 'read' } }));
439
+ expect(dangerousRead.decision).toBe('deny');
440
+ });
441
+ test('should collect reasons from reasons rules', async () => {
442
+ const policy = `
443
+ package palaryn.policy
444
+ default decision = "deny"
445
+ decision = "deny" {
446
+ input.tool.capability == "admin"
447
+ }
448
+ reasons[reason] {
449
+ input.tool.capability == "admin"
450
+ reason := "Admin capability requires approval"
451
+ }
452
+ `;
453
+ const engine = new opa_engine_1.OPAEngine({
454
+ enabled: true,
455
+ rego_policy: policy,
456
+ });
457
+ const result = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'admin' } }));
458
+ expect(result.decision).toBe('deny');
459
+ expect(result.reasons).toContain('Admin capability requires approval');
460
+ });
461
+ test('should handle inequality conditions', async () => {
462
+ const policy = `
463
+ package palaryn.policy
464
+ default decision = "deny"
465
+ decision = "allow" {
466
+ input.tool.capability != "admin"
467
+ }
468
+ `;
469
+ const engine = new opa_engine_1.OPAEngine({
470
+ enabled: true,
471
+ rego_policy: policy,
472
+ });
473
+ const readResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'read' } }));
474
+ expect(readResult.decision).toBe('allow');
475
+ const adminResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'admin' } }));
476
+ expect(adminResult.decision).toBe('deny');
477
+ });
478
+ test('should handle AND logic (multiple conditions in a rule)', async () => {
479
+ const policy = `
480
+ package palaryn.policy
481
+ default decision = "deny"
482
+ decision = "allow" {
483
+ input.tool.capability == "read"
484
+ input.source.platform == "langgraph"
485
+ }
486
+ `;
487
+ const engine = new opa_engine_1.OPAEngine({
488
+ enabled: true,
489
+ rego_policy: policy,
490
+ });
491
+ // Both conditions met
492
+ const result1 = await engine.evaluate(buildToolCall({
493
+ tool: { name: 'http.request', capability: 'read' },
494
+ source: { platform: 'langgraph' },
495
+ }));
496
+ expect(result1.decision).toBe('allow');
497
+ // Only one condition met
498
+ const result2 = await engine.evaluate(buildToolCall({
499
+ tool: { name: 'http.request', capability: 'read' },
500
+ source: { platform: 'n8n' },
501
+ }));
502
+ expect(result2.decision).toBe('deny');
503
+ });
504
+ test('should handle actor type conditions', async () => {
505
+ const policy = `
506
+ package palaryn.policy
507
+ default decision = "deny"
508
+ decision = "allow" {
509
+ input.actor.type == "user"
510
+ }
511
+ `;
512
+ const engine = new opa_engine_1.OPAEngine({
513
+ enabled: true,
514
+ rego_policy: policy,
515
+ });
516
+ const agentResult = await engine.evaluate(buildToolCall({ actor: { type: 'agent', id: 'a1' } }));
517
+ expect(agentResult.decision).toBe('deny');
518
+ const userResult = await engine.evaluate(buildToolCall({ actor: { type: 'user', id: 'u1' } }));
519
+ expect(userResult.decision).toBe('allow');
520
+ });
521
+ test('should handle workspace_id conditions', async () => {
522
+ const policy = `
523
+ package palaryn.policy
524
+ default decision = "deny"
525
+ decision = "allow" {
526
+ input.workspace_id == "ws-allowed"
527
+ }
528
+ `;
529
+ const engine = new opa_engine_1.OPAEngine({
530
+ enabled: true,
531
+ rego_policy: policy,
532
+ });
533
+ const tc1 = {
534
+ ...buildToolCall(),
535
+ workspace_id: 'ws-allowed',
536
+ };
537
+ const result1 = await engine.evaluate(tc1);
538
+ expect(result1.decision).toBe('allow');
539
+ const tc2 = {
540
+ ...buildToolCall(),
541
+ workspace_id: 'ws-blocked',
542
+ };
543
+ const result2 = await engine.evaluate(tc2);
544
+ expect(result2.decision).toBe('deny');
545
+ });
546
+ test('should handle startswith function', async () => {
547
+ const policy = `
548
+ package palaryn.policy
549
+ default decision = "deny"
550
+ decision = "allow" {
551
+ startswith(input.tool.name, "http.")
552
+ }
553
+ `;
554
+ const engine = new opa_engine_1.OPAEngine({
555
+ enabled: true,
556
+ rego_policy: policy,
557
+ });
558
+ const httpResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'read' } }));
559
+ expect(httpResult.decision).toBe('allow');
560
+ const slackResult = await engine.evaluate(buildToolCall({ tool: { name: 'slack.post', capability: 'write' } }));
561
+ expect(slackResult.decision).toBe('deny');
562
+ });
563
+ test('should handle endswith function', async () => {
564
+ const policy = `
565
+ package palaryn.policy
566
+ default decision = "deny"
567
+ decision = "allow" {
568
+ endswith(input.args.url, "/repos")
569
+ }
570
+ `;
571
+ const engine = new opa_engine_1.OPAEngine({
572
+ enabled: true,
573
+ rego_policy: policy,
574
+ });
575
+ const matchResult = await engine.evaluate(buildToolCall({ args: { url: 'https://api.github.com/repos' } }));
576
+ expect(matchResult.decision).toBe('allow');
577
+ const noMatchResult = await engine.evaluate(buildToolCall({ args: { url: 'https://api.github.com/users' } }));
578
+ expect(noMatchResult.decision).toBe('deny');
579
+ });
580
+ test('should handle contains function', async () => {
581
+ const policy = `
582
+ package palaryn.policy
583
+ default decision = "deny"
584
+ decision = "allow" {
585
+ contains(input.args.url, "github.com")
586
+ }
587
+ `;
588
+ const engine = new opa_engine_1.OPAEngine({
589
+ enabled: true,
590
+ rego_policy: policy,
591
+ });
592
+ const githubResult = await engine.evaluate(buildToolCall({ args: { url: 'https://api.github.com/repos' } }));
593
+ expect(githubResult.decision).toBe('allow');
594
+ const otherResult = await engine.evaluate(buildToolCall({ args: { url: 'https://api.gitlab.com/repos' } }));
595
+ expect(otherResult.decision).toBe('deny');
596
+ });
597
+ test('should handle truthy check condition', async () => {
598
+ const policy = `
599
+ package palaryn.policy
600
+ default decision = "deny"
601
+ decision = "allow" {
602
+ input.context.purpose
603
+ }
604
+ `;
605
+ const engine = new opa_engine_1.OPAEngine({
606
+ enabled: true,
607
+ rego_policy: policy,
608
+ });
609
+ const withPurpose = await engine.evaluate(buildToolCall({ context: { purpose: 'testing' } }));
610
+ expect(withPurpose.decision).toBe('allow');
611
+ const withoutPurpose = await engine.evaluate(buildToolCall());
612
+ expect(withoutPurpose.decision).toBe('deny');
613
+ });
614
+ test('should handle not condition', async () => {
615
+ const policy = `
616
+ package palaryn.policy
617
+ default decision = "deny"
618
+ decision = "allow" {
619
+ not input.constraints.max_cost_usd
620
+ }
621
+ `;
622
+ const engine = new opa_engine_1.OPAEngine({
623
+ enabled: true,
624
+ rego_policy: policy,
625
+ });
626
+ const noConstraints = await engine.evaluate(buildToolCall());
627
+ expect(noConstraints.decision).toBe('allow');
628
+ const withConstraints = await engine.evaluate(buildToolCall({ constraints: { max_cost_usd: 5 } }));
629
+ expect(withConstraints.decision).toBe('deny');
630
+ });
631
+ test('should handle custom default rule_id and rule_name', async () => {
632
+ const policy = `
633
+ package palaryn.policy
634
+ default decision = "allow"
635
+ default rule_id = "custom_opa"
636
+ default rule_name = "Custom OPA Policy"
637
+ `;
638
+ const engine = new opa_engine_1.OPAEngine({
639
+ enabled: true,
640
+ rego_policy: policy,
641
+ });
642
+ const result = await engine.evaluate(buildToolCall());
643
+ expect(result.decision).toBe('allow');
644
+ expect(result.rule_id).toBe('custom_opa');
645
+ expect(result.rule_name).toBe('Custom OPA Policy');
646
+ });
647
+ test('should provide default reason when no reasons rules match', async () => {
648
+ const policy = `
649
+ package palaryn.policy
650
+ default decision = "allow"
651
+ `;
652
+ const engine = new opa_engine_1.OPAEngine({
653
+ enabled: true,
654
+ rego_policy: policy,
655
+ });
656
+ const result = await engine.evaluate(buildToolCall());
657
+ expect(result.reasons).toEqual(['OPA local decision: allow']);
658
+ });
659
+ test('should handle require_approval decision locally', async () => {
660
+ const policy = `
661
+ package palaryn.policy
662
+ default decision = "deny"
663
+ decision = "require_approval" {
664
+ input.tool.capability == "delete"
665
+ }
666
+ `;
667
+ const engine = new opa_engine_1.OPAEngine({
668
+ enabled: true,
669
+ rego_policy: policy,
670
+ });
671
+ const result = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'delete' } }));
672
+ expect(result.decision).toBe('require_approval');
673
+ });
674
+ test('should handle transform decision locally', async () => {
675
+ const policy = `
676
+ package palaryn.policy
677
+ default decision = "deny"
678
+ decision = "transform" {
679
+ input.tool.capability == "write"
680
+ }
681
+ `;
682
+ const engine = new opa_engine_1.OPAEngine({
683
+ enabled: true,
684
+ rego_policy: policy,
685
+ });
686
+ const result = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'write' } }));
687
+ expect(result.decision).toBe('transform');
688
+ });
689
+ test('should skip comment lines in Rego policy', async () => {
690
+ const policy = `
691
+ package palaryn.policy
692
+ # This is a comment
693
+ default decision = "allow"
694
+ # Another comment
695
+ decision = "deny" {
696
+ input.tool.name == "blocked"
697
+ }
698
+ `;
699
+ const engine = new opa_engine_1.OPAEngine({
700
+ enabled: true,
701
+ rego_policy: policy,
702
+ });
703
+ const result = await engine.evaluate(buildToolCall());
704
+ expect(result.decision).toBe('allow');
705
+ });
706
+ test('should fall back on Rego evaluation error', async () => {
707
+ // Test with empty rego_policy (edge case)
708
+ const engine = new opa_engine_1.OPAEngine({
709
+ enabled: true,
710
+ rego_policy: '',
711
+ fallback_decision: 'deny',
712
+ });
713
+ const result = await engine.evaluate(buildToolCall());
714
+ // Empty policy = no defaults, so fallback to normalizeDecision(undefined) = 'deny'
715
+ expect(result.decision).toBe('deny');
716
+ });
717
+ test('should handle boolean comparison in conditions', async () => {
718
+ const policy = `
719
+ package palaryn.policy
720
+ default decision = "deny"
721
+ decision = "allow" {
722
+ input.tool.capability == "read"
723
+ }
724
+ `;
725
+ const engine = new opa_engine_1.OPAEngine({
726
+ enabled: true,
727
+ rego_policy: policy,
728
+ });
729
+ const result = await engine.evaluate(buildToolCall({ tool: { name: 'test', capability: 'read' } }));
730
+ expect(result.decision).toBe('allow');
731
+ });
732
+ });
733
+ // ---------------------------------------------------------------------------
734
+ // Decision normalization
735
+ // ---------------------------------------------------------------------------
736
+ describe('OPAEngine - Decision normalization', () => {
737
+ let engine;
738
+ beforeEach(() => {
739
+ engine = new opa_engine_1.OPAEngine({
740
+ enabled: true,
741
+ fallback_decision: 'deny',
742
+ });
743
+ });
744
+ test('should pass through valid decisions', () => {
745
+ expect(engine.normalizeDecision('allow')).toBe('allow');
746
+ expect(engine.normalizeDecision('deny')).toBe('deny');
747
+ expect(engine.normalizeDecision('transform')).toBe('transform');
748
+ expect(engine.normalizeDecision('require_approval')).toBe('require_approval');
749
+ });
750
+ test('should handle case-insensitive decisions', () => {
751
+ expect(engine.normalizeDecision('ALLOW')).toBe('allow');
752
+ expect(engine.normalizeDecision('DENY')).toBe('deny');
753
+ expect(engine.normalizeDecision('Transform')).toBe('transform');
754
+ expect(engine.normalizeDecision('REQUIRE_APPROVAL')).toBe('require_approval');
755
+ });
756
+ test('should map common OPA boolean patterns', () => {
757
+ expect(engine.normalizeDecision('true')).toBe('allow');
758
+ expect(engine.normalizeDecision('allowed')).toBe('allow');
759
+ expect(engine.normalizeDecision('false')).toBe('deny');
760
+ expect(engine.normalizeDecision('denied')).toBe('deny');
761
+ });
762
+ test('should return fallback for undefined decision', () => {
763
+ expect(engine.normalizeDecision(undefined)).toBe('deny');
764
+ });
765
+ test('should return fallback for unknown decision strings', () => {
766
+ expect(engine.normalizeDecision('maybe')).toBe('deny');
767
+ expect(engine.normalizeDecision('pending')).toBe('deny');
768
+ expect(engine.normalizeDecision('')).toBe('deny');
769
+ });
770
+ test('should use custom fallback decision', () => {
771
+ const customEngine = new opa_engine_1.OPAEngine({
772
+ enabled: true,
773
+ fallback_decision: 'allow',
774
+ });
775
+ expect(customEngine.normalizeDecision(undefined)).toBe('allow');
776
+ expect(customEngine.normalizeDecision('unknown')).toBe('allow');
777
+ });
778
+ });
779
+ // ---------------------------------------------------------------------------
780
+ // parseOPAResponse
781
+ // ---------------------------------------------------------------------------
782
+ describe('OPAEngine - parseOPAResponse', () => {
783
+ let engine;
784
+ const tc = buildToolCall();
785
+ beforeEach(() => {
786
+ engine = new opa_engine_1.OPAEngine({ enabled: true, fallback_decision: 'deny' });
787
+ });
788
+ test('should parse complete OPA response', () => {
789
+ const response = {
790
+ result: {
791
+ decision: 'allow',
792
+ rule_id: 'rule_1',
793
+ rule_name: 'Rule One',
794
+ reasons: ['Allowed by policy'],
795
+ transformations: [{ type: 'strip_header', target: 'Authorization' }],
796
+ approval: { scope: 'admin', ttl_seconds: 3600 },
797
+ },
798
+ };
799
+ const result = engine.parseOPAResponse(response, tc);
800
+ expect(result.decision).toBe('allow');
801
+ expect(result.rule_id).toBe('rule_1');
802
+ expect(result.rule_name).toBe('Rule One');
803
+ expect(result.reasons).toEqual(['Allowed by policy']);
804
+ expect(result.transformations).toHaveLength(1);
805
+ expect(result.approval).toEqual({ scope: 'admin', ttl_seconds: 3600 });
806
+ });
807
+ test('should handle null response', () => {
808
+ const result = engine.parseOPAResponse(null, tc);
809
+ expect(result.decision).toBe('deny');
810
+ expect(result.rule_id).toBe('opa_fallback');
811
+ });
812
+ test('should handle response with no result', () => {
813
+ const result = engine.parseOPAResponse({}, tc);
814
+ expect(result.decision).toBe('deny');
815
+ expect(result.rule_id).toBe('opa_fallback');
816
+ });
817
+ test('should handle response with partial result', () => {
818
+ const response = {
819
+ result: {
820
+ decision: 'deny',
821
+ },
822
+ };
823
+ const result = engine.parseOPAResponse(response, tc);
824
+ expect(result.decision).toBe('deny');
825
+ expect(result.rule_id).toBe('opa_policy');
826
+ expect(result.rule_name).toBe('OPA policy evaluation');
827
+ expect(result.reasons).toEqual(['OPA decision: deny']);
828
+ expect(result.transformations).toBeUndefined();
829
+ });
830
+ });
831
+ // ---------------------------------------------------------------------------
832
+ // buildInput
833
+ // ---------------------------------------------------------------------------
834
+ describe('OPAEngine - buildInput', () => {
835
+ test('should convert ToolCall to OPA input format', () => {
836
+ const engine = new opa_engine_1.OPAEngine({ enabled: true });
837
+ const tc = buildToolCall({
838
+ tool: { name: 'http.get', capability: 'read', version: '1.0' },
839
+ context: { purpose: 'testing', labels: ['ci', 'automated'] },
840
+ constraints: { max_cost_usd: 1.0, timeout_ms: 5000 },
841
+ });
842
+ tc.timestamp = '2025-01-01T00:00:00Z';
843
+ const input = engine.buildInput(tc);
844
+ expect(input.tool_call_id).toBe('tc-opa-001');
845
+ expect(input.task_id).toBe('task-opa-001');
846
+ expect(input.workspace_id).toBe('ws-default');
847
+ expect(input.actor.type).toBe('agent');
848
+ expect(input.tool.name).toBe('http.get');
849
+ expect(input.tool.capability).toBe('read');
850
+ expect(input.context.purpose).toBe('testing');
851
+ expect(input.constraints.max_cost_usd).toBe(1.0);
852
+ expect(input.timestamp).toBe('2025-01-01T00:00:00Z');
853
+ });
854
+ test('should handle ToolCall with minimal fields', () => {
855
+ const engine = new opa_engine_1.OPAEngine({ enabled: true });
856
+ const tc = buildToolCall();
857
+ const input = engine.buildInput(tc);
858
+ expect(input.constraints).toBeUndefined();
859
+ expect(input.context).toBeUndefined();
860
+ expect(input.timestamp).toBeUndefined();
861
+ });
862
+ });
863
+ // ---------------------------------------------------------------------------
864
+ // No configuration (fallback)
865
+ // ---------------------------------------------------------------------------
866
+ describe('OPAEngine - No configuration', () => {
867
+ test('should return fallback when neither server_url nor rego_policy is configured', async () => {
868
+ const engine = new opa_engine_1.OPAEngine({
869
+ enabled: true,
870
+ fallback_decision: 'deny',
871
+ });
872
+ const result = await engine.evaluate(buildToolCall());
873
+ expect(result.decision).toBe('deny');
874
+ expect(result.rule_id).toBe('opa_fallback');
875
+ expect(result.reasons[0]).toBe('OPA not configured: no server_url or rego_policy provided');
876
+ });
877
+ test('should return allow fallback when configured', async () => {
878
+ const engine = new opa_engine_1.OPAEngine({
879
+ enabled: true,
880
+ fallback_decision: 'allow',
881
+ });
882
+ const result = await engine.evaluate(buildToolCall());
883
+ expect(result.decision).toBe('allow');
884
+ });
885
+ });
886
+ // ---------------------------------------------------------------------------
887
+ // isAvailable
888
+ // ---------------------------------------------------------------------------
889
+ describe('OPAEngine - isAvailable', () => {
890
+ test('should return true when rego_policy is configured', async () => {
891
+ const engine = new opa_engine_1.OPAEngine({
892
+ enabled: true,
893
+ rego_policy: 'package palaryn.policy\ndefault decision = "allow"',
894
+ });
895
+ const available = await engine.isAvailable();
896
+ expect(available).toBe(true);
897
+ });
898
+ test('should return false when nothing is configured', async () => {
899
+ const engine = new opa_engine_1.OPAEngine({
900
+ enabled: true,
901
+ });
902
+ const available = await engine.isAvailable();
903
+ expect(available).toBe(false);
904
+ });
905
+ test('should return false when server_url is unreachable', async () => {
906
+ const engine = new opa_engine_1.OPAEngine({
907
+ enabled: true,
908
+ server_url: 'http://127.0.0.1:1', // unlikely to be listening
909
+ });
910
+ const available = await engine.isAvailable();
911
+ expect(available).toBe(false);
912
+ });
913
+ test('should return true when server_url is reachable', async () => {
914
+ // Create a quick mock OPA server
915
+ const server = http.createServer((req, res) => {
916
+ res.writeHead(200, { 'Content-Type': 'application/json' });
917
+ res.end(JSON.stringify({ result: {} }));
918
+ });
919
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
920
+ const port = server.address().port;
921
+ const engine = new opa_engine_1.OPAEngine({
922
+ enabled: true,
923
+ server_url: `http://127.0.0.1:${port}`,
924
+ });
925
+ const available = await engine.isAvailable();
926
+ expect(available).toBe(true);
927
+ await new Promise((resolve) => server.close(() => resolve()));
928
+ });
929
+ });
930
+ // ---------------------------------------------------------------------------
931
+ // Circuit breaker (S15)
932
+ // ---------------------------------------------------------------------------
933
+ describe('OPAEngine - Circuit breaker', () => {
934
+ let opaServer;
935
+ let opaPort;
936
+ let requestCount;
937
+ let serverShouldFail;
938
+ beforeAll((done) => {
939
+ opaServer = http.createServer((req, res) => {
940
+ requestCount++;
941
+ if (serverShouldFail) {
942
+ res.destroy();
943
+ return;
944
+ }
945
+ let body = '';
946
+ req.on('data', (chunk) => body += chunk);
947
+ req.on('end', () => {
948
+ res.writeHead(200, { 'Content-Type': 'application/json' });
949
+ res.end(JSON.stringify({ result: { decision: 'allow' } }));
950
+ });
951
+ });
952
+ opaServer.listen(0, '127.0.0.1', () => {
953
+ opaPort = opaServer.address().port;
954
+ done();
955
+ });
956
+ });
957
+ afterAll((done) => {
958
+ opaServer.close(done);
959
+ });
960
+ beforeEach(() => {
961
+ requestCount = 0;
962
+ serverShouldFail = false;
963
+ });
964
+ test('should open circuit after consecutive failures', async () => {
965
+ serverShouldFail = true;
966
+ const engine = new opa_engine_1.OPAEngine({
967
+ enabled: true,
968
+ server_url: `http://127.0.0.1:${opaPort}`,
969
+ fallback_decision: 'deny',
970
+ });
971
+ // 3 consecutive failures should open the circuit
972
+ for (let i = 0; i < 3; i++) {
973
+ await engine.evaluate(buildToolCall());
974
+ }
975
+ expect(engine.getCircuitState().state).toBe('open');
976
+ expect(engine.getCircuitState().failures).toBe(3);
977
+ });
978
+ test('should skip HTTP call when circuit is open', async () => {
979
+ serverShouldFail = true;
980
+ const engine = new opa_engine_1.OPAEngine({
981
+ enabled: true,
982
+ server_url: `http://127.0.0.1:${opaPort}`,
983
+ fallback_decision: 'deny',
984
+ });
985
+ // Trip the circuit
986
+ for (let i = 0; i < 3; i++) {
987
+ await engine.evaluate(buildToolCall());
988
+ }
989
+ const beforeCount = requestCount;
990
+ // Next call should NOT make an HTTP request (circuit open)
991
+ const result = await engine.evaluate(buildToolCall());
992
+ expect(result.decision).toBe('deny');
993
+ expect(result.reasons[0]).toContain('circuit breaker open');
994
+ expect(requestCount).toBe(beforeCount); // no new HTTP calls
995
+ });
996
+ test('should return fallback with circuit breaker reason when open', async () => {
997
+ serverShouldFail = true;
998
+ const engine = new opa_engine_1.OPAEngine({
999
+ enabled: true,
1000
+ server_url: `http://127.0.0.1:${opaPort}`,
1001
+ fallback_decision: 'deny',
1002
+ });
1003
+ // Trip the circuit
1004
+ for (let i = 0; i < 3; i++) {
1005
+ await engine.evaluate(buildToolCall());
1006
+ }
1007
+ const result = await engine.evaluate(buildToolCall());
1008
+ expect(result.decision).toBe('deny');
1009
+ expect(result.rule_id).toBe('opa_fallback');
1010
+ expect(result.reasons[0]).toContain('circuit breaker');
1011
+ });
1012
+ test('should reset circuit on successful call', async () => {
1013
+ const engine = new opa_engine_1.OPAEngine({
1014
+ enabled: true,
1015
+ server_url: `http://127.0.0.1:${opaPort}`,
1016
+ fallback_decision: 'deny',
1017
+ });
1018
+ // Cause some failures (but not enough to trip)
1019
+ serverShouldFail = true;
1020
+ await engine.evaluate(buildToolCall());
1021
+ await engine.evaluate(buildToolCall());
1022
+ expect(engine.getCircuitState().failures).toBe(2);
1023
+ // Now succeed
1024
+ serverShouldFail = false;
1025
+ await engine.evaluate(buildToolCall());
1026
+ expect(engine.getCircuitState().state).toBe('closed');
1027
+ expect(engine.getCircuitState().failures).toBe(0);
1028
+ });
1029
+ test('should expose circuit state via getCircuitState()', () => {
1030
+ const engine = new opa_engine_1.OPAEngine({
1031
+ enabled: true,
1032
+ server_url: `http://127.0.0.1:${opaPort}`,
1033
+ });
1034
+ const state = engine.getCircuitState();
1035
+ expect(state.state).toBe('closed');
1036
+ expect(state.failures).toBe(0);
1037
+ expect(state.lastFailure).toBe(0);
1038
+ });
1039
+ test('should allow resetCircuit() to force close the circuit', async () => {
1040
+ serverShouldFail = true;
1041
+ const engine = new opa_engine_1.OPAEngine({
1042
+ enabled: true,
1043
+ server_url: `http://127.0.0.1:${opaPort}`,
1044
+ fallback_decision: 'deny',
1045
+ });
1046
+ // Trip the circuit
1047
+ for (let i = 0; i < 3; i++) {
1048
+ await engine.evaluate(buildToolCall());
1049
+ }
1050
+ expect(engine.getCircuitState().state).toBe('open');
1051
+ // Admin reset
1052
+ engine.resetCircuit();
1053
+ expect(engine.getCircuitState().state).toBe('closed');
1054
+ expect(engine.getCircuitState().failures).toBe(0);
1055
+ });
1056
+ });
1057
+ // ---------------------------------------------------------------------------
1058
+ // getConfig
1059
+ // ---------------------------------------------------------------------------
1060
+ describe('OPAEngine - getConfig', () => {
1061
+ test('should return the engine configuration with defaults applied', () => {
1062
+ const engine = new opa_engine_1.OPAEngine({
1063
+ enabled: true,
1064
+ server_url: 'http://localhost:8181',
1065
+ });
1066
+ const config = engine.getConfig();
1067
+ expect(config.enabled).toBe(true);
1068
+ expect(config.server_url).toBe('http://localhost:8181');
1069
+ expect(config.policy_path).toBe('v1/data/palaryn/policy');
1070
+ expect(config.timeout_ms).toBe(5000);
1071
+ expect(config.fallback_decision).toBe('deny');
1072
+ expect(config.package_name).toBe('palaryn.policy');
1073
+ });
1074
+ test('should return custom configuration values', () => {
1075
+ const engine = new opa_engine_1.OPAEngine({
1076
+ enabled: true,
1077
+ server_url: 'http://opa.internal:8181',
1078
+ policy_path: 'v1/data/custom/policy',
1079
+ timeout_ms: 10000,
1080
+ fallback_decision: 'allow',
1081
+ package_name: 'custom.policy',
1082
+ });
1083
+ const config = engine.getConfig();
1084
+ expect(config.policy_path).toBe('v1/data/custom/policy');
1085
+ expect(config.timeout_ms).toBe(10000);
1086
+ expect(config.fallback_decision).toBe('allow');
1087
+ expect(config.package_name).toBe('custom.policy');
1088
+ });
1089
+ });
1090
+ // ---------------------------------------------------------------------------
1091
+ // Gateway integration behavior
1092
+ // ---------------------------------------------------------------------------
1093
+ describe('OPAEngine - Gateway integration', () => {
1094
+ test('OPA disabled: existing YAML behavior unchanged', () => {
1095
+ // When OPA is not enabled, the config.policy.opa is undefined,
1096
+ // so the Gateway constructor won't create an OPAEngine.
1097
+ // This test verifies that building a config without OPA still works.
1098
+ const config = {
1099
+ enabled: false,
1100
+ };
1101
+ // The Gateway should not initialize OPA when enabled=false
1102
+ expect(config.enabled).toBe(false);
1103
+ });
1104
+ test('OPA fallback result has rule_id opa_fallback', async () => {
1105
+ const engine = new opa_engine_1.OPAEngine({
1106
+ enabled: true,
1107
+ fallback_decision: 'deny',
1108
+ });
1109
+ const result = await engine.evaluate(buildToolCall());
1110
+ // When OPA is not configured and returns a fallback, gateway.ts checks
1111
+ // for rule_id === 'opa_fallback' to decide whether to fall back to YAML
1112
+ expect(result.rule_id).toBe('opa_fallback');
1113
+ });
1114
+ test('OPA successful result does not have opa_fallback rule_id', async () => {
1115
+ const policy = `
1116
+ package palaryn.policy
1117
+ default decision = "allow"
1118
+ `;
1119
+ const engine = new opa_engine_1.OPAEngine({
1120
+ enabled: true,
1121
+ rego_policy: policy,
1122
+ });
1123
+ const result = await engine.evaluate(buildToolCall());
1124
+ expect(result.rule_id).not.toBe('opa_fallback');
1125
+ expect(result.rule_id).toBe('opa_local');
1126
+ });
1127
+ });
1128
+ // ---------------------------------------------------------------------------
1129
+ // evaluateRegoSubset (direct method testing)
1130
+ // ---------------------------------------------------------------------------
1131
+ describe('OPAEngine - evaluateRegoSubset', () => {
1132
+ let engine;
1133
+ beforeEach(() => {
1134
+ engine = new opa_engine_1.OPAEngine({ enabled: true, rego_policy: '' });
1135
+ });
1136
+ test('should handle empty policy', () => {
1137
+ const result = engine.evaluateRegoSubset('', { tool: { name: 'test', capability: 'read' } });
1138
+ // No defaults, no rules -- normalizeDecision(undefined) = fallback = 'deny'
1139
+ expect(result.decision).toBe('deny');
1140
+ });
1141
+ test('should handle policy with only package declaration', () => {
1142
+ const result = engine.evaluateRegoSubset('package palaryn.policy', { tool: { name: 'test' } });
1143
+ expect(result.decision).toBe('deny');
1144
+ });
1145
+ test('should handle deeply nested input paths', () => {
1146
+ const policy = `
1147
+ package palaryn.policy
1148
+ default decision = "deny"
1149
+ decision = "allow" {
1150
+ input.args.headers.Authorization == "Bearer token"
1151
+ }
1152
+ `;
1153
+ const input = {
1154
+ args: {
1155
+ headers: { Authorization: 'Bearer token' },
1156
+ },
1157
+ };
1158
+ const result = engine.evaluateRegoSubset(policy, input);
1159
+ expect(result.decision).toBe('allow');
1160
+ });
1161
+ test('should handle missing nested paths gracefully', () => {
1162
+ const policy = `
1163
+ package palaryn.policy
1164
+ default decision = "deny"
1165
+ decision = "allow" {
1166
+ input.nonexistent.deep.path == "value"
1167
+ }
1168
+ `;
1169
+ const result = engine.evaluateRegoSubset(policy, { tool: { name: 'test' } });
1170
+ expect(result.decision).toBe('deny');
1171
+ });
1172
+ test('should handle multiple reasons rules', () => {
1173
+ const policy = `
1174
+ package palaryn.policy
1175
+ default decision = "deny"
1176
+ decision = "deny" {
1177
+ input.tool.capability == "admin"
1178
+ }
1179
+ reasons[reason] {
1180
+ input.tool.capability == "admin"
1181
+ reason := "Admin access is restricted"
1182
+ }
1183
+ reasons[reason] {
1184
+ input.tool.name == "dangerous"
1185
+ reason := "Dangerous tool detected"
1186
+ }
1187
+ `;
1188
+ const input = { tool: { capability: 'admin', name: 'dangerous' } };
1189
+ const result = engine.evaluateRegoSubset(policy, input);
1190
+ expect(result.decision).toBe('deny');
1191
+ expect(result.reasons).toContain('Admin access is restricted');
1192
+ expect(result.reasons).toContain('Dangerous tool detected');
1193
+ });
1194
+ test('should handle numeric comparison', () => {
1195
+ const policy = `
1196
+ package palaryn.policy
1197
+ default decision = "deny"
1198
+ decision = "allow" {
1199
+ input.constraints.max_cost_usd == 5
1200
+ }
1201
+ `;
1202
+ const input = { constraints: { max_cost_usd: 5 } };
1203
+ const result = engine.evaluateRegoSubset(policy, input);
1204
+ expect(result.decision).toBe('allow');
1205
+ });
1206
+ });
1207
+ // ---------------------------------------------------------------------------
1208
+ // Extended Rego patterns: some, count, numeric comparisons, regex.match,
1209
+ // type checks, in keyword
1210
+ // ---------------------------------------------------------------------------
1211
+ describe('OPAEngine - Extended Rego patterns', () => {
1212
+ let engine;
1213
+ beforeEach(() => {
1214
+ engine = new opa_engine_1.OPAEngine({ enabled: true, rego_policy: '' });
1215
+ });
1216
+ // -------------------------------------------------------------------------
1217
+ // `some` keyword for iteration
1218
+ // -------------------------------------------------------------------------
1219
+ describe('some keyword for iteration', () => {
1220
+ test('should match when array contains matching element (semicolon syntax)', () => {
1221
+ const policy = `
1222
+ package palaryn.policy
1223
+ default decision = "deny"
1224
+ decision = "allow" {
1225
+ some x in input.context.labels; x == "sensitive"
1226
+ }
1227
+ `;
1228
+ const input = { context: { labels: ['public', 'sensitive', 'data'] } };
1229
+ const result = engine.evaluateRegoSubset(policy, input);
1230
+ expect(result.decision).toBe('allow');
1231
+ });
1232
+ test('should not match when array does not contain matching element', () => {
1233
+ const policy = `
1234
+ package palaryn.policy
1235
+ default decision = "deny"
1236
+ decision = "allow" {
1237
+ some x in input.context.labels; x == "sensitive"
1238
+ }
1239
+ `;
1240
+ const input = { context: { labels: ['public', 'data'] } };
1241
+ const result = engine.evaluateRegoSubset(policy, input);
1242
+ expect(result.decision).toBe('deny');
1243
+ });
1244
+ test('should match with multi-line some block', () => {
1245
+ const policy = `
1246
+ package palaryn.policy
1247
+ default decision = "deny"
1248
+ decision = "allow" {
1249
+ some x in input.context.labels
1250
+ x == "admin"
1251
+ }
1252
+ `;
1253
+ const input = { context: { labels: ['user', 'admin', 'editor'] } };
1254
+ const result = engine.evaluateRegoSubset(policy, input);
1255
+ expect(result.decision).toBe('allow');
1256
+ });
1257
+ test('should not match when collection is not an array', () => {
1258
+ const policy = `
1259
+ package palaryn.policy
1260
+ default decision = "deny"
1261
+ decision = "allow" {
1262
+ some x in input.context.purpose
1263
+ x == "test"
1264
+ }
1265
+ `;
1266
+ const input = { context: { purpose: 'testing' } };
1267
+ const result = engine.evaluateRegoSubset(policy, input);
1268
+ expect(result.decision).toBe('deny');
1269
+ });
1270
+ test('should handle some with empty array', () => {
1271
+ const policy = `
1272
+ package palaryn.policy
1273
+ default decision = "deny"
1274
+ decision = "allow" {
1275
+ some x in input.context.labels
1276
+ x == "admin"
1277
+ }
1278
+ `;
1279
+ const input = { context: { labels: [] } };
1280
+ const result = engine.evaluateRegoSubset(policy, input);
1281
+ expect(result.decision).toBe('deny');
1282
+ });
1283
+ test('should handle some with additional normal conditions', () => {
1284
+ const policy = `
1285
+ package palaryn.policy
1286
+ default decision = "deny"
1287
+ decision = "allow" {
1288
+ input.tool.capability == "read"
1289
+ some x in input.context.labels
1290
+ x == "approved"
1291
+ }
1292
+ `;
1293
+ // Both conditions met
1294
+ const input1 = { tool: { capability: 'read' }, context: { labels: ['approved'] } };
1295
+ const result1 = engine.evaluateRegoSubset(policy, input1);
1296
+ expect(result1.decision).toBe('allow');
1297
+ // Normal condition fails
1298
+ const input2 = { tool: { capability: 'write' }, context: { labels: ['approved'] } };
1299
+ const result2 = engine.evaluateRegoSubset(policy, input2);
1300
+ expect(result2.decision).toBe('deny');
1301
+ // Some condition fails
1302
+ const input3 = { tool: { capability: 'read' }, context: { labels: ['pending'] } };
1303
+ const result3 = engine.evaluateRegoSubset(policy, input3);
1304
+ expect(result3.decision).toBe('deny');
1305
+ });
1306
+ test('should handle some with inequality sub-condition', () => {
1307
+ const policy = `
1308
+ package palaryn.policy
1309
+ default decision = "deny"
1310
+ decision = "allow" {
1311
+ some x in input.context.labels; x != "blocked"
1312
+ }
1313
+ `;
1314
+ // Has at least one element that is not "blocked"
1315
+ const input1 = { context: { labels: ['blocked', 'ok'] } };
1316
+ const result1 = engine.evaluateRegoSubset(policy, input1);
1317
+ expect(result1.decision).toBe('allow');
1318
+ // All elements are "blocked" -- no x satisfies x != "blocked"
1319
+ const input2 = { context: { labels: ['blocked'] } };
1320
+ const result2 = engine.evaluateRegoSubset(policy, input2);
1321
+ expect(result2.decision).toBe('deny');
1322
+ });
1323
+ });
1324
+ // -------------------------------------------------------------------------
1325
+ // count() built-in
1326
+ // -------------------------------------------------------------------------
1327
+ describe('count() built-in', () => {
1328
+ test('should count array elements with > comparison', () => {
1329
+ const policy = `
1330
+ package palaryn.policy
1331
+ default decision = "deny"
1332
+ decision = "allow" {
1333
+ count(input.context.labels) > 3
1334
+ }
1335
+ `;
1336
+ const input1 = { context: { labels: ['a', 'b', 'c', 'd'] } };
1337
+ const result1 = engine.evaluateRegoSubset(policy, input1);
1338
+ expect(result1.decision).toBe('allow');
1339
+ const input2 = { context: { labels: ['a', 'b', 'c'] } };
1340
+ const result2 = engine.evaluateRegoSubset(policy, input2);
1341
+ expect(result2.decision).toBe('deny');
1342
+ });
1343
+ test('should count array elements with == comparison', () => {
1344
+ const policy = `
1345
+ package palaryn.policy
1346
+ default decision = "deny"
1347
+ decision = "allow" {
1348
+ count(input.context.labels) == 2
1349
+ }
1350
+ `;
1351
+ const input = { context: { labels: ['a', 'b'] } };
1352
+ const result = engine.evaluateRegoSubset(policy, input);
1353
+ expect(result.decision).toBe('allow');
1354
+ });
1355
+ test('should count array elements with < comparison', () => {
1356
+ const policy = `
1357
+ package palaryn.policy
1358
+ default decision = "deny"
1359
+ decision = "allow" {
1360
+ count(input.context.labels) < 3
1361
+ }
1362
+ `;
1363
+ const input1 = { context: { labels: ['a', 'b'] } };
1364
+ const result1 = engine.evaluateRegoSubset(policy, input1);
1365
+ expect(result1.decision).toBe('allow');
1366
+ const input2 = { context: { labels: ['a', 'b', 'c'] } };
1367
+ const result2 = engine.evaluateRegoSubset(policy, input2);
1368
+ expect(result2.decision).toBe('deny');
1369
+ });
1370
+ test('should count array elements with >= comparison', () => {
1371
+ const policy = `
1372
+ package palaryn.policy
1373
+ default decision = "deny"
1374
+ decision = "allow" {
1375
+ count(input.context.labels) >= 2
1376
+ }
1377
+ `;
1378
+ const input1 = { context: { labels: ['a', 'b'] } };
1379
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1380
+ const input2 = { context: { labels: ['a'] } };
1381
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1382
+ });
1383
+ test('should count array elements with <= comparison', () => {
1384
+ const policy = `
1385
+ package palaryn.policy
1386
+ default decision = "deny"
1387
+ decision = "allow" {
1388
+ count(input.context.labels) <= 2
1389
+ }
1390
+ `;
1391
+ const input1 = { context: { labels: ['a', 'b'] } };
1392
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1393
+ const input2 = { context: { labels: ['a', 'b', 'c'] } };
1394
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1395
+ });
1396
+ test('should count array elements with != comparison', () => {
1397
+ const policy = `
1398
+ package palaryn.policy
1399
+ default decision = "deny"
1400
+ decision = "allow" {
1401
+ count(input.context.labels) != 0
1402
+ }
1403
+ `;
1404
+ const input1 = { context: { labels: ['a'] } };
1405
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1406
+ const input2 = { context: { labels: [] } };
1407
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1408
+ });
1409
+ test('should count object keys', () => {
1410
+ const policy = `
1411
+ package palaryn.policy
1412
+ default decision = "deny"
1413
+ decision = "allow" {
1414
+ count(input.args) > 1
1415
+ }
1416
+ `;
1417
+ const input = { args: { method: 'GET', url: 'https://example.com' } };
1418
+ const result = engine.evaluateRegoSubset(policy, input);
1419
+ expect(result.decision).toBe('allow');
1420
+ });
1421
+ test('should count string length', () => {
1422
+ const policy = `
1423
+ package palaryn.policy
1424
+ default decision = "deny"
1425
+ decision = "allow" {
1426
+ count(input.tool.name) > 5
1427
+ }
1428
+ `;
1429
+ const input = { tool: { name: 'http.request' } };
1430
+ const result = engine.evaluateRegoSubset(policy, input);
1431
+ expect(result.decision).toBe('allow');
1432
+ });
1433
+ test('should return 0 for undefined value', () => {
1434
+ const policy = `
1435
+ package palaryn.policy
1436
+ default decision = "deny"
1437
+ decision = "allow" {
1438
+ count(input.nonexistent) == 0
1439
+ }
1440
+ `;
1441
+ const result = engine.evaluateRegoSubset(policy, { tool: { name: 'test' } });
1442
+ expect(result.decision).toBe('allow');
1443
+ });
1444
+ });
1445
+ // -------------------------------------------------------------------------
1446
+ // Numeric comparisons (>, <, >=, <=)
1447
+ // -------------------------------------------------------------------------
1448
+ describe('numeric comparisons', () => {
1449
+ test('should evaluate greater than', () => {
1450
+ const policy = `
1451
+ package palaryn.policy
1452
+ default decision = "deny"
1453
+ decision = "allow" {
1454
+ input.constraints.max_cost_usd > 10
1455
+ }
1456
+ `;
1457
+ const input1 = { constraints: { max_cost_usd: 15 } };
1458
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1459
+ const input2 = { constraints: { max_cost_usd: 10 } };
1460
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1461
+ const input3 = { constraints: { max_cost_usd: 5 } };
1462
+ expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
1463
+ });
1464
+ test('should evaluate less than', () => {
1465
+ const policy = `
1466
+ package palaryn.policy
1467
+ default decision = "deny"
1468
+ decision = "allow" {
1469
+ input.constraints.max_cost_usd < 10
1470
+ }
1471
+ `;
1472
+ const input1 = { constraints: { max_cost_usd: 5 } };
1473
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1474
+ const input2 = { constraints: { max_cost_usd: 10 } };
1475
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1476
+ });
1477
+ test('should evaluate greater than or equal', () => {
1478
+ const policy = `
1479
+ package palaryn.policy
1480
+ default decision = "deny"
1481
+ decision = "allow" {
1482
+ input.constraints.max_cost_usd >= 10
1483
+ }
1484
+ `;
1485
+ const input1 = { constraints: { max_cost_usd: 10 } };
1486
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1487
+ const input2 = { constraints: { max_cost_usd: 15 } };
1488
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('allow');
1489
+ const input3 = { constraints: { max_cost_usd: 9 } };
1490
+ expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
1491
+ });
1492
+ test('should evaluate less than or equal', () => {
1493
+ const policy = `
1494
+ package palaryn.policy
1495
+ default decision = "deny"
1496
+ decision = "allow" {
1497
+ input.constraints.max_cost_usd <= 10
1498
+ }
1499
+ `;
1500
+ const input1 = { constraints: { max_cost_usd: 10 } };
1501
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1502
+ const input2 = { constraints: { max_cost_usd: 5 } };
1503
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('allow');
1504
+ const input3 = { constraints: { max_cost_usd: 11 } };
1505
+ expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
1506
+ });
1507
+ test('should return false for non-numeric comparisons', () => {
1508
+ const policy = `
1509
+ package palaryn.policy
1510
+ default decision = "deny"
1511
+ decision = "allow" {
1512
+ input.tool.name > 5
1513
+ }
1514
+ `;
1515
+ const input = { tool: { name: 'http.request' } };
1516
+ expect(engine.evaluateRegoSubset(policy, input).decision).toBe('deny');
1517
+ });
1518
+ test('should handle decimal numbers', () => {
1519
+ const policy = `
1520
+ package palaryn.policy
1521
+ default decision = "deny"
1522
+ decision = "allow" {
1523
+ input.constraints.max_cost_usd <= 0.5
1524
+ }
1525
+ `;
1526
+ const input1 = { constraints: { max_cost_usd: 0.3 } };
1527
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1528
+ const input2 = { constraints: { max_cost_usd: 0.7 } };
1529
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1530
+ });
1531
+ });
1532
+ // -------------------------------------------------------------------------
1533
+ // regex.match() built-in
1534
+ // -------------------------------------------------------------------------
1535
+ describe('regex.match() built-in', () => {
1536
+ test('should match URL pattern', () => {
1537
+ const policy = `
1538
+ package palaryn.policy
1539
+ default decision = "deny"
1540
+ decision = "allow" {
1541
+ regex.match("^https://.*\\.example\\.com", input.args.url)
1542
+ }
1543
+ `;
1544
+ const input1 = { args: { url: 'https://api.example.com/data' } };
1545
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1546
+ const input2 = { args: { url: 'http://api.example.com/data' } };
1547
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1548
+ const input3 = { args: { url: 'https://malicious.com/data' } };
1549
+ expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
1550
+ });
1551
+ test('should match simple patterns', () => {
1552
+ const policy = `
1553
+ package palaryn.policy
1554
+ default decision = "deny"
1555
+ decision = "allow" {
1556
+ regex.match("^http\\.", input.tool.name)
1557
+ }
1558
+ `;
1559
+ const input1 = { tool: { name: 'http.request' } };
1560
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1561
+ const input2 = { tool: { name: 'slack.post' } };
1562
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1563
+ });
1564
+ test('should return false for non-string values', () => {
1565
+ const policy = `
1566
+ package palaryn.policy
1567
+ default decision = "deny"
1568
+ decision = "allow" {
1569
+ regex.match("^test", input.constraints.max_cost_usd)
1570
+ }
1571
+ `;
1572
+ const input = { constraints: { max_cost_usd: 5 } };
1573
+ expect(engine.evaluateRegoSubset(policy, input).decision).toBe('deny');
1574
+ });
1575
+ test('should handle invalid regex gracefully', () => {
1576
+ const policy = `
1577
+ package palaryn.policy
1578
+ default decision = "deny"
1579
+ decision = "allow" {
1580
+ regex.match("[invalid(", input.tool.name)
1581
+ }
1582
+ `;
1583
+ const input = { tool: { name: 'test' } };
1584
+ expect(engine.evaluateRegoSubset(policy, input).decision).toBe('deny');
1585
+ });
1586
+ });
1587
+ // -------------------------------------------------------------------------
1588
+ // Type checks: is_string, is_number, is_boolean, is_array
1589
+ // -------------------------------------------------------------------------
1590
+ describe('type checks', () => {
1591
+ test('should check is_string', () => {
1592
+ const policy = `
1593
+ package palaryn.policy
1594
+ default decision = "deny"
1595
+ decision = "allow" {
1596
+ is_string(input.args.url)
1597
+ }
1598
+ `;
1599
+ const input1 = { args: { url: 'https://example.com' } };
1600
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1601
+ const input2 = { args: { url: 123 } };
1602
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1603
+ });
1604
+ test('should check is_number', () => {
1605
+ const policy = `
1606
+ package palaryn.policy
1607
+ default decision = "deny"
1608
+ decision = "allow" {
1609
+ is_number(input.constraints.max_cost_usd)
1610
+ }
1611
+ `;
1612
+ const input1 = { constraints: { max_cost_usd: 5.0 } };
1613
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1614
+ const input2 = { constraints: { max_cost_usd: 'five' } };
1615
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1616
+ });
1617
+ test('should check is_boolean', () => {
1618
+ const policy = `
1619
+ package palaryn.policy
1620
+ default decision = "deny"
1621
+ decision = "allow" {
1622
+ is_boolean(input.args.dry_run)
1623
+ }
1624
+ `;
1625
+ const input1 = { args: { dry_run: true } };
1626
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1627
+ const input2 = { args: { dry_run: 'true' } };
1628
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1629
+ });
1630
+ test('should check is_array', () => {
1631
+ const policy = `
1632
+ package palaryn.policy
1633
+ default decision = "deny"
1634
+ decision = "allow" {
1635
+ is_array(input.context.labels)
1636
+ }
1637
+ `;
1638
+ const input1 = { context: { labels: ['a', 'b'] } };
1639
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1640
+ const input2 = { context: { labels: 'not-an-array' } };
1641
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1642
+ });
1643
+ test('should return false for undefined values', () => {
1644
+ const policy = `
1645
+ package palaryn.policy
1646
+ default decision = "deny"
1647
+ decision = "allow" {
1648
+ is_string(input.nonexistent.path)
1649
+ }
1650
+ `;
1651
+ const result = engine.evaluateRegoSubset(policy, { tool: { name: 'test' } });
1652
+ expect(result.decision).toBe('deny');
1653
+ });
1654
+ test('should combine type checks with other conditions', () => {
1655
+ const policy = `
1656
+ package palaryn.policy
1657
+ default decision = "deny"
1658
+ decision = "allow" {
1659
+ is_string(input.args.url)
1660
+ startswith(input.args.url, "https://")
1661
+ }
1662
+ `;
1663
+ const input1 = { args: { url: 'https://example.com' } };
1664
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1665
+ const input2 = { args: { url: 'http://example.com' } };
1666
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1667
+ });
1668
+ });
1669
+ // -------------------------------------------------------------------------
1670
+ // Array membership with `in` keyword
1671
+ // -------------------------------------------------------------------------
1672
+ describe('in keyword for array membership', () => {
1673
+ test('should check string membership in array', () => {
1674
+ const policy = `
1675
+ package palaryn.policy
1676
+ default decision = "deny"
1677
+ decision = "allow" {
1678
+ "admin" in input.context.labels
1679
+ }
1680
+ `;
1681
+ const input1 = { context: { labels: ['user', 'admin', 'editor'] } };
1682
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1683
+ const input2 = { context: { labels: ['user', 'editor'] } };
1684
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1685
+ });
1686
+ test('should check numeric membership in array', () => {
1687
+ const policy = `
1688
+ package palaryn.policy
1689
+ default decision = "deny"
1690
+ decision = "allow" {
1691
+ 42 in input.context.labels
1692
+ }
1693
+ `;
1694
+ const input1 = { context: { labels: [1, 42, 100] } };
1695
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1696
+ const input2 = { context: { labels: [1, 2, 3] } };
1697
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1698
+ });
1699
+ test('should return false when target is not an array', () => {
1700
+ const policy = `
1701
+ package palaryn.policy
1702
+ default decision = "deny"
1703
+ decision = "allow" {
1704
+ "admin" in input.tool.name
1705
+ }
1706
+ `;
1707
+ const input = { tool: { name: 'http.request' } };
1708
+ expect(engine.evaluateRegoSubset(policy, input).decision).toBe('deny');
1709
+ });
1710
+ test('should handle boolean membership', () => {
1711
+ const policy = `
1712
+ package palaryn.policy
1713
+ default decision = "deny"
1714
+ decision = "allow" {
1715
+ true in input.context.labels
1716
+ }
1717
+ `;
1718
+ const input1 = { context: { labels: [true, false] } };
1719
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1720
+ const input2 = { context: { labels: ['true', false] } };
1721
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1722
+ });
1723
+ test('should combine in keyword with other conditions', () => {
1724
+ const policy = `
1725
+ package palaryn.policy
1726
+ default decision = "deny"
1727
+ decision = "allow" {
1728
+ input.tool.capability == "write"
1729
+ "approved" in input.context.labels
1730
+ }
1731
+ `;
1732
+ const input1 = { tool: { capability: 'write' }, context: { labels: ['approved'] } };
1733
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1734
+ const input2 = { tool: { capability: 'write' }, context: { labels: ['pending'] } };
1735
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1736
+ const input3 = { tool: { capability: 'read' }, context: { labels: ['approved'] } };
1737
+ expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
1738
+ });
1739
+ });
1740
+ // -------------------------------------------------------------------------
1741
+ // Integration: combining multiple new patterns
1742
+ // -------------------------------------------------------------------------
1743
+ describe('combined patterns', () => {
1744
+ test('should combine count, in, and numeric comparisons', () => {
1745
+ const policy = `
1746
+ package palaryn.policy
1747
+ default decision = "deny"
1748
+ decision = "allow" {
1749
+ count(input.context.labels) >= 2
1750
+ "approved" in input.context.labels
1751
+ input.constraints.max_cost_usd <= 100
1752
+ }
1753
+ `;
1754
+ const input1 = {
1755
+ context: { labels: ['approved', 'reviewed'] },
1756
+ constraints: { max_cost_usd: 50 },
1757
+ };
1758
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1759
+ // count fails
1760
+ const input2 = {
1761
+ context: { labels: ['approved'] },
1762
+ constraints: { max_cost_usd: 50 },
1763
+ };
1764
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1765
+ // in fails
1766
+ const input3 = {
1767
+ context: { labels: ['pending', 'reviewed'] },
1768
+ constraints: { max_cost_usd: 50 },
1769
+ };
1770
+ expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
1771
+ // numeric comparison fails
1772
+ const input4 = {
1773
+ context: { labels: ['approved', 'reviewed'] },
1774
+ constraints: { max_cost_usd: 150 },
1775
+ };
1776
+ expect(engine.evaluateRegoSubset(policy, input4).decision).toBe('deny');
1777
+ });
1778
+ test('should combine regex.match with type checks', () => {
1779
+ const policy = `
1780
+ package palaryn.policy
1781
+ default decision = "deny"
1782
+ decision = "allow" {
1783
+ is_string(input.args.url)
1784
+ regex.match("^https://.*\\.example\\.com", input.args.url)
1785
+ }
1786
+ `;
1787
+ const input1 = { args: { url: 'https://api.example.com/v1' } };
1788
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1789
+ const input2 = { args: { url: 123 } };
1790
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1791
+ });
1792
+ test('should combine some with count', () => {
1793
+ const policy = `
1794
+ package palaryn.policy
1795
+ default decision = "deny"
1796
+ decision = "allow" {
1797
+ count(input.context.labels) > 1
1798
+ some x in input.context.labels
1799
+ x == "critical"
1800
+ }
1801
+ `;
1802
+ const input1 = { context: { labels: ['info', 'critical'] } };
1803
+ expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
1804
+ // count satisfied but some fails
1805
+ const input2 = { context: { labels: ['info', 'warning'] } };
1806
+ expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
1807
+ // some satisfied but count fails
1808
+ const input3 = { context: { labels: ['critical'] } };
1809
+ expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
1810
+ });
1811
+ });
1812
+ });
1813
+ //# sourceMappingURL=opa-engine.test.js.map