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,1399 @@
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
+ const defaults_1 = require("../../src/config/defaults");
9
+ // ---------------------------------------------------------------------------
10
+ // Setup
11
+ // ---------------------------------------------------------------------------
12
+ let app;
13
+ let saasStores;
14
+ let gateway;
15
+ const now = new Date().toISOString();
16
+ function createTestConfig() {
17
+ return {
18
+ ...defaults_1.DEFAULT_CONFIG,
19
+ port: 0,
20
+ host: '127.0.0.1',
21
+ auth: {
22
+ ...defaults_1.DEFAULT_CONFIG.auth,
23
+ enabled: true,
24
+ api_keys: {},
25
+ rbac: {
26
+ enabled: false,
27
+ roles: {},
28
+ },
29
+ },
30
+ policy: {
31
+ pack_path: './policy-packs/dev_fast.yaml',
32
+ default_effect: 'DENY',
33
+ hot_reload: false,
34
+ },
35
+ audit: {
36
+ enabled: true,
37
+ log_dir: '',
38
+ console_output: false,
39
+ retention_days: 30,
40
+ },
41
+ rate_limit: {
42
+ enabled: false,
43
+ actor_max_per_window: 100,
44
+ workspace_max_per_window: 500,
45
+ window_ms: 60000,
46
+ },
47
+ oauth: {
48
+ enabled: true,
49
+ session_secret: 'test-session-secret-for-saas',
50
+ session_ttl_seconds: 3600,
51
+ },
52
+ };
53
+ }
54
+ beforeAll(() => {
55
+ const config = createTestConfig();
56
+ const result = (0, app_1.createApp)(config);
57
+ app = result.app;
58
+ saasStores = result.saasStores;
59
+ gateway = result.gateway;
60
+ });
61
+ beforeEach(() => {
62
+ // Seed fresh user, session, workspace, and membership before each test
63
+ saasStores.userStore.create({
64
+ id: 'u1',
65
+ email: 'test@test.com',
66
+ display_name: 'Test User',
67
+ avatar_url: 'https://img.example.com/avatar.png',
68
+ status: 'active',
69
+ onboarding_completed: false,
70
+ created_at: now,
71
+ updated_at: now,
72
+ });
73
+ saasStores.sessionStore.create({
74
+ id: 'sess1',
75
+ user_id: 'u1',
76
+ expires_at: new Date(Date.now() + 3600000).toISOString(),
77
+ last_active_at: now,
78
+ created_at: now,
79
+ });
80
+ });
81
+ afterEach(() => {
82
+ // Clean up all stores between tests to prevent cross-contamination
83
+ // We need to delete known items since in-memory stores don't have a clear method
84
+ saasStores.userStore.delete('u1');
85
+ saasStores.userStore.delete('u2');
86
+ saasStores.sessionStore.delete('sess1');
87
+ saasStores.sessionStore.delete('sess2');
88
+ // Clean up any workspaces/members/keys created during tests
89
+ const allWorkspaces = saasStores.workspaceStore.list();
90
+ for (const ws of allWorkspaces) {
91
+ const members = saasStores.workspaceMemberStore.getByWorkspace(ws.id);
92
+ for (const m of members) {
93
+ saasStores.workspaceMemberStore.delete(m.id);
94
+ }
95
+ const keys = saasStores.userApiKeyStore.getByWorkspace(ws.id);
96
+ for (const k of keys) {
97
+ saasStores.userApiKeyStore.delete(k.id);
98
+ }
99
+ saasStores.workspaceStore.delete(ws.id);
100
+ }
101
+ });
102
+ // ===========================================================================
103
+ // Tests
104
+ // ===========================================================================
105
+ describe('SaaS Routes', () => {
106
+ // -------------------------------------------------------------------------
107
+ // Authentication
108
+ // -------------------------------------------------------------------------
109
+ describe('401 without session cookie', () => {
110
+ it('returns 401 for GET /api/v1/user/profile without session', async () => {
111
+ const res = await (0, supertest_1.default)(app)
112
+ .get('/api/v1/user/profile')
113
+ .expect(401);
114
+ expect(res.body.error).toBeDefined();
115
+ });
116
+ it('returns 401 for GET /api/v1/workspaces without session', async () => {
117
+ const res = await (0, supertest_1.default)(app)
118
+ .get('/api/v1/workspaces')
119
+ .expect(401);
120
+ expect(res.body.error).toBeDefined();
121
+ });
122
+ it('returns 401 for POST /api/v1/workspaces without session', async () => {
123
+ const res = await (0, supertest_1.default)(app)
124
+ .post('/api/v1/workspaces')
125
+ .send({ name: 'Test' })
126
+ .expect(401);
127
+ expect(res.body.error).toBeDefined();
128
+ });
129
+ });
130
+ // -------------------------------------------------------------------------
131
+ // GET /api/v1/user/profile
132
+ // -------------------------------------------------------------------------
133
+ describe('GET /api/v1/user/profile', () => {
134
+ it('returns user profile with session cookie', async () => {
135
+ const res = await (0, supertest_1.default)(app)
136
+ .get('/api/v1/user/profile')
137
+ .set('Cookie', 'pn_session=sess1')
138
+ .expect(200);
139
+ expect(res.body.id).toBe('u1');
140
+ expect(res.body.email).toBe('test@test.com');
141
+ expect(res.body.display_name).toBe('Test User');
142
+ expect(res.body.avatar_url).toBe('https://img.example.com/avatar.png');
143
+ expect(res.body.status).toBe('active');
144
+ expect(res.body.onboarding_completed).toBe(false);
145
+ expect(res.body.created_at).toBe(now);
146
+ });
147
+ });
148
+ // -------------------------------------------------------------------------
149
+ // PUT /api/v1/user/profile
150
+ // -------------------------------------------------------------------------
151
+ describe('PUT /api/v1/user/profile', () => {
152
+ it('updates display_name', async () => {
153
+ const res = await (0, supertest_1.default)(app)
154
+ .put('/api/v1/user/profile')
155
+ .set('Cookie', 'pn_session=sess1')
156
+ .send({ display_name: 'Alice Updated' })
157
+ .expect(200);
158
+ expect(res.body.display_name).toBe('Alice Updated');
159
+ // Verify it was persisted
160
+ const user = saasStores.userStore.getById('u1');
161
+ expect(user?.display_name).toBe('Alice Updated');
162
+ });
163
+ it('returns 400 when display_name is missing', async () => {
164
+ const res = await (0, supertest_1.default)(app)
165
+ .put('/api/v1/user/profile')
166
+ .set('Cookie', 'pn_session=sess1')
167
+ .send({})
168
+ .expect(400);
169
+ expect(res.body.error).toContain('display_name');
170
+ });
171
+ it('returns 400 when display_name is empty string', async () => {
172
+ const res = await (0, supertest_1.default)(app)
173
+ .put('/api/v1/user/profile')
174
+ .set('Cookie', 'pn_session=sess1')
175
+ .send({ display_name: ' ' })
176
+ .expect(400);
177
+ expect(res.body.error).toContain('display_name');
178
+ });
179
+ it('returns 400 when display_name exceeds 100 characters', async () => {
180
+ const res = await (0, supertest_1.default)(app)
181
+ .put('/api/v1/user/profile')
182
+ .set('Cookie', 'pn_session=sess1')
183
+ .send({ display_name: 'x'.repeat(101) })
184
+ .expect(400);
185
+ expect(res.body.error).toContain('100 characters');
186
+ });
187
+ });
188
+ // -------------------------------------------------------------------------
189
+ // PUT /api/v1/user/onboarding
190
+ // -------------------------------------------------------------------------
191
+ describe('PUT /api/v1/user/onboarding', () => {
192
+ it('marks onboarding as complete', async () => {
193
+ const res = await (0, supertest_1.default)(app)
194
+ .put('/api/v1/user/onboarding')
195
+ .set('Cookie', 'pn_session=sess1')
196
+ .expect(200);
197
+ expect(res.body.onboarding_completed).toBe(true);
198
+ // Verify persisted
199
+ const user = saasStores.userStore.getById('u1');
200
+ expect(user?.onboarding_completed).toBe(true);
201
+ });
202
+ });
203
+ // -------------------------------------------------------------------------
204
+ // GET /api/v1/workspaces
205
+ // -------------------------------------------------------------------------
206
+ describe('GET /api/v1/workspaces', () => {
207
+ it('returns user workspaces', async () => {
208
+ // Create a workspace and membership
209
+ saasStores.workspaceStore.create({
210
+ id: 'ws-test-1',
211
+ name: 'Test Workspace',
212
+ slug: 'test-workspace',
213
+ owner_user_id: 'u1',
214
+ plan: 'free',
215
+ settings: {},
216
+ created_at: now,
217
+ updated_at: now,
218
+ });
219
+ saasStores.workspaceMemberStore.create({
220
+ id: 'mem-test-1',
221
+ workspace_id: 'ws-test-1',
222
+ user_id: 'u1',
223
+ role: 'owner',
224
+ joined_at: now,
225
+ });
226
+ const res = await (0, supertest_1.default)(app)
227
+ .get('/api/v1/workspaces')
228
+ .set('Cookie', 'pn_session=sess1')
229
+ .expect(200);
230
+ expect(res.body.workspaces).toHaveLength(1);
231
+ expect(res.body.workspaces[0].id).toBe('ws-test-1');
232
+ expect(res.body.workspaces[0].name).toBe('Test Workspace');
233
+ expect(res.body.workspaces[0].role).toBe('owner');
234
+ });
235
+ it('returns empty workspaces when user has no memberships', async () => {
236
+ const res = await (0, supertest_1.default)(app)
237
+ .get('/api/v1/workspaces')
238
+ .set('Cookie', 'pn_session=sess1')
239
+ .expect(200);
240
+ expect(res.body.workspaces).toEqual([]);
241
+ });
242
+ });
243
+ // -------------------------------------------------------------------------
244
+ // POST /api/v1/workspaces
245
+ // -------------------------------------------------------------------------
246
+ describe('POST /api/v1/workspaces', () => {
247
+ it('creates a workspace and membership', async () => {
248
+ const res = await (0, supertest_1.default)(app)
249
+ .post('/api/v1/workspaces')
250
+ .set('Cookie', 'pn_session=sess1')
251
+ .send({ name: 'New Workspace' })
252
+ .expect(201);
253
+ expect(res.body.name).toBe('New Workspace');
254
+ expect(res.body.slug).toBe('new-workspace');
255
+ expect(res.body.owner_user_id).toBe('u1');
256
+ expect(res.body.plan).toBe('free');
257
+ expect(res.body.id).toBeDefined();
258
+ // Verify membership was created
259
+ const members = saasStores.workspaceMemberStore.getByWorkspace(res.body.id);
260
+ expect(members).toHaveLength(1);
261
+ expect(members[0].user_id).toBe('u1');
262
+ expect(members[0].role).toBe('owner');
263
+ });
264
+ it('generates a slug from the name when slug is not provided', async () => {
265
+ const res = await (0, supertest_1.default)(app)
266
+ .post('/api/v1/workspaces')
267
+ .set('Cookie', 'pn_session=sess1')
268
+ .send({ name: 'My Cool Project!' })
269
+ .expect(201);
270
+ expect(res.body.slug).toBe('my-cool-project-');
271
+ });
272
+ it('uses the provided slug', async () => {
273
+ const res = await (0, supertest_1.default)(app)
274
+ .post('/api/v1/workspaces')
275
+ .set('Cookie', 'pn_session=sess1')
276
+ .send({ name: 'Test', slug: 'custom-slug' })
277
+ .expect(201);
278
+ expect(res.body.slug).toBe('custom-slug');
279
+ });
280
+ it('returns 400 when name is missing', async () => {
281
+ const res = await (0, supertest_1.default)(app)
282
+ .post('/api/v1/workspaces')
283
+ .set('Cookie', 'pn_session=sess1')
284
+ .send({})
285
+ .expect(400);
286
+ expect(res.body.error).toContain('name');
287
+ });
288
+ it('returns 409 when slug is already taken', async () => {
289
+ // Create first workspace
290
+ saasStores.workspaceStore.create({
291
+ id: 'ws-existing',
292
+ name: 'Existing',
293
+ slug: 'existing-slug',
294
+ owner_user_id: 'u1',
295
+ plan: 'free',
296
+ settings: {},
297
+ created_at: now,
298
+ updated_at: now,
299
+ });
300
+ const res = await (0, supertest_1.default)(app)
301
+ .post('/api/v1/workspaces')
302
+ .set('Cookie', 'pn_session=sess1')
303
+ .send({ name: 'New', slug: 'existing-slug' })
304
+ .expect(409);
305
+ expect(res.body.error).toContain('slug');
306
+ });
307
+ });
308
+ // -------------------------------------------------------------------------
309
+ // GET /api/v1/workspaces/:id
310
+ // -------------------------------------------------------------------------
311
+ describe('GET /api/v1/workspaces/:id', () => {
312
+ it('returns workspace details for a member', async () => {
313
+ saasStores.workspaceStore.create({
314
+ id: 'ws-detail-1',
315
+ name: 'Detail Workspace',
316
+ slug: 'detail-ws',
317
+ owner_user_id: 'u1',
318
+ plan: 'pro',
319
+ settings: { feature_x: true },
320
+ created_at: now,
321
+ updated_at: now,
322
+ });
323
+ saasStores.workspaceMemberStore.create({
324
+ id: 'mem-detail-1',
325
+ workspace_id: 'ws-detail-1',
326
+ user_id: 'u1',
327
+ role: 'owner',
328
+ joined_at: now,
329
+ });
330
+ const res = await (0, supertest_1.default)(app)
331
+ .get('/api/v1/workspaces/ws-detail-1')
332
+ .set('Cookie', 'pn_session=sess1')
333
+ .expect(200);
334
+ expect(res.body.id).toBe('ws-detail-1');
335
+ expect(res.body.name).toBe('Detail Workspace');
336
+ expect(res.body.plan).toBe('pro');
337
+ expect(res.body.role).toBe('owner');
338
+ });
339
+ it('returns 403 when user is not a member', async () => {
340
+ saasStores.workspaceStore.create({
341
+ id: 'ws-private',
342
+ name: 'Private Workspace',
343
+ slug: 'private-ws',
344
+ owner_user_id: 'u2',
345
+ plan: 'free',
346
+ settings: {},
347
+ created_at: now,
348
+ updated_at: now,
349
+ });
350
+ const res = await (0, supertest_1.default)(app)
351
+ .get('/api/v1/workspaces/ws-private')
352
+ .set('Cookie', 'pn_session=sess1')
353
+ .expect(403);
354
+ expect(res.body.error).toContain('Not a member');
355
+ });
356
+ it('returns 404 when workspace does not exist but user has no membership', async () => {
357
+ const res = await (0, supertest_1.default)(app)
358
+ .get('/api/v1/workspaces/ws-nonexistent')
359
+ .set('Cookie', 'pn_session=sess1')
360
+ .expect(403);
361
+ // The route checks membership first, so it returns 403 not 404
362
+ expect(res.body.error).toBeDefined();
363
+ });
364
+ });
365
+ // -------------------------------------------------------------------------
366
+ // POST /api/v1/workspaces/:id/api-keys
367
+ // -------------------------------------------------------------------------
368
+ describe('POST /api/v1/workspaces/:id/api-keys', () => {
369
+ beforeEach(() => {
370
+ saasStores.workspaceStore.create({
371
+ id: 'ws-keys',
372
+ name: 'Keys Workspace',
373
+ slug: 'keys-ws',
374
+ owner_user_id: 'u1',
375
+ plan: 'free',
376
+ settings: {},
377
+ created_at: now,
378
+ updated_at: now,
379
+ });
380
+ saasStores.workspaceMemberStore.create({
381
+ id: 'mem-keys',
382
+ workspace_id: 'ws-keys',
383
+ user_id: 'u1',
384
+ role: 'owner',
385
+ joined_at: now,
386
+ });
387
+ });
388
+ it('creates an API key and returns the plaintext key', async () => {
389
+ const res = await (0, supertest_1.default)(app)
390
+ .post('/api/v1/workspaces/ws-keys/api-keys')
391
+ .set('Cookie', 'pn_session=sess1')
392
+ .send({ name: 'Production Key', roles: ['agent'] })
393
+ .expect(201);
394
+ expect(res.body.id).toBeDefined();
395
+ expect(res.body.key).toBeDefined();
396
+ expect(res.body.key).toMatch(/^pn_[0-9a-f]{64}$/);
397
+ expect(res.body.key_prefix).toBeDefined();
398
+ expect(res.body.key_prefix.startsWith('pn_')).toBe(true);
399
+ expect(res.body.name).toBe('Production Key');
400
+ expect(res.body.roles).toEqual(['agent']);
401
+ expect(res.body.workspace_id).toBe('ws-keys');
402
+ expect(res.body.created_at).toBeDefined();
403
+ });
404
+ it('stores the key hash (not the plaintext) in the store', async () => {
405
+ const res = await (0, supertest_1.default)(app)
406
+ .post('/api/v1/workspaces/ws-keys/api-keys')
407
+ .set('Cookie', 'pn_session=sess1')
408
+ .send({ name: 'Stored Key' })
409
+ .expect(201);
410
+ const storedKey = saasStores.userApiKeyStore.getById(res.body.id);
411
+ expect(storedKey).toBeDefined();
412
+ expect(storedKey?.key_hash).toBeDefined();
413
+ // The hash should not equal the plaintext key
414
+ expect(storedKey?.key_hash).not.toBe(res.body.key);
415
+ });
416
+ it('defaults roles to [agent] when not provided', async () => {
417
+ const res = await (0, supertest_1.default)(app)
418
+ .post('/api/v1/workspaces/ws-keys/api-keys')
419
+ .set('Cookie', 'pn_session=sess1')
420
+ .send({ name: 'Default Roles Key' })
421
+ .expect(201);
422
+ expect(res.body.roles).toEqual(['agent']);
423
+ });
424
+ it('returns 400 when name is missing', async () => {
425
+ const res = await (0, supertest_1.default)(app)
426
+ .post('/api/v1/workspaces/ws-keys/api-keys')
427
+ .set('Cookie', 'pn_session=sess1')
428
+ .send({})
429
+ .expect(400);
430
+ expect(res.body.error).toContain('name');
431
+ });
432
+ it('returns 403 when user is not owner or admin', async () => {
433
+ // Add user as member (not owner/admin)
434
+ saasStores.workspaceMemberStore.delete('mem-keys');
435
+ saasStores.workspaceMemberStore.create({
436
+ id: 'mem-keys-viewer',
437
+ workspace_id: 'ws-keys',
438
+ user_id: 'u1',
439
+ role: 'member',
440
+ joined_at: now,
441
+ });
442
+ const res = await (0, supertest_1.default)(app)
443
+ .post('/api/v1/workspaces/ws-keys/api-keys')
444
+ .set('Cookie', 'pn_session=sess1')
445
+ .send({ name: 'Unauthorized Key' })
446
+ .expect(403);
447
+ expect(res.body.error).toContain('owners and admins');
448
+ });
449
+ });
450
+ // -------------------------------------------------------------------------
451
+ // GET /api/v1/workspaces/:id/api-keys
452
+ // -------------------------------------------------------------------------
453
+ describe('GET /api/v1/workspaces/:id/api-keys', () => {
454
+ beforeEach(() => {
455
+ saasStores.workspaceStore.create({
456
+ id: 'ws-list-keys',
457
+ name: 'List Keys Workspace',
458
+ slug: 'list-keys-ws',
459
+ owner_user_id: 'u1',
460
+ plan: 'free',
461
+ settings: {},
462
+ created_at: now,
463
+ updated_at: now,
464
+ });
465
+ saasStores.workspaceMemberStore.create({
466
+ id: 'mem-list-keys',
467
+ workspace_id: 'ws-list-keys',
468
+ user_id: 'u1',
469
+ role: 'owner',
470
+ joined_at: now,
471
+ });
472
+ });
473
+ it('lists keys without returning the key hash', async () => {
474
+ // Create some keys in the store directly
475
+ saasStores.userApiKeyStore.create({
476
+ id: 'k1',
477
+ key_hash: 'hash-secret-1',
478
+ key_prefix: 'pn_abc123',
479
+ user_id: 'u1',
480
+ workspace_id: 'ws-list-keys',
481
+ name: 'Key One',
482
+ roles: ['agent'],
483
+ tags: [],
484
+ revoked: false,
485
+ created_at: now,
486
+ });
487
+ saasStores.userApiKeyStore.create({
488
+ id: 'k2',
489
+ key_hash: 'hash-secret-2',
490
+ key_prefix: 'pn_def456',
491
+ user_id: 'u1',
492
+ workspace_id: 'ws-list-keys',
493
+ name: 'Key Two',
494
+ roles: ['admin'],
495
+ tags: [],
496
+ revoked: true,
497
+ created_at: now,
498
+ });
499
+ const res = await (0, supertest_1.default)(app)
500
+ .get('/api/v1/workspaces/ws-list-keys/api-keys')
501
+ .set('Cookie', 'pn_session=sess1')
502
+ .expect(200);
503
+ expect(res.body.api_keys).toHaveLength(2);
504
+ // Verify no hash is returned
505
+ for (const key of res.body.api_keys) {
506
+ expect(key.key_hash).toBeUndefined();
507
+ expect(key.id).toBeDefined();
508
+ expect(key.key_prefix).toBeDefined();
509
+ expect(key.name).toBeDefined();
510
+ expect(key.roles).toBeDefined();
511
+ expect(key.revoked).toBeDefined();
512
+ expect(key.created_at).toBeDefined();
513
+ }
514
+ });
515
+ it('returns empty array when no keys exist', async () => {
516
+ const res = await (0, supertest_1.default)(app)
517
+ .get('/api/v1/workspaces/ws-list-keys/api-keys')
518
+ .set('Cookie', 'pn_session=sess1')
519
+ .expect(200);
520
+ expect(res.body.api_keys).toEqual([]);
521
+ });
522
+ it('returns 403 when user is not a member', async () => {
523
+ saasStores.workspaceMemberStore.delete('mem-list-keys');
524
+ const res = await (0, supertest_1.default)(app)
525
+ .get('/api/v1/workspaces/ws-list-keys/api-keys')
526
+ .set('Cookie', 'pn_session=sess1')
527
+ .expect(403);
528
+ expect(res.body.error).toContain('Not a member');
529
+ });
530
+ });
531
+ // -------------------------------------------------------------------------
532
+ // DELETE /api/v1/workspaces/:id/api-keys/:keyId
533
+ // -------------------------------------------------------------------------
534
+ describe('DELETE /api/v1/workspaces/:id/api-keys/:keyId', () => {
535
+ beforeEach(() => {
536
+ saasStores.workspaceStore.create({
537
+ id: 'ws-del-keys',
538
+ name: 'Del Keys WS',
539
+ slug: 'del-keys-ws',
540
+ owner_user_id: 'u1',
541
+ plan: 'free',
542
+ settings: {},
543
+ created_at: now,
544
+ updated_at: now,
545
+ });
546
+ saasStores.workspaceMemberStore.create({
547
+ id: 'mem-del-keys',
548
+ workspace_id: 'ws-del-keys',
549
+ user_id: 'u1',
550
+ role: 'owner',
551
+ joined_at: now,
552
+ });
553
+ saasStores.userApiKeyStore.create({
554
+ id: 'key-to-revoke',
555
+ key_hash: 'hash-revokable',
556
+ key_prefix: 'pn_rev123',
557
+ user_id: 'u1',
558
+ workspace_id: 'ws-del-keys',
559
+ name: 'Revokable Key',
560
+ roles: ['agent'],
561
+ tags: [],
562
+ revoked: false,
563
+ created_at: now,
564
+ });
565
+ });
566
+ it('revokes the API key', async () => {
567
+ const res = await (0, supertest_1.default)(app)
568
+ .delete('/api/v1/workspaces/ws-del-keys/api-keys/key-to-revoke')
569
+ .set('Cookie', 'pn_session=sess1')
570
+ .expect(200);
571
+ expect(res.body.status).toBe('ok');
572
+ expect(res.body.id).toBe('key-to-revoke');
573
+ // Verify the key is now revoked
574
+ const key = saasStores.userApiKeyStore.getById('key-to-revoke');
575
+ expect(key?.revoked).toBe(true);
576
+ });
577
+ it('returns 404 when key does not exist', async () => {
578
+ const res = await (0, supertest_1.default)(app)
579
+ .delete('/api/v1/workspaces/ws-del-keys/api-keys/nonexistent-key')
580
+ .set('Cookie', 'pn_session=sess1')
581
+ .expect(404);
582
+ expect(res.body.error).toContain('not found');
583
+ });
584
+ it('returns 404 when key belongs to a different workspace', async () => {
585
+ // Create key in a different workspace
586
+ saasStores.userApiKeyStore.create({
587
+ id: 'key-other-ws',
588
+ key_hash: 'hash-other',
589
+ key_prefix: 'pn_other',
590
+ user_id: 'u1',
591
+ workspace_id: 'ws-other',
592
+ name: 'Other WS Key',
593
+ roles: ['agent'],
594
+ tags: [],
595
+ revoked: false,
596
+ created_at: now,
597
+ });
598
+ const res = await (0, supertest_1.default)(app)
599
+ .delete('/api/v1/workspaces/ws-del-keys/api-keys/key-other-ws')
600
+ .set('Cookie', 'pn_session=sess1')
601
+ .expect(404);
602
+ expect(res.body.error).toContain('not found');
603
+ });
604
+ it('returns 403 when user is not owner or admin', async () => {
605
+ saasStores.workspaceMemberStore.delete('mem-del-keys');
606
+ saasStores.workspaceMemberStore.create({
607
+ id: 'mem-del-keys-member',
608
+ workspace_id: 'ws-del-keys',
609
+ user_id: 'u1',
610
+ role: 'viewer',
611
+ joined_at: now,
612
+ });
613
+ const res = await (0, supertest_1.default)(app)
614
+ .delete('/api/v1/workspaces/ws-del-keys/api-keys/key-to-revoke')
615
+ .set('Cookie', 'pn_session=sess1')
616
+ .expect(403);
617
+ expect(res.body.error).toContain('owners and admins');
618
+ });
619
+ });
620
+ // -------------------------------------------------------------------------
621
+ // GET /api/v1/workspaces/:id/approvals
622
+ // -------------------------------------------------------------------------
623
+ describe('GET /api/v1/workspaces/:id/approvals', () => {
624
+ beforeEach(() => {
625
+ saasStores.workspaceStore.create({
626
+ id: 'ws-approvals',
627
+ name: 'Approvals Workspace',
628
+ slug: 'approvals-ws',
629
+ owner_user_id: 'u1',
630
+ plan: 'free',
631
+ settings: {},
632
+ created_at: now,
633
+ updated_at: now,
634
+ });
635
+ saasStores.workspaceMemberStore.create({
636
+ id: 'mem-approvals',
637
+ workspace_id: 'ws-approvals',
638
+ user_id: 'u1',
639
+ role: 'owner',
640
+ joined_at: now,
641
+ });
642
+ });
643
+ it('returns empty approvals when none exist', async () => {
644
+ const res = await (0, supertest_1.default)(app)
645
+ .get('/api/v1/workspaces/ws-approvals/approvals')
646
+ .set('Cookie', 'pn_session=sess1')
647
+ .expect(200);
648
+ expect(res.body.approvals).toEqual([]);
649
+ });
650
+ it('returns pending approvals for the workspace', async () => {
651
+ // Create a pending approval via the gateway
652
+ const toolCall = {
653
+ tool_call_id: 'tc-approval-1',
654
+ task_id: 'task-approval-1',
655
+ workspace_id: 'ws-approvals',
656
+ actor: { id: 'agent-1', type: 'agent' },
657
+ source: { platform: 'test' },
658
+ tool: { name: 'http.request', version: '1.0', capability: 'write' },
659
+ args: { method: 'DELETE', url: 'https://example.com/resource' },
660
+ timestamp: new Date().toISOString(),
661
+ };
662
+ const { approval } = gateway.getApprovalManager().createApproval(toolCall, 'admin', 'Destructive operation');
663
+ const res = await (0, supertest_1.default)(app)
664
+ .get('/api/v1/workspaces/ws-approvals/approvals')
665
+ .set('Cookie', 'pn_session=sess1')
666
+ .expect(200);
667
+ expect(res.body.approvals).toHaveLength(1);
668
+ expect(res.body.approvals[0].tool_name).toBe('http.request');
669
+ expect(res.body.approvals[0].reason).toBe('Destructive operation');
670
+ // Ensure JWT token is NOT exposed
671
+ expect(res.body.approvals[0].token).toBeUndefined();
672
+ });
673
+ it('returns 403 when user is not a member', async () => {
674
+ saasStores.workspaceMemberStore.delete('mem-approvals');
675
+ const res = await (0, supertest_1.default)(app)
676
+ .get('/api/v1/workspaces/ws-approvals/approvals')
677
+ .set('Cookie', 'pn_session=sess1')
678
+ .expect(403);
679
+ expect(res.body.error).toContain('Not a member');
680
+ });
681
+ });
682
+ // -------------------------------------------------------------------------
683
+ // POST /api/v1/workspaces/:id/approvals/:approvalId/approve
684
+ // -------------------------------------------------------------------------
685
+ describe('POST /api/v1/workspaces/:id/approvals/:approvalId/approve', () => {
686
+ beforeEach(() => {
687
+ gateway.getApprovalManager().clear();
688
+ saasStores.workspaceStore.create({
689
+ id: 'ws-approve',
690
+ name: 'Approve Workspace',
691
+ slug: 'approve-ws',
692
+ owner_user_id: 'u1',
693
+ plan: 'free',
694
+ settings: {},
695
+ created_at: now,
696
+ updated_at: now,
697
+ });
698
+ saasStores.workspaceMemberStore.create({
699
+ id: 'mem-approve',
700
+ workspace_id: 'ws-approve',
701
+ user_id: 'u1',
702
+ role: 'owner',
703
+ joined_at: now,
704
+ });
705
+ });
706
+ it('approves a pending approval', async () => {
707
+ const toolCall = {
708
+ tool_call_id: 'tc-approve-2',
709
+ task_id: 'task-approve-2',
710
+ workspace_id: 'ws-approve',
711
+ actor: { id: 'agent-2', type: 'agent' },
712
+ source: { platform: 'test' },
713
+ tool: { name: 'http.request', version: '1.0', capability: 'write' },
714
+ args: { method: 'POST', url: 'https://example.com/create' },
715
+ timestamp: new Date().toISOString(),
716
+ };
717
+ const { approval } = gateway.getApprovalManager().createApproval(toolCall, 'admin', 'Needs review');
718
+ const res = await (0, supertest_1.default)(app)
719
+ .post(`/api/v1/workspaces/ws-approve/approvals/${approval.approval_id}/approve`)
720
+ .set('Cookie', 'pn_session=sess1')
721
+ .expect(200);
722
+ expect(res.body.status).toBe('approved');
723
+ expect(res.body.approval_id).toBe(approval.approval_id);
724
+ // Verify the approval is now approved
725
+ const updated = gateway.getApprovalManager().getApproval(approval.approval_id);
726
+ expect(updated?.status).toBe('approved');
727
+ });
728
+ it('returns 404 for non-existent approval', async () => {
729
+ const res = await (0, supertest_1.default)(app)
730
+ .post('/api/v1/workspaces/ws-approve/approvals/nonexistent/approve')
731
+ .set('Cookie', 'pn_session=sess1')
732
+ .expect(404);
733
+ expect(res.body.error).toContain('not found');
734
+ });
735
+ it('returns 404 when approval belongs to different workspace', async () => {
736
+ const toolCall = {
737
+ tool_call_id: 'tc-approve-other',
738
+ task_id: 'task-approve-other',
739
+ workspace_id: 'ws-other-approval',
740
+ actor: { id: 'agent-3', type: 'agent' },
741
+ source: { platform: 'test' },
742
+ tool: { name: 'http.request', version: '1.0', capability: 'read' },
743
+ args: { method: 'GET', url: 'https://example.com' },
744
+ timestamp: new Date().toISOString(),
745
+ };
746
+ const { approval } = gateway.getApprovalManager().createApproval(toolCall, 'admin', 'Test');
747
+ const res = await (0, supertest_1.default)(app)
748
+ .post(`/api/v1/workspaces/ws-approve/approvals/${approval.approval_id}/approve`)
749
+ .set('Cookie', 'pn_session=sess1')
750
+ .expect(404);
751
+ expect(res.body.error).toContain('not found');
752
+ });
753
+ });
754
+ // -------------------------------------------------------------------------
755
+ // POST /api/v1/workspaces/:id/approvals/:approvalId/deny
756
+ // -------------------------------------------------------------------------
757
+ describe('POST /api/v1/workspaces/:id/approvals/:approvalId/deny', () => {
758
+ beforeEach(() => {
759
+ gateway.getApprovalManager().clear();
760
+ saasStores.workspaceStore.create({
761
+ id: 'ws-deny',
762
+ name: 'Deny Workspace',
763
+ slug: 'deny-ws',
764
+ owner_user_id: 'u1',
765
+ plan: 'free',
766
+ settings: {},
767
+ created_at: now,
768
+ updated_at: now,
769
+ });
770
+ saasStores.workspaceMemberStore.create({
771
+ id: 'mem-deny',
772
+ workspace_id: 'ws-deny',
773
+ user_id: 'u1',
774
+ role: 'owner',
775
+ joined_at: now,
776
+ });
777
+ });
778
+ it('denies a pending approval', async () => {
779
+ const toolCall = {
780
+ tool_call_id: 'tc-deny-1',
781
+ task_id: 'task-deny-1',
782
+ workspace_id: 'ws-deny',
783
+ actor: { id: 'agent-4', type: 'agent' },
784
+ source: { platform: 'test' },
785
+ tool: { name: 'http.request', version: '1.0', capability: 'delete' },
786
+ args: { method: 'DELETE', url: 'https://example.com/resource' },
787
+ timestamp: new Date().toISOString(),
788
+ };
789
+ const { approval } = gateway.getApprovalManager().createApproval(toolCall, 'admin', 'Destructive');
790
+ const res = await (0, supertest_1.default)(app)
791
+ .post(`/api/v1/workspaces/ws-deny/approvals/${approval.approval_id}/deny`)
792
+ .set('Cookie', 'pn_session=sess1')
793
+ .send({ reason: 'Not authorized for this resource' })
794
+ .expect(200);
795
+ expect(res.body.status).toBe('denied');
796
+ expect(res.body.approval_id).toBe(approval.approval_id);
797
+ // Verify the approval is now denied
798
+ const updated = gateway.getApprovalManager().getApproval(approval.approval_id);
799
+ expect(updated?.status).toBe('denied');
800
+ });
801
+ it('uses default reason when none provided', async () => {
802
+ const toolCall = {
803
+ tool_call_id: 'tc-deny-2',
804
+ task_id: 'task-deny-2',
805
+ workspace_id: 'ws-deny',
806
+ actor: { id: 'agent-5', type: 'agent' },
807
+ source: { platform: 'test' },
808
+ tool: { name: 'http.request', version: '1.0', capability: 'write' },
809
+ args: { method: 'PUT', url: 'https://example.com/update' },
810
+ timestamp: new Date().toISOString(),
811
+ };
812
+ const { approval } = gateway.getApprovalManager().createApproval(toolCall, 'admin', 'Test');
813
+ const res = await (0, supertest_1.default)(app)
814
+ .post(`/api/v1/workspaces/ws-deny/approvals/${approval.approval_id}/deny`)
815
+ .set('Cookie', 'pn_session=sess1')
816
+ .expect(200);
817
+ expect(res.body.status).toBe('denied');
818
+ });
819
+ });
820
+ // -------------------------------------------------------------------------
821
+ // GET /api/v1/workspaces/:id/traces/:taskId
822
+ // -------------------------------------------------------------------------
823
+ describe('GET /api/v1/workspaces/:id/traces/:taskId', () => {
824
+ beforeEach(() => {
825
+ saasStores.workspaceStore.create({
826
+ id: 'ws-trace-detail',
827
+ name: 'Trace Detail Workspace',
828
+ slug: 'trace-detail-ws',
829
+ owner_user_id: 'u1',
830
+ plan: 'free',
831
+ settings: {},
832
+ created_at: now,
833
+ updated_at: now,
834
+ });
835
+ saasStores.workspaceMemberStore.create({
836
+ id: 'mem-trace-detail',
837
+ workspace_id: 'ws-trace-detail',
838
+ user_id: 'u1',
839
+ role: 'owner',
840
+ joined_at: now,
841
+ });
842
+ });
843
+ it('returns events for a task', async () => {
844
+ const res = await (0, supertest_1.default)(app)
845
+ .get('/api/v1/workspaces/ws-trace-detail/traces/some-task-id')
846
+ .set('Cookie', 'pn_session=sess1')
847
+ .expect(200);
848
+ expect(res.body.task_id).toBe('some-task-id');
849
+ expect(Array.isArray(res.body.events)).toBe(true);
850
+ expect(typeof res.body.total).toBe('number');
851
+ });
852
+ it('returns empty events for unknown task', async () => {
853
+ const res = await (0, supertest_1.default)(app)
854
+ .get('/api/v1/workspaces/ws-trace-detail/traces/nonexistent-task')
855
+ .set('Cookie', 'pn_session=sess1')
856
+ .expect(200);
857
+ expect(res.body.task_id).toBe('nonexistent-task');
858
+ expect(res.body.events).toEqual([]);
859
+ expect(res.body.total).toBe(0);
860
+ });
861
+ it('returns 403 when user is not a member', async () => {
862
+ saasStores.workspaceMemberStore.delete('mem-trace-detail');
863
+ const res = await (0, supertest_1.default)(app)
864
+ .get('/api/v1/workspaces/ws-trace-detail/traces/some-task')
865
+ .set('Cookie', 'pn_session=sess1')
866
+ .expect(403);
867
+ expect(res.body.error).toContain('Not a member');
868
+ });
869
+ });
870
+ // -------------------------------------------------------------------------
871
+ // GET /api/v1/workspaces/:id/policies
872
+ // -------------------------------------------------------------------------
873
+ describe('GET /api/v1/workspaces/:id/policies', () => {
874
+ beforeEach(() => {
875
+ saasStores.workspaceStore.create({
876
+ id: 'ws-policies',
877
+ name: 'Policies Workspace',
878
+ slug: 'policies-ws',
879
+ owner_user_id: 'u1',
880
+ plan: 'free',
881
+ settings: {},
882
+ created_at: now,
883
+ updated_at: now,
884
+ });
885
+ saasStores.workspaceMemberStore.create({
886
+ id: 'mem-policies',
887
+ workspace_id: 'ws-policies',
888
+ user_id: 'u1',
889
+ role: 'owner',
890
+ joined_at: now,
891
+ });
892
+ });
893
+ it('returns the current policy pack', async () => {
894
+ const res = await (0, supertest_1.default)(app)
895
+ .get('/api/v1/workspaces/ws-policies/policies')
896
+ .set('Cookie', 'pn_session=sess1')
897
+ .expect(200);
898
+ expect(res.body.policy).toBeDefined();
899
+ expect(res.body.policy.name).toBeDefined();
900
+ expect(res.body.policy.version).toBeDefined();
901
+ expect(Array.isArray(res.body.policy.rules)).toBe(true);
902
+ });
903
+ it('returns 403 when user is not a member', async () => {
904
+ saasStores.workspaceMemberStore.delete('mem-policies');
905
+ const res = await (0, supertest_1.default)(app)
906
+ .get('/api/v1/workspaces/ws-policies/policies')
907
+ .set('Cookie', 'pn_session=sess1')
908
+ .expect(403);
909
+ expect(res.body.error).toContain('Not a member');
910
+ });
911
+ });
912
+ // -------------------------------------------------------------------------
913
+ // GET /api/v1/workspaces/:id/security
914
+ // -------------------------------------------------------------------------
915
+ describe('GET /api/v1/workspaces/:id/security', () => {
916
+ beforeEach(() => {
917
+ saasStores.workspaceStore.create({
918
+ id: 'ws-security',
919
+ name: 'Security Workspace',
920
+ slug: 'security-ws',
921
+ owner_user_id: 'u1',
922
+ plan: 'free',
923
+ settings: {},
924
+ created_at: now,
925
+ updated_at: now,
926
+ });
927
+ saasStores.workspaceMemberStore.create({
928
+ id: 'mem-security',
929
+ workspace_id: 'ws-security',
930
+ user_id: 'u1',
931
+ role: 'owner',
932
+ joined_at: now,
933
+ });
934
+ });
935
+ it('returns empty events and stats when no DLP events exist', async () => {
936
+ const res = await (0, supertest_1.default)(app)
937
+ .get('/api/v1/workspaces/ws-security/security')
938
+ .set('Cookie', 'pn_session=sess1')
939
+ .expect(200);
940
+ expect(res.body.events).toEqual([]);
941
+ expect(res.body.total).toBe(0);
942
+ expect(res.body.stats).toBeDefined();
943
+ expect(res.body.stats.total).toBe(0);
944
+ expect(res.body.stats.high).toBe(0);
945
+ expect(res.body.stats.medium).toBe(0);
946
+ expect(res.body.stats.low).toBe(0);
947
+ expect(res.body.stats.unique_patterns).toBe(0);
948
+ });
949
+ it('returns DLP events with severity stats when events exist', async () => {
950
+ // Seed DLP events
951
+ const auditLogger = gateway.getAuditLogger();
952
+ auditLogger.log({
953
+ event_type: 'DLP_SCANNED',
954
+ tool_call_id: 'tc-dlp-1',
955
+ task_id: 'task-dlp-1',
956
+ workspace_id: 'ws-security',
957
+ actor_id: 'agent-dlp',
958
+ tool_name: 'http.request',
959
+ metadata: { severity: 'high', detected: ['aws_key', 'api_token'], redaction_count: 2 },
960
+ });
961
+ auditLogger.log({
962
+ event_type: 'DLP_SCANNED',
963
+ tool_call_id: 'tc-dlp-2',
964
+ task_id: 'task-dlp-2',
965
+ workspace_id: 'ws-security',
966
+ actor_id: 'agent-dlp',
967
+ tool_name: 'http.post',
968
+ metadata: { severity: 'medium', detected: ['email_address'], redaction_count: 1 },
969
+ });
970
+ const res = await (0, supertest_1.default)(app)
971
+ .get('/api/v1/workspaces/ws-security/security')
972
+ .set('Cookie', 'pn_session=sess1')
973
+ .expect(200);
974
+ expect(res.body.events).toHaveLength(2);
975
+ expect(res.body.total).toBe(2);
976
+ expect(res.body.stats.total).toBe(2);
977
+ expect(res.body.stats.high).toBe(1);
978
+ expect(res.body.stats.medium).toBe(1);
979
+ expect(res.body.stats.low).toBe(0);
980
+ expect(res.body.stats.unique_patterns).toBe(3); // aws_key, api_token, email_address
981
+ });
982
+ it('returns 403 when user is not a member', async () => {
983
+ saasStores.workspaceMemberStore.delete('mem-security');
984
+ const res = await (0, supertest_1.default)(app)
985
+ .get('/api/v1/workspaces/ws-security/security')
986
+ .set('Cookie', 'pn_session=sess1')
987
+ .expect(403);
988
+ expect(res.body.error).toContain('Not a member');
989
+ });
990
+ });
991
+ // -------------------------------------------------------------------------
992
+ // POST /api/v1/workspaces/:id/switch
993
+ // -------------------------------------------------------------------------
994
+ describe('POST /api/v1/workspaces/:id/switch', () => {
995
+ beforeEach(() => {
996
+ saasStores.workspaceStore.create({
997
+ id: 'ws-switch-1',
998
+ name: 'Switch WS 1',
999
+ slug: 'switch-ws-1',
1000
+ owner_user_id: 'u1',
1001
+ plan: 'free',
1002
+ settings: {},
1003
+ created_at: now,
1004
+ updated_at: now,
1005
+ });
1006
+ saasStores.workspaceMemberStore.create({
1007
+ id: 'mem-switch-1',
1008
+ workspace_id: 'ws-switch-1',
1009
+ user_id: 'u1',
1010
+ role: 'owner',
1011
+ joined_at: now,
1012
+ });
1013
+ saasStores.workspaceStore.create({
1014
+ id: 'ws-switch-2',
1015
+ name: 'Switch WS 2',
1016
+ slug: 'switch-ws-2',
1017
+ owner_user_id: 'u1',
1018
+ plan: 'pro',
1019
+ settings: {},
1020
+ created_at: now,
1021
+ updated_at: now,
1022
+ });
1023
+ saasStores.workspaceMemberStore.create({
1024
+ id: 'mem-switch-2',
1025
+ workspace_id: 'ws-switch-2',
1026
+ user_id: 'u1',
1027
+ role: 'admin',
1028
+ joined_at: now,
1029
+ });
1030
+ });
1031
+ it('switches workspace and returns workspace with role', async () => {
1032
+ const res = await (0, supertest_1.default)(app)
1033
+ .post('/api/v1/workspaces/ws-switch-2/switch')
1034
+ .set('Cookie', 'pn_session=sess1')
1035
+ .expect(200);
1036
+ expect(res.body.id).toBe('ws-switch-2');
1037
+ expect(res.body.name).toBe('Switch WS 2');
1038
+ expect(res.body.role).toBe('admin');
1039
+ // Verify session was updated
1040
+ const session = saasStores.sessionStore.getById('sess1');
1041
+ expect(session?.workspace_id).toBe('ws-switch-2');
1042
+ });
1043
+ it('returns 403 when user is not a member', async () => {
1044
+ saasStores.workspaceMemberStore.delete('mem-switch-2');
1045
+ const res = await (0, supertest_1.default)(app)
1046
+ .post('/api/v1/workspaces/ws-switch-2/switch')
1047
+ .set('Cookie', 'pn_session=sess1')
1048
+ .expect(403);
1049
+ expect(res.body.error).toContain('Not a member');
1050
+ });
1051
+ });
1052
+ // -------------------------------------------------------------------------
1053
+ // GET /api/v1/workspaces/:id/members
1054
+ // -------------------------------------------------------------------------
1055
+ describe('GET /api/v1/workspaces/:id/members', () => {
1056
+ beforeEach(() => {
1057
+ saasStores.workspaceStore.create({
1058
+ id: 'ws-members',
1059
+ name: 'Members Workspace',
1060
+ slug: 'members-ws',
1061
+ owner_user_id: 'u1',
1062
+ plan: 'free',
1063
+ settings: {},
1064
+ created_at: now,
1065
+ updated_at: now,
1066
+ });
1067
+ saasStores.workspaceMemberStore.create({
1068
+ id: 'mem-members-1',
1069
+ workspace_id: 'ws-members',
1070
+ user_id: 'u1',
1071
+ role: 'owner',
1072
+ joined_at: now,
1073
+ });
1074
+ });
1075
+ it('returns enriched member list with viewer role', async () => {
1076
+ const res = await (0, supertest_1.default)(app)
1077
+ .get('/api/v1/workspaces/ws-members/members')
1078
+ .set('Cookie', 'pn_session=sess1')
1079
+ .expect(200);
1080
+ expect(res.body.members).toHaveLength(1);
1081
+ expect(res.body.members[0].user_id).toBe('u1');
1082
+ expect(res.body.members[0].email).toBe('test@test.com');
1083
+ expect(res.body.members[0].display_name).toBe('Test User');
1084
+ expect(res.body.members[0].role).toBe('owner');
1085
+ expect(res.body.viewer_role).toBe('owner');
1086
+ });
1087
+ it('returns 403 when user is not a member', async () => {
1088
+ saasStores.workspaceMemberStore.delete('mem-members-1');
1089
+ const res = await (0, supertest_1.default)(app)
1090
+ .get('/api/v1/workspaces/ws-members/members')
1091
+ .set('Cookie', 'pn_session=sess1')
1092
+ .expect(403);
1093
+ expect(res.body.error).toContain('Not a member');
1094
+ });
1095
+ });
1096
+ // -------------------------------------------------------------------------
1097
+ // PUT /api/v1/workspaces/:id/members/:memberId (role change)
1098
+ // -------------------------------------------------------------------------
1099
+ describe('PUT /api/v1/workspaces/:id/members/:memberId', () => {
1100
+ beforeEach(() => {
1101
+ saasStores.userStore.create({
1102
+ id: 'u2',
1103
+ email: 'other@test.com',
1104
+ display_name: 'Other User',
1105
+ avatar_url: '',
1106
+ status: 'active',
1107
+ onboarding_completed: true,
1108
+ created_at: now,
1109
+ updated_at: now,
1110
+ });
1111
+ saasStores.workspaceStore.create({
1112
+ id: 'ws-role',
1113
+ name: 'Role WS',
1114
+ slug: 'role-ws',
1115
+ owner_user_id: 'u1',
1116
+ plan: 'free',
1117
+ settings: {},
1118
+ created_at: now,
1119
+ updated_at: now,
1120
+ });
1121
+ saasStores.workspaceMemberStore.create({
1122
+ id: 'mem-role-owner',
1123
+ workspace_id: 'ws-role',
1124
+ user_id: 'u1',
1125
+ role: 'owner',
1126
+ joined_at: now,
1127
+ });
1128
+ saasStores.workspaceMemberStore.create({
1129
+ id: 'mem-role-target',
1130
+ workspace_id: 'ws-role',
1131
+ user_id: 'u2',
1132
+ role: 'member',
1133
+ joined_at: now,
1134
+ });
1135
+ });
1136
+ it('changes a member role', async () => {
1137
+ const res = await (0, supertest_1.default)(app)
1138
+ .put('/api/v1/workspaces/ws-role/members/mem-role-target')
1139
+ .set('Cookie', 'pn_session=sess1')
1140
+ .send({ role: 'admin' })
1141
+ .expect(200);
1142
+ expect(res.body.role).toBe('admin');
1143
+ const updated = saasStores.workspaceMemberStore.getById('mem-role-target');
1144
+ expect(updated?.role).toBe('admin');
1145
+ });
1146
+ it('returns 400 when changing own role', async () => {
1147
+ const res = await (0, supertest_1.default)(app)
1148
+ .put('/api/v1/workspaces/ws-role/members/mem-role-owner')
1149
+ .set('Cookie', 'pn_session=sess1')
1150
+ .send({ role: 'admin' })
1151
+ .expect(400);
1152
+ expect(res.body.error).toContain('Cannot change your own role');
1153
+ });
1154
+ it('returns 400 for invalid role', async () => {
1155
+ const res = await (0, supertest_1.default)(app)
1156
+ .put('/api/v1/workspaces/ws-role/members/mem-role-target')
1157
+ .set('Cookie', 'pn_session=sess1')
1158
+ .send({ role: 'superadmin' })
1159
+ .expect(400);
1160
+ expect(res.body.error).toContain('role must be one of');
1161
+ });
1162
+ it('returns 403 when user is not owner or admin', async () => {
1163
+ saasStores.workspaceMemberStore.update('mem-role-owner', { role: 'member' });
1164
+ const res = await (0, supertest_1.default)(app)
1165
+ .put('/api/v1/workspaces/ws-role/members/mem-role-target')
1166
+ .set('Cookie', 'pn_session=sess1')
1167
+ .send({ role: 'admin' })
1168
+ .expect(403);
1169
+ expect(res.body.error).toContain('owners and admins');
1170
+ });
1171
+ it('returns 404 for non-existent member', async () => {
1172
+ const res = await (0, supertest_1.default)(app)
1173
+ .put('/api/v1/workspaces/ws-role/members/nonexistent-member')
1174
+ .set('Cookie', 'pn_session=sess1')
1175
+ .send({ role: 'admin' })
1176
+ .expect(404);
1177
+ expect(res.body.error).toContain('Member not found');
1178
+ });
1179
+ });
1180
+ // -------------------------------------------------------------------------
1181
+ // DELETE /api/v1/workspaces/:id/members/:memberId (remove member)
1182
+ // -------------------------------------------------------------------------
1183
+ describe('DELETE /api/v1/workspaces/:id/members/:memberId', () => {
1184
+ beforeEach(() => {
1185
+ saasStores.userStore.create({
1186
+ id: 'u2',
1187
+ email: 'other@test.com',
1188
+ display_name: 'Other User',
1189
+ avatar_url: '',
1190
+ status: 'active',
1191
+ onboarding_completed: true,
1192
+ created_at: now,
1193
+ updated_at: now,
1194
+ });
1195
+ saasStores.workspaceStore.create({
1196
+ id: 'ws-remove',
1197
+ name: 'Remove WS',
1198
+ slug: 'remove-ws',
1199
+ owner_user_id: 'u1',
1200
+ plan: 'free',
1201
+ settings: {},
1202
+ created_at: now,
1203
+ updated_at: now,
1204
+ });
1205
+ saasStores.workspaceMemberStore.create({
1206
+ id: 'mem-remove-owner',
1207
+ workspace_id: 'ws-remove',
1208
+ user_id: 'u1',
1209
+ role: 'owner',
1210
+ joined_at: now,
1211
+ });
1212
+ saasStores.workspaceMemberStore.create({
1213
+ id: 'mem-remove-target',
1214
+ workspace_id: 'ws-remove',
1215
+ user_id: 'u2',
1216
+ role: 'member',
1217
+ joined_at: now,
1218
+ });
1219
+ });
1220
+ it('removes a member', async () => {
1221
+ const res = await (0, supertest_1.default)(app)
1222
+ .delete('/api/v1/workspaces/ws-remove/members/mem-remove-target')
1223
+ .set('Cookie', 'pn_session=sess1')
1224
+ .expect(200);
1225
+ expect(res.body.status).toBe('ok');
1226
+ expect(res.body.id).toBe('mem-remove-target');
1227
+ expect(saasStores.workspaceMemberStore.getById('mem-remove-target')).toBeUndefined();
1228
+ });
1229
+ it('returns 400 when removing yourself', async () => {
1230
+ const res = await (0, supertest_1.default)(app)
1231
+ .delete('/api/v1/workspaces/ws-remove/members/mem-remove-owner')
1232
+ .set('Cookie', 'pn_session=sess1')
1233
+ .expect(400);
1234
+ expect(res.body.error).toContain('Cannot remove yourself');
1235
+ });
1236
+ it('returns 400 when removing the last owner', async () => {
1237
+ // Make target an owner
1238
+ saasStores.workspaceMemberStore.update('mem-remove-target', { role: 'owner' });
1239
+ // Remove the original owner's role so target is the only owner
1240
+ // Actually, u1 is still owner, so there are 2 owners. Let's remove u1's ownership first
1241
+ saasStores.workspaceMemberStore.update('mem-remove-owner', { role: 'admin' });
1242
+ const res = await (0, supertest_1.default)(app)
1243
+ .delete('/api/v1/workspaces/ws-remove/members/mem-remove-target')
1244
+ .set('Cookie', 'pn_session=sess1')
1245
+ .expect(400);
1246
+ expect(res.body.error).toContain('Cannot remove the last owner');
1247
+ });
1248
+ it('returns 403 when user is not owner or admin', async () => {
1249
+ saasStores.workspaceMemberStore.update('mem-remove-owner', { role: 'viewer' });
1250
+ const res = await (0, supertest_1.default)(app)
1251
+ .delete('/api/v1/workspaces/ws-remove/members/mem-remove-target')
1252
+ .set('Cookie', 'pn_session=sess1')
1253
+ .expect(403);
1254
+ expect(res.body.error).toContain('owners and admins');
1255
+ });
1256
+ });
1257
+ // -------------------------------------------------------------------------
1258
+ // POST /api/v1/workspaces/:id/members (add member by email)
1259
+ // -------------------------------------------------------------------------
1260
+ describe('POST /api/v1/workspaces/:id/members', () => {
1261
+ beforeEach(() => {
1262
+ saasStores.userStore.create({
1263
+ id: 'u2',
1264
+ email: 'other@test.com',
1265
+ display_name: 'Other User',
1266
+ avatar_url: '',
1267
+ status: 'active',
1268
+ onboarding_completed: true,
1269
+ created_at: now,
1270
+ updated_at: now,
1271
+ });
1272
+ saasStores.workspaceStore.create({
1273
+ id: 'ws-add',
1274
+ name: 'Add WS',
1275
+ slug: 'add-ws',
1276
+ owner_user_id: 'u1',
1277
+ plan: 'free',
1278
+ settings: {},
1279
+ created_at: now,
1280
+ updated_at: now,
1281
+ });
1282
+ saasStores.workspaceMemberStore.create({
1283
+ id: 'mem-add-owner',
1284
+ workspace_id: 'ws-add',
1285
+ user_id: 'u1',
1286
+ role: 'owner',
1287
+ joined_at: now,
1288
+ });
1289
+ });
1290
+ it('adds an existing user by email', async () => {
1291
+ const res = await (0, supertest_1.default)(app)
1292
+ .post('/api/v1/workspaces/ws-add/members')
1293
+ .set('Cookie', 'pn_session=sess1')
1294
+ .send({ email: 'other@test.com', role: 'member' })
1295
+ .expect(201);
1296
+ expect(res.body.user_id).toBe('u2');
1297
+ expect(res.body.email).toBe('other@test.com');
1298
+ expect(res.body.display_name).toBe('Other User');
1299
+ expect(res.body.role).toBe('member');
1300
+ expect(res.body.id).toBeDefined();
1301
+ });
1302
+ it('defaults role to member when invalid role given', async () => {
1303
+ const res = await (0, supertest_1.default)(app)
1304
+ .post('/api/v1/workspaces/ws-add/members')
1305
+ .set('Cookie', 'pn_session=sess1')
1306
+ .send({ email: 'other@test.com', role: 'superadmin' })
1307
+ .expect(201);
1308
+ expect(res.body.role).toBe('member');
1309
+ });
1310
+ it('returns 404 when user email not found', async () => {
1311
+ const res = await (0, supertest_1.default)(app)
1312
+ .post('/api/v1/workspaces/ws-add/members')
1313
+ .set('Cookie', 'pn_session=sess1')
1314
+ .send({ email: 'nonexistent@test.com', role: 'member' })
1315
+ .expect(404);
1316
+ expect(res.body.error).toContain('User not found');
1317
+ });
1318
+ it('returns 409 when user is already a member', async () => {
1319
+ // Add user first
1320
+ saasStores.workspaceMemberStore.create({
1321
+ id: 'mem-add-existing',
1322
+ workspace_id: 'ws-add',
1323
+ user_id: 'u2',
1324
+ role: 'member',
1325
+ joined_at: now,
1326
+ });
1327
+ const res = await (0, supertest_1.default)(app)
1328
+ .post('/api/v1/workspaces/ws-add/members')
1329
+ .set('Cookie', 'pn_session=sess1')
1330
+ .send({ email: 'other@test.com', role: 'member' })
1331
+ .expect(409);
1332
+ expect(res.body.error).toContain('already a member');
1333
+ });
1334
+ it('returns 400 when email is missing', async () => {
1335
+ const res = await (0, supertest_1.default)(app)
1336
+ .post('/api/v1/workspaces/ws-add/members')
1337
+ .set('Cookie', 'pn_session=sess1')
1338
+ .send({ role: 'member' })
1339
+ .expect(400);
1340
+ expect(res.body.error).toContain('email');
1341
+ });
1342
+ it('returns 403 when user is not owner or admin', async () => {
1343
+ saasStores.workspaceMemberStore.update('mem-add-owner', { role: 'member' });
1344
+ const res = await (0, supertest_1.default)(app)
1345
+ .post('/api/v1/workspaces/ws-add/members')
1346
+ .set('Cookie', 'pn_session=sess1')
1347
+ .send({ email: 'other@test.com', role: 'member' })
1348
+ .expect(403);
1349
+ expect(res.body.error).toContain('owners and admins');
1350
+ });
1351
+ });
1352
+ // -------------------------------------------------------------------------
1353
+ // GET /api/v1/workspaces/:id/budgets (with spending data)
1354
+ // -------------------------------------------------------------------------
1355
+ describe('GET /api/v1/workspaces/:id/budgets', () => {
1356
+ beforeEach(() => {
1357
+ saasStores.workspaceStore.create({
1358
+ id: 'ws-budgets',
1359
+ name: 'Budgets Workspace',
1360
+ slug: 'budgets-ws',
1361
+ owner_user_id: 'u1',
1362
+ plan: 'free',
1363
+ settings: {},
1364
+ created_at: now,
1365
+ updated_at: now,
1366
+ });
1367
+ saasStores.workspaceMemberStore.create({
1368
+ id: 'mem-budgets',
1369
+ workspace_id: 'ws-budgets',
1370
+ user_id: 'u1',
1371
+ role: 'owner',
1372
+ joined_at: now,
1373
+ });
1374
+ });
1375
+ it('returns budget config with spending summary', async () => {
1376
+ const res = await (0, supertest_1.default)(app)
1377
+ .get('/api/v1/workspaces/ws-budgets/budgets')
1378
+ .set('Cookie', 'pn_session=sess1')
1379
+ .expect(200);
1380
+ expect(res.body.workspace_id).toBe('ws-budgets');
1381
+ expect(res.body.config).toBeDefined();
1382
+ expect(res.body.spending).toBeDefined();
1383
+ expect(typeof res.body.spending.task_total).toBe('number');
1384
+ expect(typeof res.body.spending.user_daily_total).toBe('number');
1385
+ expect(typeof res.body.spending.user_monthly_total).toBe('number');
1386
+ expect(typeof res.body.spending.workspace_daily_total).toBe('number');
1387
+ expect(typeof res.body.spending.workspace_monthly_total).toBe('number');
1388
+ });
1389
+ it('returns 403 when user is not a member', async () => {
1390
+ saasStores.workspaceMemberStore.delete('mem-budgets');
1391
+ const res = await (0, supertest_1.default)(app)
1392
+ .get('/api/v1/workspaces/ws-budgets/budgets')
1393
+ .set('Cookie', 'pn_session=sess1')
1394
+ .expect(403);
1395
+ expect(res.body.error).toContain('Not a member');
1396
+ });
1397
+ });
1398
+ });
1399
+ //# sourceMappingURL=saas-routes.test.js.map