palaryn 0.1.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/README.md +243 -588
  2. package/dist/sdk/typescript/src/client.js +2 -2
  3. package/dist/sdk/typescript/src/client.js.map +1 -1
  4. package/dist/src/anomaly/detector.d.ts +7 -4
  5. package/dist/src/anomaly/detector.d.ts.map +1 -1
  6. package/dist/src/anomaly/detector.js +22 -12
  7. package/dist/src/anomaly/detector.js.map +1 -1
  8. package/dist/src/audit/logger.d.ts +10 -0
  9. package/dist/src/audit/logger.d.ts.map +1 -1
  10. package/dist/src/audit/logger.js +52 -38
  11. package/dist/src/audit/logger.js.map +1 -1
  12. package/dist/src/auth/routes.d.ts.map +1 -1
  13. package/dist/src/auth/routes.js +35 -0
  14. package/dist/src/auth/routes.js.map +1 -1
  15. package/dist/src/budget/manager.d.ts +5 -0
  16. package/dist/src/budget/manager.d.ts.map +1 -1
  17. package/dist/src/budget/manager.js +32 -0
  18. package/dist/src/budget/manager.js.map +1 -1
  19. package/dist/src/budget/model-pricing.d.ts +20 -0
  20. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  21. package/dist/src/budget/model-pricing.js +107 -0
  22. package/dist/src/budget/model-pricing.js.map +1 -0
  23. package/dist/src/budget/usage-extractor.d.ts +3 -1
  24. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  25. package/dist/src/budget/usage-extractor.js +47 -3
  26. package/dist/src/budget/usage-extractor.js.map +1 -1
  27. package/dist/src/config/defaults.d.ts.map +1 -1
  28. package/dist/src/config/defaults.js +65 -13
  29. package/dist/src/config/defaults.js.map +1 -1
  30. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  31. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  32. package/dist/src/dlp/tool-patterns.js +34 -0
  33. package/dist/src/dlp/tool-patterns.js.map +1 -0
  34. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  35. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  36. package/dist/src/executor/filesystem-executor.js +192 -0
  37. package/dist/src/executor/filesystem-executor.js.map +1 -0
  38. package/dist/src/executor/http-executor.d.ts.map +1 -1
  39. package/dist/src/executor/http-executor.js +22 -2
  40. package/dist/src/executor/http-executor.js.map +1 -1
  41. package/dist/src/executor/index.d.ts +4 -0
  42. package/dist/src/executor/index.d.ts.map +1 -1
  43. package/dist/src/executor/index.js +9 -1
  44. package/dist/src/executor/index.js.map +1 -1
  45. package/dist/src/executor/shell-executor.d.ts +22 -0
  46. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  47. package/dist/src/executor/shell-executor.js +119 -0
  48. package/dist/src/executor/shell-executor.js.map +1 -0
  49. package/dist/src/executor/sql-executor.d.ts +29 -0
  50. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  51. package/dist/src/executor/sql-executor.js +114 -0
  52. package/dist/src/executor/sql-executor.js.map +1 -0
  53. package/dist/src/executor/websocket-executor.d.ts +26 -0
  54. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  55. package/dist/src/executor/websocket-executor.js +205 -0
  56. package/dist/src/executor/websocket-executor.js.map +1 -0
  57. package/dist/src/interceptor/index.d.ts +2 -0
  58. package/dist/src/interceptor/index.d.ts.map +1 -0
  59. package/dist/src/interceptor/index.js +6 -0
  60. package/dist/src/interceptor/index.js.map +1 -0
  61. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  62. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  63. package/dist/src/interceptor/provider-interceptor.js +302 -0
  64. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  65. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  66. package/dist/src/mcp/auth-verifier.js +3 -2
  67. package/dist/src/mcp/auth-verifier.js.map +1 -1
  68. package/dist/src/mcp/bridge.d.ts +14 -10
  69. package/dist/src/mcp/bridge.d.ts.map +1 -1
  70. package/dist/src/mcp/bridge.js +51 -227
  71. package/dist/src/mcp/bridge.js.map +1 -1
  72. package/dist/src/mcp/http-transport.d.ts +2 -0
  73. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  74. package/dist/src/mcp/http-transport.js +117 -66
  75. package/dist/src/mcp/http-transport.js.map +1 -1
  76. package/dist/src/mcp/internal-auth.d.ts +13 -0
  77. package/dist/src/mcp/internal-auth.d.ts.map +1 -0
  78. package/dist/src/mcp/internal-auth.js +12 -0
  79. package/dist/src/mcp/internal-auth.js.map +1 -0
  80. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  81. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  82. package/dist/src/mcp/tool-definitions.js +491 -0
  83. package/dist/src/mcp/tool-definitions.js.map +1 -0
  84. package/dist/src/middleware/auth.js.map +1 -1
  85. package/dist/src/middleware/session.js.map +1 -1
  86. package/dist/src/middleware/validate.d.ts +8 -0
  87. package/dist/src/middleware/validate.d.ts.map +1 -1
  88. package/dist/src/middleware/validate.js +45 -0
  89. package/dist/src/middleware/validate.js.map +1 -1
  90. package/dist/src/policy/engine.d.ts +4 -0
  91. package/dist/src/policy/engine.d.ts.map +1 -1
  92. package/dist/src/policy/engine.js +117 -0
  93. package/dist/src/policy/engine.js.map +1 -1
  94. package/dist/src/saas/routes.d.ts.map +1 -1
  95. package/dist/src/saas/routes.js +355 -22
  96. package/dist/src/saas/routes.js.map +1 -1
  97. package/dist/src/server/app.d.ts.map +1 -1
  98. package/dist/src/server/app.js +24 -3
  99. package/dist/src/server/app.js.map +1 -1
  100. package/dist/src/server/gateway.d.ts.map +1 -1
  101. package/dist/src/server/gateway.js +17 -0
  102. package/dist/src/server/gateway.js.map +1 -1
  103. package/dist/src/server/index.d.ts.map +1 -1
  104. package/dist/src/server/index.js +18 -0
  105. package/dist/src/server/index.js.map +1 -1
  106. package/dist/src/storage/interfaces.d.ts +14 -3
  107. package/dist/src/storage/interfaces.d.ts.map +1 -1
  108. package/dist/src/storage/memory.d.ts +2 -0
  109. package/dist/src/storage/memory.d.ts.map +1 -1
  110. package/dist/src/storage/memory.js +6 -0
  111. package/dist/src/storage/memory.js.map +1 -1
  112. package/dist/src/storage/postgres.d.ts +5 -0
  113. package/dist/src/storage/postgres.d.ts.map +1 -1
  114. package/dist/src/storage/postgres.js +16 -0
  115. package/dist/src/storage/postgres.js.map +1 -1
  116. package/dist/src/storage/redis.d.ts +10 -0
  117. package/dist/src/storage/redis.d.ts.map +1 -1
  118. package/dist/src/storage/redis.js +65 -0
  119. package/dist/src/storage/redis.js.map +1 -1
  120. package/dist/src/types/budget.d.ts +4 -0
  121. package/dist/src/types/budget.d.ts.map +1 -1
  122. package/dist/src/types/config.d.ts +58 -0
  123. package/dist/src/types/config.d.ts.map +1 -1
  124. package/dist/src/types/events.d.ts +1 -0
  125. package/dist/src/types/events.d.ts.map +1 -1
  126. package/dist/src/types/policy.d.ts +11 -1
  127. package/dist/src/types/policy.d.ts.map +1 -1
  128. package/dist/src/types/tool-result.d.ts +11 -0
  129. package/dist/src/types/tool-result.d.ts.map +1 -1
  130. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  131. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  132. package/dist/tests/unit/app-routes.test.js +715 -0
  133. package/dist/tests/unit/app-routes.test.js.map +1 -0
  134. package/dist/tests/unit/audit-logger.test.js +105 -0
  135. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  136. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  137. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  138. package/dist/tests/unit/auth-providers.test.js +279 -0
  139. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  140. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  141. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  142. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  143. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  144. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  145. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  146. package/dist/tests/unit/auth-verifier.test.js +505 -0
  147. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  148. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  149. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  150. package/dist/tests/unit/billing-routes.test.js +432 -0
  151. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  152. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  153. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  154. package/dist/tests/unit/config-defaults.test.js +119 -0
  155. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  156. package/dist/tests/unit/defaults.test.js +0 -10
  157. package/dist/tests/unit/defaults.test.js.map +1 -1
  158. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  159. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  160. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  161. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  162. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  163. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  164. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  165. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  166. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  167. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  168. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  169. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  170. package/dist/tests/unit/logger.test.d.ts +2 -0
  171. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  172. package/dist/tests/unit/logger.test.js +97 -0
  173. package/dist/tests/unit/logger.test.js.map +1 -0
  174. package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
  175. package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
  176. package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
  177. package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
  178. package/dist/tests/unit/metrics.test.js +102 -0
  179. package/dist/tests/unit/metrics.test.js.map +1 -1
  180. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  181. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  182. package/dist/tests/unit/model-pricing.test.js +87 -0
  183. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  184. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  185. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  186. package/dist/tests/unit/oauth-stores.test.js +260 -0
  187. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  188. package/dist/tests/unit/policy-engine.test.js +466 -0
  189. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  190. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  191. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  192. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  193. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  194. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  195. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  196. package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
  197. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  198. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  199. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  200. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  201. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  202. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  203. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  204. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  205. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  206. package/dist/tests/unit/saas-routes.test.js +3 -3
  207. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  208. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  209. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  210. package/dist/tests/unit/shell-executor.test.js +145 -0
  211. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  212. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  213. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  214. package/dist/tests/unit/sql-executor.test.js +177 -0
  215. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  216. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  217. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  218. package/dist/tests/unit/stream-proxy.test.js +147 -0
  219. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  220. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  221. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  222. package/dist/tests/unit/tool-definitions.test.js +184 -0
  223. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  224. package/dist/tests/unit/usage-extractor.test.js +140 -0
  225. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  226. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  227. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  228. package/dist/tests/unit/webhook-handler.test.js +453 -0
  229. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  230. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  231. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  232. package/dist/tests/unit/webhook-routes.test.js +69 -0
  233. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  234. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  235. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  236. package/dist/tests/unit/websocket-executor.test.js +121 -0
  237. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  238. package/package.json +8 -2
  239. package/policy-packs/demo_fail.yaml +41 -0
  240. package/policy-packs/full_tools.yaml +136 -0
  241. package/src/admin/index.ts +1 -0
  242. package/src/admin/routes.ts +509 -0
  243. package/src/admin/templates.ts +572 -0
  244. package/src/anomaly/detector.ts +730 -0
  245. package/src/anomaly/index.ts +1 -0
  246. package/src/approval/manager.ts +569 -0
  247. package/src/approval/webhook.ts +133 -0
  248. package/src/audit/logger.ts +490 -0
  249. package/src/auth/index.ts +5 -0
  250. package/src/auth/password.ts +21 -0
  251. package/src/auth/pkce.ts +22 -0
  252. package/src/auth/providers.ts +208 -0
  253. package/src/auth/routes.ts +561 -0
  254. package/src/auth/session.ts +84 -0
  255. package/src/billing/index.ts +6 -0
  256. package/src/billing/plan-enforcer.ts +135 -0
  257. package/src/billing/routes.ts +229 -0
  258. package/src/billing/stripe-client.ts +58 -0
  259. package/src/billing/webhook-handler.ts +182 -0
  260. package/src/billing/webhook-routes.ts +28 -0
  261. package/src/budget/manager.ts +679 -0
  262. package/src/budget/model-pricing.ts +119 -0
  263. package/src/budget/usage-extractor.ts +214 -0
  264. package/src/cli.ts +91 -0
  265. package/src/config/defaults.ts +261 -0
  266. package/src/config/validate.ts +88 -0
  267. package/src/dlp/composite-scanner.ts +213 -0
  268. package/src/dlp/index.ts +9 -0
  269. package/src/dlp/interfaces.ts +34 -0
  270. package/src/dlp/patterns.ts +30 -0
  271. package/src/dlp/prompt-injection-backend.ts +181 -0
  272. package/src/dlp/prompt-injection-patterns.ts +302 -0
  273. package/src/dlp/regex-backend.ts +181 -0
  274. package/src/dlp/scanner.ts +502 -0
  275. package/src/dlp/text-normalizer.ts +225 -0
  276. package/src/dlp/tool-patterns.ts +35 -0
  277. package/src/dlp/trufflehog-backend.ts +190 -0
  278. package/src/executor/filesystem-executor.ts +196 -0
  279. package/src/executor/http-executor.ts +349 -0
  280. package/src/executor/index.ts +9 -0
  281. package/src/executor/interfaces.ts +11 -0
  282. package/src/executor/noop-executor.ts +23 -0
  283. package/src/executor/registry.ts +64 -0
  284. package/src/executor/shell-executor.ts +148 -0
  285. package/src/executor/slack-executor.ts +176 -0
  286. package/src/executor/sql-executor.ts +146 -0
  287. package/src/executor/websocket-executor.ts +211 -0
  288. package/src/index.ts +24 -0
  289. package/src/interceptor/index.ts +1 -0
  290. package/src/interceptor/provider-interceptor.ts +315 -0
  291. package/src/mcp/auth-verifier.ts +152 -0
  292. package/src/mcp/bridge.ts +703 -0
  293. package/src/mcp/http-transport.ts +698 -0
  294. package/src/mcp/index.ts +9 -0
  295. package/src/mcp/internal-auth.ts +14 -0
  296. package/src/mcp/oauth-pages.ts +139 -0
  297. package/src/mcp/oauth-postgres-stores.ts +278 -0
  298. package/src/mcp/oauth-provider.ts +536 -0
  299. package/src/mcp/oauth-stores.ts +202 -0
  300. package/src/mcp/server.ts +55 -0
  301. package/src/mcp/tool-definitions.ts +562 -0
  302. package/src/metrics/collector.ts +357 -0
  303. package/src/metrics/index.ts +1 -0
  304. package/src/middleware/auth.ts +814 -0
  305. package/src/middleware/session.ts +85 -0
  306. package/src/middleware/validate.ts +130 -0
  307. package/src/policy/engine.ts +815 -0
  308. package/src/policy/index.ts +2 -0
  309. package/src/policy/opa-engine.ts +829 -0
  310. package/src/proxy/forward-proxy.ts +649 -0
  311. package/src/proxy/index.ts +1 -0
  312. package/src/ratelimit/limiter.ts +196 -0
  313. package/src/replay/engine.ts +142 -0
  314. package/src/replay/index.ts +1 -0
  315. package/src/saas/index.ts +1 -0
  316. package/src/saas/routes.ts +2178 -0
  317. package/src/server/app.ts +985 -0
  318. package/src/server/errors.ts +49 -0
  319. package/src/server/gateway.ts +1130 -0
  320. package/src/server/index.ts +307 -0
  321. package/src/server/logger.ts +255 -0
  322. package/src/server/stream-proxy.ts +202 -0
  323. package/src/storage/file-persistence.ts +315 -0
  324. package/src/storage/index.ts +4 -0
  325. package/src/storage/interfaces.ts +287 -0
  326. package/src/storage/memory.ts +686 -0
  327. package/src/storage/postgres.ts +1831 -0
  328. package/src/storage/redis.ts +835 -0
  329. package/src/tracing/index.ts +1 -0
  330. package/src/tracing/provider.ts +100 -0
  331. package/src/trust/calculator.ts +141 -0
  332. package/src/trust/index.ts +7 -0
  333. package/src/types/budget.ts +36 -0
  334. package/src/types/config.ts +278 -0
  335. package/src/types/events.ts +41 -0
  336. package/src/types/express.d.ts +14 -0
  337. package/src/types/index.ts +7 -0
  338. package/src/types/policy.ts +83 -0
  339. package/src/types/stripe-config.ts +11 -0
  340. package/src/types/subscription.ts +59 -0
  341. package/src/types/tool-call.ts +47 -0
  342. package/src/types/tool-result.ts +82 -0
  343. package/src/types/user.ts +125 -0
  344. package/tsconfig.json +24 -0
@@ -0,0 +1,715 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const supertest_1 = __importDefault(require("supertest"));
7
+ const app_1 = require("../../src/server/app");
8
+ // ---------------------------------------------------------------------------
9
+ // Test config helper
10
+ // ---------------------------------------------------------------------------
11
+ function testConfig(overrides) {
12
+ return {
13
+ port: 0,
14
+ host: '127.0.0.1',
15
+ auth: { enabled: false, api_keys: {} },
16
+ policy: { pack_path: './policy-packs/dev_fast.yaml', default_effect: 'ALLOW', hot_reload: false },
17
+ dlp: { enabled: false, scan_args: false, scan_output: false, secrets_detection: false, pii_detection: false, default_redaction_method: 'mask' },
18
+ budget: { task_budget_usd: 100, max_steps_per_task: 1000, max_retries_per_call: 3, max_wall_clock_ms: 300000 },
19
+ audit: { enabled: false, log_dir: '', console_output: false, retention_days: 30 },
20
+ executor: { http: { timeout_ms: 5000, max_retries: 1, backoff_base_ms: 100 }, cache: { enabled: false, ttl_ms: 0 } },
21
+ approval: { enabled: false, token_secret: 'test-secret', default_ttl_seconds: 3600 },
22
+ ...overrides,
23
+ };
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Minimal valid tool call body (passes validateToolCall middleware)
27
+ // ---------------------------------------------------------------------------
28
+ function validToolCall(extra) {
29
+ return {
30
+ tool_call_id: 'tc-000001',
31
+ task_id: 'task-001',
32
+ actor: { type: 'agent', id: 'agent-1' },
33
+ source: { platform: 'test' },
34
+ tool: { name: 'http', capability: 'read' },
35
+ args: { method: 'GET', url: 'https://example.com' },
36
+ ...extra,
37
+ };
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // Suite
41
+ // ---------------------------------------------------------------------------
42
+ describe('app.ts routes', () => {
43
+ let app;
44
+ let gateway;
45
+ let metrics;
46
+ let healthChecks;
47
+ beforeEach(() => {
48
+ const result = (0, app_1.createApp)(testConfig());
49
+ app = result.app;
50
+ gateway = result.gateway;
51
+ metrics = result.metrics;
52
+ healthChecks = result.healthChecks;
53
+ });
54
+ afterEach(async () => {
55
+ await gateway.shutdown();
56
+ });
57
+ // =========================================================================
58
+ // 1. GET /health
59
+ // =========================================================================
60
+ describe('GET /health', () => {
61
+ it('returns { status: "ok" } when no health checks registered', async () => {
62
+ const res = await (0, supertest_1.default)(app).get('/health');
63
+ expect(res.status).toBe(200);
64
+ expect(res.body).toEqual({ status: 'ok' });
65
+ });
66
+ it('returns check results when a healthy health check is registered', async () => {
67
+ healthChecks.push({
68
+ name: 'test-db',
69
+ check: async () => ({ status: 'ok' }),
70
+ });
71
+ const res = await (0, supertest_1.default)(app).get('/health');
72
+ expect(res.status).toBe(200);
73
+ expect(res.body.status).toBe('ok');
74
+ });
75
+ it('returns 503 when an unhealthy health check is registered', async () => {
76
+ healthChecks.push({
77
+ name: 'test-db',
78
+ check: async () => ({ status: 'unhealthy', message: 'connection refused' }),
79
+ });
80
+ const res = await (0, supertest_1.default)(app).get('/health');
81
+ expect(res.status).toBe(503);
82
+ expect(res.body.status).toBe('unhealthy');
83
+ });
84
+ it('returns 200 with degraded status when health check is degraded', async () => {
85
+ healthChecks.push({
86
+ name: 'test-cache',
87
+ check: async () => ({ status: 'degraded', message: 'high latency' }),
88
+ });
89
+ const res = await (0, supertest_1.default)(app).get('/health');
90
+ expect(res.status).toBe(200);
91
+ expect(res.body.status).toBe('degraded');
92
+ });
93
+ it('marks overall as unhealthy when health check throws', async () => {
94
+ healthChecks.push({
95
+ name: 'test-crash',
96
+ check: async () => { throw new Error('boom'); },
97
+ });
98
+ const res = await (0, supertest_1.default)(app).get('/health');
99
+ expect(res.status).toBe(503);
100
+ expect(res.body.status).toBe('unhealthy');
101
+ });
102
+ it('unhealthy overrides degraded when multiple checks present', async () => {
103
+ healthChecks.push({ name: 'degraded-svc', check: async () => ({ status: 'degraded' }) }, { name: 'unhealthy-svc', check: async () => ({ status: 'unhealthy' }) });
104
+ const res = await (0, supertest_1.default)(app).get('/health');
105
+ expect(res.status).toBe(503);
106
+ expect(res.body.status).toBe('unhealthy');
107
+ });
108
+ it('returns minimal response for unauthenticated requests (no version/uptime)', async () => {
109
+ const res = await (0, supertest_1.default)(app).get('/health');
110
+ expect(res.body).not.toHaveProperty('version');
111
+ expect(res.body).not.toHaveProperty('uptime_seconds');
112
+ expect(res.body).not.toHaveProperty('checks');
113
+ });
114
+ it('always returns minimal response since session middleware is registered after /health', async () => {
115
+ // The session middleware is registered after the /health route in app.ts,
116
+ // so (req as any).auth is always undefined for /health requests.
117
+ // This test verifies the unauthenticated path is always taken.
118
+ healthChecks.push({
119
+ name: 'test-svc',
120
+ check: async () => ({ status: 'ok' }),
121
+ });
122
+ const res = await (0, supertest_1.default)(app).get('/health');
123
+ expect(res.status).toBe(200);
124
+ // No version, uptime, checks, or timestamp in unauthenticated response
125
+ expect(res.body).toEqual({ status: 'ok' });
126
+ });
127
+ });
128
+ // =========================================================================
129
+ // 2. GET /ready
130
+ // =========================================================================
131
+ describe('GET /ready', () => {
132
+ it('returns { ready: true } in normal state', async () => {
133
+ const res = await (0, supertest_1.default)(app).get('/ready');
134
+ expect(res.status).toBe(200);
135
+ expect(res.body.ready).toBe(true);
136
+ // Should always include policy_engine check
137
+ expect(res.body.checks.find((c) => c.name === 'policy_engine')).toBeDefined();
138
+ });
139
+ it('returns 503 { ready: false } when gateway is shutting down', async () => {
140
+ // Trigger shutdown to set isShuttingDown = true
141
+ await gateway.shutdown();
142
+ const res = await (0, supertest_1.default)(app).get('/ready');
143
+ expect(res.status).toBe(503);
144
+ expect(res.body.ready).toBe(false);
145
+ const gwCheck = res.body.checks.find((c) => c.name === 'gateway');
146
+ expect(gwCheck.status).toBe('unhealthy');
147
+ expect(gwCheck.message).toBe('shutting down');
148
+ });
149
+ it('returns 503 when a health check throws', async () => {
150
+ healthChecks.push({
151
+ name: 'test-db',
152
+ check: async () => { throw new Error('db connection failed'); },
153
+ });
154
+ const res = await (0, supertest_1.default)(app).get('/ready');
155
+ expect(res.status).toBe(503);
156
+ expect(res.body.ready).toBe(false);
157
+ const dbCheck = res.body.checks.find((c) => c.name === 'test-db');
158
+ expect(dbCheck.status).toBe('unhealthy');
159
+ expect(dbCheck.message).toBe('db connection failed');
160
+ });
161
+ it('includes error message from non-Error throw', async () => {
162
+ healthChecks.push({
163
+ name: 'test-db',
164
+ check: async () => { throw 'string error'; },
165
+ });
166
+ const res = await (0, supertest_1.default)(app).get('/ready');
167
+ expect(res.status).toBe(503);
168
+ const dbCheck = res.body.checks.find((c) => c.name === 'test-db');
169
+ expect(dbCheck.status).toBe('unhealthy');
170
+ expect(dbCheck.message).toBe('check failed');
171
+ });
172
+ it('returns 503 when health check returns unhealthy', async () => {
173
+ healthChecks.push({
174
+ name: 'test-redis',
175
+ check: async () => ({ status: 'unhealthy', message: 'timeout' }),
176
+ });
177
+ const res = await (0, supertest_1.default)(app).get('/ready');
178
+ expect(res.status).toBe(503);
179
+ expect(res.body.ready).toBe(false);
180
+ });
181
+ it('returns 200 when health check is degraded (ready but degraded)', async () => {
182
+ healthChecks.push({
183
+ name: 'test-cache',
184
+ check: async () => ({ status: 'degraded', message: 'slow' }),
185
+ });
186
+ const res = await (0, supertest_1.default)(app).get('/ready');
187
+ // degraded does not make ready=false, only unhealthy does
188
+ expect(res.status).toBe(200);
189
+ expect(res.body.ready).toBe(true);
190
+ });
191
+ });
192
+ // =========================================================================
193
+ // 3. GET /metrics
194
+ // =========================================================================
195
+ describe('GET /metrics', () => {
196
+ it('returns metrics without auth in non-production', async () => {
197
+ const res = await (0, supertest_1.default)(app).get('/metrics');
198
+ expect(res.status).toBe(200);
199
+ expect(res.text).toContain('palaryn_');
200
+ });
201
+ it('returns 401 in production mode with auth enabled and no key', async () => {
202
+ const origEnv = process.env.NODE_ENV;
203
+ process.env.NODE_ENV = 'production';
204
+ try {
205
+ const prodResult = (0, app_1.createApp)(testConfig({
206
+ auth: { enabled: true, api_keys: { 'prod-key-12345': { workspace_id: 'ws-1' } } },
207
+ }));
208
+ const res = await (0, supertest_1.default)(prodResult.app).get('/metrics');
209
+ expect(res.status).toBe(401);
210
+ expect(res.body.error_code).toBe('AUTH_REQUIRED');
211
+ await prodResult.gateway.shutdown();
212
+ }
213
+ finally {
214
+ process.env.NODE_ENV = origEnv;
215
+ }
216
+ });
217
+ it('returns metrics in production mode with x-api-key header', async () => {
218
+ const origEnv = process.env.NODE_ENV;
219
+ process.env.NODE_ENV = 'production';
220
+ try {
221
+ const prodResult = (0, app_1.createApp)(testConfig({
222
+ auth: { enabled: true, api_keys: { 'prod-key-12345': { workspace_id: 'ws-1' } } },
223
+ }));
224
+ const res = await (0, supertest_1.default)(prodResult.app)
225
+ .get('/metrics')
226
+ .set('x-api-key', 'prod-key-12345');
227
+ expect(res.status).toBe(200);
228
+ expect(res.text).toContain('palaryn_');
229
+ await prodResult.gateway.shutdown();
230
+ }
231
+ finally {
232
+ process.env.NODE_ENV = origEnv;
233
+ }
234
+ });
235
+ it('returns metrics in production mode with bearer token', async () => {
236
+ const origEnv = process.env.NODE_ENV;
237
+ process.env.NODE_ENV = 'production';
238
+ try {
239
+ const prodResult = (0, app_1.createApp)(testConfig({
240
+ auth: { enabled: true, api_keys: { 'bearer-key-123': { workspace_id: 'ws-1' } } },
241
+ }));
242
+ const res = await (0, supertest_1.default)(prodResult.app)
243
+ .get('/metrics')
244
+ .set('Authorization', 'Bearer some-bearer-token');
245
+ expect(res.status).toBe(200);
246
+ await prodResult.gateway.shutdown();
247
+ }
248
+ finally {
249
+ process.env.NODE_ENV = origEnv;
250
+ }
251
+ });
252
+ it('returns 500 when metrics.getMetrics() throws', async () => {
253
+ jest.spyOn(metrics, 'getMetrics').mockRejectedValueOnce(new Error('metrics boom'));
254
+ const res = await (0, supertest_1.default)(app).get('/metrics');
255
+ expect(res.status).toBe(500);
256
+ });
257
+ });
258
+ // =========================================================================
259
+ // 4. GET /install.sh
260
+ // =========================================================================
261
+ describe('GET /install.sh', () => {
262
+ it('returns install script with text/plain content type when file exists', async () => {
263
+ // Mock fs to simulate install.sh existing (path resolution differs in Jest vs dist)
264
+ const fs = require('fs');
265
+ const origExistsSync = fs.existsSync;
266
+ const origReadFileSync = fs.readFileSync;
267
+ fs.existsSync = (p) => {
268
+ if (p.endsWith('install.sh'))
269
+ return true;
270
+ return origExistsSync(p);
271
+ };
272
+ fs.readFileSync = (p, enc) => {
273
+ if (typeof p === 'string' && p.endsWith('install.sh'))
274
+ return '#!/bin/sh\necho "install"';
275
+ return origReadFileSync(p, enc);
276
+ };
277
+ try {
278
+ const result2 = (0, app_1.createApp)(testConfig());
279
+ const res = await (0, supertest_1.default)(result2.app).get('/install.sh');
280
+ expect(res.status).toBe(200);
281
+ expect(res.headers['content-type']).toContain('text/plain');
282
+ expect(res.text).toContain('#!/bin/sh');
283
+ await result2.gateway.shutdown();
284
+ }
285
+ finally {
286
+ fs.existsSync = origExistsSync;
287
+ fs.readFileSync = origReadFileSync;
288
+ }
289
+ });
290
+ it('returns 404 when file does not exist', async () => {
291
+ const fs = require('fs');
292
+ const origExistsSync = fs.existsSync;
293
+ fs.existsSync = (p) => {
294
+ if (p.endsWith('install.sh'))
295
+ return false;
296
+ return origExistsSync(p);
297
+ };
298
+ try {
299
+ const result2 = (0, app_1.createApp)(testConfig());
300
+ const res = await (0, supertest_1.default)(result2.app).get('/install.sh');
301
+ expect(res.status).toBe(404);
302
+ expect(res.text).toContain('install.sh not found');
303
+ await result2.gateway.shutdown();
304
+ }
305
+ finally {
306
+ fs.existsSync = origExistsSync;
307
+ }
308
+ });
309
+ });
310
+ // =========================================================================
311
+ // 5. POST /v1/tool/execute
312
+ // =========================================================================
313
+ describe('POST /v1/tool/execute', () => {
314
+ it('returns 200 when gateway.execute returns status "ok"', async () => {
315
+ const mockResult = {
316
+ tool_call_id: 'tc-000001',
317
+ task_id: 'task-001',
318
+ status: 'ok',
319
+ policy: { decision: 'allow', reasons: [] },
320
+ dlp: { detected: [], redactions: [], severity: 'low' },
321
+ budget: { estimated_cost_usd: 0, spent_cost_usd_task: 0, remaining_cost_usd_task: 100 },
322
+ output: { http_status: 200, body: { data: 'ok' } },
323
+ timing: { started_at: new Date().toISOString(), duration_ms: 10 },
324
+ };
325
+ jest.spyOn(gateway, 'execute').mockResolvedValueOnce(mockResult);
326
+ const res = await (0, supertest_1.default)(app)
327
+ .post('/v1/tool/execute')
328
+ .send(validToolCall());
329
+ expect(res.status).toBe(200);
330
+ expect(res.body.status).toBe('ok');
331
+ });
332
+ it('returns 403 when gateway.execute returns status "blocked"', async () => {
333
+ const mockResult = {
334
+ tool_call_id: 'tc-000001',
335
+ task_id: 'task-001',
336
+ status: 'blocked',
337
+ policy: { decision: 'deny', rule_id: 'block-all', reasons: ['blocked by policy'] },
338
+ dlp: { detected: [], redactions: [], severity: 'low' },
339
+ budget: { estimated_cost_usd: 0, spent_cost_usd_task: 0, remaining_cost_usd_task: 100 },
340
+ timing: { started_at: new Date().toISOString(), duration_ms: 5 },
341
+ };
342
+ jest.spyOn(gateway, 'execute').mockResolvedValueOnce(mockResult);
343
+ const res = await (0, supertest_1.default)(app)
344
+ .post('/v1/tool/execute')
345
+ .send(validToolCall());
346
+ expect(res.status).toBe(403);
347
+ expect(res.body.status).toBe('blocked');
348
+ });
349
+ it('returns 429 when blocked with rate_limit rule_id', async () => {
350
+ const mockResult = {
351
+ tool_call_id: 'tc-000001',
352
+ task_id: 'task-001',
353
+ status: 'blocked',
354
+ policy: { decision: 'deny', rule_id: 'rate_limit', reasons: ['rate limited'] },
355
+ dlp: { detected: [], redactions: [], severity: 'low' },
356
+ budget: { estimated_cost_usd: 0, spent_cost_usd_task: 0, remaining_cost_usd_task: 100 },
357
+ timing: { started_at: new Date().toISOString(), duration_ms: 5 },
358
+ };
359
+ jest.spyOn(gateway, 'execute').mockResolvedValueOnce(mockResult);
360
+ const res = await (0, supertest_1.default)(app)
361
+ .post('/v1/tool/execute')
362
+ .send(validToolCall());
363
+ expect(res.status).toBe(429);
364
+ });
365
+ it('returns 202 when status is "needs_approval"', async () => {
366
+ const mockResult = {
367
+ tool_call_id: 'tc-000001',
368
+ task_id: 'task-001',
369
+ status: 'needs_approval',
370
+ policy: { decision: 'require_approval', reasons: ['requires admin approval'] },
371
+ dlp: { detected: [], redactions: [], severity: 'low' },
372
+ budget: { estimated_cost_usd: 0, spent_cost_usd_task: 0, remaining_cost_usd_task: 100 },
373
+ timing: { started_at: new Date().toISOString(), duration_ms: 5 },
374
+ };
375
+ jest.spyOn(gateway, 'execute').mockResolvedValueOnce(mockResult);
376
+ const res = await (0, supertest_1.default)(app)
377
+ .post('/v1/tool/execute')
378
+ .send(validToolCall());
379
+ expect(res.status).toBe(202);
380
+ expect(res.body.status).toBe('needs_approval');
381
+ });
382
+ it('sanitizes error in result (never leaks internal details)', async () => {
383
+ const mockResult = {
384
+ tool_call_id: 'tc-000001',
385
+ task_id: 'task-001',
386
+ status: 'ok',
387
+ policy: { decision: 'allow', reasons: [] },
388
+ dlp: { detected: [], redactions: [], severity: 'low' },
389
+ budget: { estimated_cost_usd: 0, spent_cost_usd_task: 0, remaining_cost_usd_task: 100 },
390
+ error: 'Internal path /var/secrets/key.pem leaked',
391
+ timing: { started_at: new Date().toISOString(), duration_ms: 10 },
392
+ };
393
+ jest.spyOn(gateway, 'execute').mockResolvedValueOnce(mockResult);
394
+ const res = await (0, supertest_1.default)(app)
395
+ .post('/v1/tool/execute')
396
+ .send(validToolCall());
397
+ expect(res.status).toBe(200);
398
+ expect(res.body.error).toBe('Tool execution failed');
399
+ });
400
+ it('returns 500 when gateway.execute throws', async () => {
401
+ jest.spyOn(gateway, 'execute').mockRejectedValueOnce(new Error('unexpected crash'));
402
+ const res = await (0, supertest_1.default)(app)
403
+ .post('/v1/tool/execute')
404
+ .send(validToolCall());
405
+ expect(res.status).toBe(500);
406
+ expect(res.body.error_code).toBe('TOOL_EXECUTION_ERROR');
407
+ });
408
+ it('returns 500 when gateway.execute throws non-Error', async () => {
409
+ jest.spyOn(gateway, 'execute').mockRejectedValueOnce('string error');
410
+ const res = await (0, supertest_1.default)(app)
411
+ .post('/v1/tool/execute')
412
+ .send(validToolCall());
413
+ expect(res.status).toBe(500);
414
+ expect(res.body.error_code).toBe('TOOL_EXECUTION_ERROR');
415
+ });
416
+ it('merges api_key_tags into context.labels', async () => {
417
+ // Create an app with auth enabled and an API key that has tags
418
+ const authConfig = testConfig({
419
+ auth: {
420
+ enabled: true,
421
+ api_keys: {
422
+ 'tagged-key-12345': {
423
+ workspace_id: 'ws-tags',
424
+ roles: ['operator'],
425
+ },
426
+ },
427
+ },
428
+ });
429
+ const authResult = (0, app_1.createApp)(authConfig);
430
+ // The api_key_tags won't be set by config-based keys (they come from SaaS keys),
431
+ // but we can test the merge logic by setting auth context via a mock.
432
+ // Instead, we spy on execute to verify the toolCall passed to it has merged labels.
433
+ const executeSpy = jest.spyOn(authResult.gateway, 'execute').mockResolvedValueOnce({
434
+ tool_call_id: 'tc-000001',
435
+ task_id: 'task-001',
436
+ status: 'ok',
437
+ policy: { decision: 'allow', reasons: [] },
438
+ dlp: { detected: [], redactions: [], severity: 'low' },
439
+ budget: { estimated_cost_usd: 0, spent_cost_usd_task: 0, remaining_cost_usd_task: 100 },
440
+ timing: { started_at: new Date().toISOString(), duration_ms: 10 },
441
+ });
442
+ const res = await (0, supertest_1.default)(authResult.app)
443
+ .post('/v1/tool/execute')
444
+ .set('x-api-key', 'tagged-key-12345')
445
+ .send(validToolCall({
446
+ context: { labels: ['existing-label'] },
447
+ }));
448
+ expect(res.status).toBe(200);
449
+ // Gateway should have been called with the tool call
450
+ expect(executeSpy).toHaveBeenCalled();
451
+ await authResult.gateway.shutdown();
452
+ });
453
+ it('returns 403 for capability-specific RBAC check when rbac is enabled', async () => {
454
+ const rbacConfig = testConfig({
455
+ auth: {
456
+ enabled: true,
457
+ api_keys: {
458
+ 'rbac-key-12345': {
459
+ workspace_id: 'ws-rbac',
460
+ roles: ['reader'],
461
+ },
462
+ },
463
+ rbac: {
464
+ enabled: true,
465
+ roles: {
466
+ reader: {
467
+ permissions: ['tool:execute:read'],
468
+ },
469
+ },
470
+ },
471
+ },
472
+ });
473
+ const rbacResult = (0, app_1.createApp)(rbacConfig);
474
+ // Request with a write capability but only reader role
475
+ const res = await (0, supertest_1.default)(rbacResult.app)
476
+ .post('/v1/tool/execute')
477
+ .set('x-api-key', 'rbac-key-12345')
478
+ .send(validToolCall({
479
+ tool: { name: 'http', capability: 'write' },
480
+ }));
481
+ // The reader role has tool:execute:read but not tool:execute:write
482
+ // However, the broad rbacToolExecute middleware checks tool:execute
483
+ // which the reader doesn't have either, so it may be blocked at the middleware level
484
+ expect([403]).toContain(res.status);
485
+ await rbacResult.gateway.shutdown();
486
+ });
487
+ });
488
+ // =========================================================================
489
+ // 6. POST /v1/tool/approve
490
+ // =========================================================================
491
+ describe('POST /v1/tool/approve', () => {
492
+ it('returns 400 when approval_token is missing', async () => {
493
+ const res = await (0, supertest_1.default)(app)
494
+ .post('/v1/tool/approve')
495
+ .send({ approved: true });
496
+ expect(res.status).toBe(400);
497
+ expect(res.body.error_code).toBe('VALIDATION_FAILED');
498
+ expect(res.body.error).toContain('approval_token');
499
+ });
500
+ it('returns { status: "ok" } on successful approval', async () => {
501
+ jest.spyOn(gateway, 'processApproval').mockResolvedValueOnce({
502
+ success: true,
503
+ });
504
+ const res = await (0, supertest_1.default)(app)
505
+ .post('/v1/tool/approve')
506
+ .send({ approval_token: 'valid-token-123', approved: true });
507
+ expect(res.status).toBe(200);
508
+ expect(res.body.status).toBe('ok');
509
+ expect(res.body.success).toBe(true);
510
+ });
511
+ it('returns 400 when processApproval returns success=false', async () => {
512
+ jest.spyOn(gateway, 'processApproval').mockResolvedValueOnce({
513
+ success: false,
514
+ error: 'Token expired',
515
+ });
516
+ const res = await (0, supertest_1.default)(app)
517
+ .post('/v1/tool/approve')
518
+ .send({ approval_token: 'expired-token', approved: true });
519
+ expect(res.status).toBe(400);
520
+ expect(res.body.error_code).toBe('VALIDATION_FAILED');
521
+ expect(res.body.error).toContain('Token expired');
522
+ });
523
+ it('returns 400 with default message when processApproval fails without error', async () => {
524
+ jest.spyOn(gateway, 'processApproval').mockResolvedValueOnce({
525
+ success: false,
526
+ });
527
+ const res = await (0, supertest_1.default)(app)
528
+ .post('/v1/tool/approve')
529
+ .send({ approval_token: 'bad-token' });
530
+ expect(res.status).toBe(400);
531
+ expect(res.body.error).toContain('Approval processing failed');
532
+ });
533
+ it('returns 500 when processApproval throws', async () => {
534
+ jest.spyOn(gateway, 'processApproval').mockRejectedValueOnce(new Error('internal failure'));
535
+ const res = await (0, supertest_1.default)(app)
536
+ .post('/v1/tool/approve')
537
+ .send({ approval_token: 'crash-token' });
538
+ expect(res.status).toBe(500);
539
+ expect(res.body.error_code).toBe('INTERNAL_ERROR');
540
+ });
541
+ it('returns 500 when processApproval throws non-Error', async () => {
542
+ jest.spyOn(gateway, 'processApproval').mockRejectedValueOnce('string crash');
543
+ const res = await (0, supertest_1.default)(app)
544
+ .post('/v1/tool/approve')
545
+ .send({ approval_token: 'crash-token' });
546
+ expect(res.status).toBe(500);
547
+ expect(res.body.error_code).toBe('INTERNAL_ERROR');
548
+ });
549
+ it('uses approved=true when approved field is omitted (defaults to approved !== false)', async () => {
550
+ const spy = jest.spyOn(gateway, 'processApproval').mockResolvedValueOnce({ success: true });
551
+ await (0, supertest_1.default)(app)
552
+ .post('/v1/tool/approve')
553
+ .send({ approval_token: 'token-no-approved-field' });
554
+ // approved !== false should be true when approved is undefined
555
+ expect(spy).toHaveBeenCalledWith('token-no-approved-field', expect.any(String), true, undefined, undefined);
556
+ });
557
+ });
558
+ // =========================================================================
559
+ // 7. POST /v1/usage/report
560
+ // =========================================================================
561
+ describe('POST /v1/usage/report', () => {
562
+ it('returns 400 when tool_call_id is missing', async () => {
563
+ const res = await (0, supertest_1.default)(app)
564
+ .post('/v1/usage/report')
565
+ .send({ task_id: 'task-001' });
566
+ expect(res.status).toBe(400);
567
+ expect(res.body.error_code).toBe('VALIDATION_FAILED');
568
+ expect(res.body.error).toContain('tool_call_id');
569
+ });
570
+ it('returns 400 when task_id is missing', async () => {
571
+ const res = await (0, supertest_1.default)(app)
572
+ .post('/v1/usage/report')
573
+ .send({ tool_call_id: 'tc-001' });
574
+ expect(res.status).toBe(400);
575
+ expect(res.body.error_code).toBe('VALIDATION_FAILED');
576
+ });
577
+ it('returns 400 when both tool_call_id and task_id are missing', async () => {
578
+ const res = await (0, supertest_1.default)(app)
579
+ .post('/v1/usage/report')
580
+ .send({});
581
+ expect(res.status).toBe(400);
582
+ });
583
+ it('returns { status: "ok" } on successful usage report', async () => {
584
+ jest.spyOn(gateway, 'reportUsage').mockImplementation(() => { });
585
+ const res = await (0, supertest_1.default)(app)
586
+ .post('/v1/usage/report')
587
+ .send({
588
+ tool_call_id: 'tc-001',
589
+ task_id: 'task-001',
590
+ actual_cost_usd: 0.05,
591
+ usage: { input_tokens: 100, output_tokens: 50 },
592
+ });
593
+ expect(res.status).toBe(200);
594
+ expect(res.body.status).toBe('ok');
595
+ });
596
+ it('returns 500 when reportUsage throws an Error', async () => {
597
+ jest.spyOn(gateway, 'reportUsage').mockImplementation(() => {
598
+ throw new Error('storage failure');
599
+ });
600
+ const res = await (0, supertest_1.default)(app)
601
+ .post('/v1/usage/report')
602
+ .send({ tool_call_id: 'tc-001', task_id: 'task-001' });
603
+ expect(res.status).toBe(500);
604
+ expect(res.body.error_code).toBe('INTERNAL_ERROR');
605
+ });
606
+ it('returns 500 when reportUsage throws a non-Error', async () => {
607
+ jest.spyOn(gateway, 'reportUsage').mockImplementation(() => {
608
+ throw 'string error';
609
+ });
610
+ const res = await (0, supertest_1.default)(app)
611
+ .post('/v1/usage/report')
612
+ .send({ tool_call_id: 'tc-001', task_id: 'task-001' });
613
+ expect(res.status).toBe(500);
614
+ expect(res.body.error_code).toBe('INTERNAL_ERROR');
615
+ });
616
+ });
617
+ // =========================================================================
618
+ // 8. Error handler
619
+ // =========================================================================
620
+ describe('Global error handler', () => {
621
+ it('returns 400 with VALIDATION_FAILED for invalid JSON body', async () => {
622
+ const res = await (0, supertest_1.default)(app)
623
+ .post('/v1/tool/execute')
624
+ .set('Content-Type', 'application/json')
625
+ .send('{ invalid json }');
626
+ expect(res.status).toBe(400);
627
+ expect(res.body.error_code).toBe('VALIDATION_FAILED');
628
+ expect(res.body.error).toContain('Invalid JSON');
629
+ });
630
+ it('returns 413 for body too large', async () => {
631
+ // The express.json() limit is 10MB; send > 10MB
632
+ const largeBody = 'x'.repeat(11 * 1024 * 1024);
633
+ const res = await (0, supertest_1.default)(app)
634
+ .post('/v1/tool/execute')
635
+ .set('Content-Type', 'application/json')
636
+ .send(largeBody);
637
+ expect(res.status).toBe(413);
638
+ expect(res.body.error_code).toBe('VALIDATION_FAILED');
639
+ expect(res.body.error).toContain('too large');
640
+ });
641
+ });
642
+ // =========================================================================
643
+ // 9. IP rate limiter
644
+ // =========================================================================
645
+ describe('IP rate limiter', () => {
646
+ it('allows requests within limit', async () => {
647
+ // Use a very tight rate limit
648
+ const limitedResult = (0, app_1.createApp)(testConfig({
649
+ public_rate_limit: { max_per_window: 5, window_ms: 60000 },
650
+ }));
651
+ const res = await (0, supertest_1.default)(limitedResult.app).get('/health');
652
+ expect(res.status).toBe(200);
653
+ await limitedResult.gateway.shutdown();
654
+ });
655
+ it('returns 429 when exceeding the rate limit', async () => {
656
+ const limitedResult = (0, app_1.createApp)(testConfig({
657
+ public_rate_limit: { max_per_window: 2, window_ms: 60000 },
658
+ }));
659
+ // Make requests up to the limit
660
+ await (0, supertest_1.default)(limitedResult.app).get('/health');
661
+ await (0, supertest_1.default)(limitedResult.app).get('/health');
662
+ // Third request should be blocked
663
+ const res = await (0, supertest_1.default)(limitedResult.app).get('/health');
664
+ expect(res.status).toBe(429);
665
+ expect(res.body.error_code).toBe('RATE_LIMIT_EXCEEDED');
666
+ expect(res.body.details).toHaveProperty('retry_after_ms');
667
+ await limitedResult.gateway.shutdown();
668
+ });
669
+ });
670
+ // =========================================================================
671
+ // Additional route coverage
672
+ // =========================================================================
673
+ describe('POST /v1/tool/execute - validation', () => {
674
+ it('returns 400 for missing required fields', async () => {
675
+ const res = await (0, supertest_1.default)(app)
676
+ .post('/v1/tool/execute')
677
+ .send({ tool_call_id: 'tc-bad' });
678
+ expect(res.status).toBe(400);
679
+ expect(res.body.error_code).toBe('VALIDATION_FAILED');
680
+ });
681
+ });
682
+ describe('POST /v1/tool/execute - status fallback to 500', () => {
683
+ it('returns 500 for an unknown result status', async () => {
684
+ const mockResult = {
685
+ tool_call_id: 'tc-000001',
686
+ task_id: 'task-001',
687
+ status: 'error',
688
+ policy: { decision: 'allow', reasons: [] },
689
+ dlp: { detected: [], redactions: [], severity: 'low' },
690
+ budget: { estimated_cost_usd: 0, spent_cost_usd_task: 0, remaining_cost_usd_task: 100 },
691
+ timing: { started_at: new Date().toISOString(), duration_ms: 10 },
692
+ };
693
+ jest.spyOn(gateway, 'execute').mockResolvedValueOnce(mockResult);
694
+ const res = await (0, supertest_1.default)(app)
695
+ .post('/v1/tool/execute')
696
+ .send(validToolCall());
697
+ expect(res.status).toBe(500);
698
+ });
699
+ });
700
+ describe('POST /v1/tool/approve - denial flow', () => {
701
+ it('handles explicit denied approval (approved=false)', async () => {
702
+ const spy = jest.spyOn(gateway, 'processApproval').mockResolvedValueOnce({ success: true });
703
+ const res = await (0, supertest_1.default)(app)
704
+ .post('/v1/tool/approve')
705
+ .send({
706
+ approval_token: 'deny-token',
707
+ approved: false,
708
+ reason: 'Not needed',
709
+ });
710
+ expect(res.status).toBe(200);
711
+ expect(spy).toHaveBeenCalledWith('deny-token', expect.any(String), false, 'Not needed', undefined);
712
+ });
713
+ });
714
+ });
715
+ //# sourceMappingURL=app-routes.test.js.map