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,135 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { PlanTier, PLAN_LIMITS, PlanLimits } from '../types/subscription';
3
+ import { WorkspaceStore, WorkspaceMemberStore, UserApiKeyStore } from '../storage/interfaces';
4
+
5
+ export interface PlanEnforceResult {
6
+ allowed: boolean;
7
+ reason?: string;
8
+ }
9
+
10
+ export class PlanEnforcer {
11
+ static getLimits(plan: PlanTier): PlanLimits {
12
+ return PLAN_LIMITS[plan];
13
+ }
14
+
15
+ static checkCallLimit(plan: PlanTier, currentCount: number): { allowed: boolean; limit: number; current: number } {
16
+ const limits = PLAN_LIMITS[plan];
17
+ return {
18
+ allowed: currentCount < limits.calls_per_month,
19
+ limit: limits.calls_per_month,
20
+ current: currentCount,
21
+ };
22
+ }
23
+
24
+ static checkFeature(plan: PlanTier, feature: string): boolean {
25
+ return PLAN_LIMITS[plan].features.includes(feature);
26
+ }
27
+
28
+ /**
29
+ * Check if a workspace can add more members.
30
+ */
31
+ static checkMemberLimit(
32
+ plan: PlanTier,
33
+ currentMemberCount: number,
34
+ ): PlanEnforceResult {
35
+ const limits = PLAN_LIMITS[plan];
36
+ if (currentMemberCount >= limits.members_per_workspace) {
37
+ return {
38
+ allowed: false,
39
+ reason: `Plan "${plan}" allows a maximum of ${limits.members_per_workspace} members per workspace. Current: ${currentMemberCount}.`,
40
+ };
41
+ }
42
+ return { allowed: true };
43
+ }
44
+
45
+ /**
46
+ * Check if a workspace can create more API keys.
47
+ */
48
+ static checkApiKeyLimit(
49
+ plan: PlanTier,
50
+ currentKeyCount: number,
51
+ ): PlanEnforceResult {
52
+ const limits = PLAN_LIMITS[plan];
53
+ if (currentKeyCount >= limits.api_keys_per_workspace) {
54
+ return {
55
+ allowed: false,
56
+ reason: `Plan "${plan}" allows a maximum of ${limits.api_keys_per_workspace} API keys per workspace. Current: ${currentKeyCount}.`,
57
+ };
58
+ }
59
+ return { allowed: true };
60
+ }
61
+
62
+ /**
63
+ * Check if an org/user can create more workspaces.
64
+ */
65
+ static checkWorkspaceLimit(
66
+ plan: PlanTier,
67
+ currentWorkspaceCount: number,
68
+ ): PlanEnforceResult {
69
+ const limits = PLAN_LIMITS[plan];
70
+ if (currentWorkspaceCount >= limits.workspaces) {
71
+ return {
72
+ allowed: false,
73
+ reason: `Plan "${plan}" allows a maximum of ${limits.workspaces} workspaces. Current: ${currentWorkspaceCount}.`,
74
+ };
75
+ }
76
+ return { allowed: true };
77
+ }
78
+
79
+ // TODO: Integrate into gateway pipeline — call enforce() from gateway.preExecute()
80
+ // or wire createPlanEnforcerMiddleware() in src/server/app.ts before POST /v1/tool/execute.
81
+ /**
82
+ * Enforce call limits for a tool execution.
83
+ * Returns { allowed: true } or { allowed: false, reason: '...' }.
84
+ */
85
+ static enforce(plan: PlanTier, currentMonthlyCount: number): PlanEnforceResult {
86
+ const check = PlanEnforcer.checkCallLimit(plan, currentMonthlyCount);
87
+ if (!check.allowed) {
88
+ return {
89
+ allowed: false,
90
+ reason: `Monthly call limit reached for plan "${plan}": ${check.current}/${check.limit}. Upgrade your plan for more calls.`,
91
+ };
92
+ }
93
+ return { allowed: true };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Express middleware factory that enforces plan call limits before tool execution.
99
+ *
100
+ * Wire into app.ts:
101
+ * app.post('/v1/tool/execute', createPlanEnforcerMiddleware(deps), executeHandler);
102
+ */
103
+ export function createPlanEnforcerMiddleware(deps: {
104
+ workspaceStore: WorkspaceStore;
105
+ getMonthlyCallCount: (workspaceId: string) => number;
106
+ }): (req: Request, res: Response, next: NextFunction) => void {
107
+ return (req: Request, res: Response, next: NextFunction) => {
108
+ const workspaceId = req.body?.workspace_id || (req as any).workspace_id;
109
+ if (!workspaceId) {
110
+ next();
111
+ return;
112
+ }
113
+
114
+ const workspace = deps.workspaceStore.getById(workspaceId);
115
+ if (!workspace) {
116
+ next();
117
+ return;
118
+ }
119
+
120
+ const plan = (workspace.plan || 'free') as PlanTier;
121
+ const count = deps.getMonthlyCallCount(workspaceId);
122
+ const result = PlanEnforcer.enforce(plan, count);
123
+
124
+ if (!result.allowed) {
125
+ res.status(403).json({
126
+ error: result.reason,
127
+ error_code: 'PLAN_LIMIT_EXCEEDED',
128
+ hint: 'Upgrade your plan at /billing or wait until the next billing cycle.',
129
+ });
130
+ return;
131
+ }
132
+
133
+ next();
134
+ };
135
+ }
@@ -0,0 +1,229 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { StripeClient } from './stripe-client';
3
+ import { PlanEnforcer } from './plan-enforcer';
4
+ import { PlanTier, PLAN_LIMITS } from '../types/subscription';
5
+ import { StripeConfig } from '../types/stripe-config';
6
+ import {
7
+ SubscriptionStore,
8
+ WorkspaceStore,
9
+ WorkspaceMemberStore,
10
+ } from '../storage/interfaces';
11
+ import { Gateway } from '../server/gateway';
12
+
13
+ export interface BillingRouteDeps {
14
+ stripeClient: StripeClient;
15
+ stripeConfig: StripeConfig;
16
+ subscriptionStore: SubscriptionStore;
17
+ workspaceStore: WorkspaceStore;
18
+ workspaceMemberStore: WorkspaceMemberStore;
19
+ gateway: Gateway;
20
+ }
21
+
22
+ /** Extract a route param as string (Express 5 returns string | string[]). */
23
+ function param(req: Request, name: string): string {
24
+ const val = req.params[name];
25
+ return Array.isArray(val) ? val[0] : val;
26
+ }
27
+
28
+ function requireSession(req: Request, res: Response): boolean {
29
+ if (!(req as any).sessionUser) {
30
+ res.status(401).json({ error: 'Session authentication required' });
31
+ return false;
32
+ }
33
+ return true;
34
+ }
35
+
36
+ export function createBillingRouter(deps: BillingRouteDeps): Router {
37
+ const router = Router();
38
+ const { stripeClient, stripeConfig, subscriptionStore, workspaceStore, workspaceMemberStore, gateway } = deps;
39
+
40
+ // GET /workspaces/:id/billing - current plan + subscription status
41
+ router.get('/workspaces/:id/billing', (req: Request, res: Response) => {
42
+ if (!requireSession(req, res)) return;
43
+ const user = (req as any).sessionUser;
44
+ const workspaceId = param(req, 'id');
45
+
46
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
47
+ if (!membership) {
48
+ res.status(403).json({ error: 'Not a member of this workspace' });
49
+ return;
50
+ }
51
+
52
+ const workspace = workspaceStore.getById(workspaceId);
53
+ if (!workspace) {
54
+ res.status(404).json({ error: 'Workspace not found' });
55
+ return;
56
+ }
57
+
58
+ const subscription = subscriptionStore.getByWorkspace(workspaceId);
59
+ const plan = (workspace.plan || 'free') as PlanTier;
60
+ const limits = PlanEnforcer.getLimits(plan);
61
+
62
+ res.json({
63
+ plan,
64
+ limits,
65
+ subscription: subscription ? {
66
+ id: subscription.id,
67
+ status: subscription.status,
68
+ current_period_start: subscription.current_period_start,
69
+ current_period_end: subscription.current_period_end,
70
+ cancel_at_period_end: subscription.cancel_at_period_end,
71
+ } : null,
72
+ });
73
+ });
74
+
75
+ // POST /workspaces/:id/billing/checkout - create Stripe Checkout session
76
+ router.post('/workspaces/:id/billing/checkout', async (req: Request, res: Response) => {
77
+ if (!requireSession(req, res)) return;
78
+ const user = (req as any).sessionUser;
79
+ const workspaceId = param(req, 'id');
80
+
81
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
82
+ if (!membership || !['owner', 'admin'].includes(membership.role)) {
83
+ res.status(403).json({ error: 'Only workspace owners and admins can manage billing' });
84
+ return;
85
+ }
86
+
87
+ const { plan } = req.body;
88
+ if (!plan || !['pro', 'business'].includes(plan)) {
89
+ res.status(400).json({ error: 'plan must be "pro" or "business"' });
90
+ return;
91
+ }
92
+
93
+ const priceId = plan === 'pro'
94
+ ? stripeConfig.price_ids.pro_monthly
95
+ : stripeConfig.price_ids.business_monthly;
96
+
97
+ try {
98
+ // Get or create Stripe customer
99
+ let subscription = subscriptionStore.getByWorkspace(workspaceId);
100
+ let customerId: string;
101
+
102
+ if (subscription?.stripe_customer_id) {
103
+ customerId = subscription.stripe_customer_id;
104
+ } else {
105
+ const customer = await stripeClient.createCustomer(user.email, workspaceId, user.display_name);
106
+ customerId = customer.id;
107
+ }
108
+
109
+ const session = await stripeClient.createCheckoutSession(customerId, priceId, workspaceId);
110
+ res.json({ url: session.url });
111
+ } catch (err) {
112
+ const message = err instanceof Error ? err.message : 'Failed to create checkout session';
113
+ res.status(500).json({ error: message });
114
+ }
115
+ });
116
+
117
+ // POST /workspaces/:id/billing/portal - create Customer Portal session
118
+ router.post('/workspaces/:id/billing/portal', async (req: Request, res: Response) => {
119
+ if (!requireSession(req, res)) return;
120
+ const user = (req as any).sessionUser;
121
+ const workspaceId = param(req, 'id');
122
+
123
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
124
+ if (!membership || !['owner', 'admin'].includes(membership.role)) {
125
+ res.status(403).json({ error: 'Only workspace owners and admins can manage billing' });
126
+ return;
127
+ }
128
+
129
+ const subscription = subscriptionStore.getByWorkspace(workspaceId);
130
+ if (!subscription?.stripe_customer_id) {
131
+ res.status(400).json({ error: 'No billing account found. Subscribe to a plan first.' });
132
+ return;
133
+ }
134
+
135
+ try {
136
+ const session = await stripeClient.createPortalSession(subscription.stripe_customer_id);
137
+ res.json({ url: session.url });
138
+ } catch (err) {
139
+ const message = err instanceof Error ? err.message : 'Failed to create portal session';
140
+ res.status(500).json({ error: message });
141
+ }
142
+ });
143
+
144
+ // GET /workspaces/:id/billing/invoices - invoice history
145
+ router.get('/workspaces/:id/billing/invoices', async (req: Request, res: Response) => {
146
+ if (!requireSession(req, res)) return;
147
+ const user = (req as any).sessionUser;
148
+ const workspaceId = param(req, 'id');
149
+
150
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
151
+ if (!membership) {
152
+ res.status(403).json({ error: 'Not a member of this workspace' });
153
+ return;
154
+ }
155
+
156
+ const subscription = subscriptionStore.getByWorkspace(workspaceId);
157
+ if (!subscription?.stripe_customer_id) {
158
+ res.json({ invoices: [] });
159
+ return;
160
+ }
161
+
162
+ try {
163
+ const invoices = await stripeClient.getInvoices(subscription.stripe_customer_id);
164
+ res.json({
165
+ invoices: invoices.map(inv => ({
166
+ id: inv.id,
167
+ number: inv.number,
168
+ status: inv.status,
169
+ amount_due: inv.amount_due,
170
+ amount_paid: inv.amount_paid,
171
+ currency: inv.currency,
172
+ period_start: inv.period_start ? new Date(inv.period_start * 1000).toISOString() : null,
173
+ period_end: inv.period_end ? new Date(inv.period_end * 1000).toISOString() : null,
174
+ hosted_invoice_url: inv.hosted_invoice_url,
175
+ created: inv.created ? new Date(inv.created * 1000).toISOString() : null,
176
+ })),
177
+ });
178
+ } catch (err) {
179
+ const message = err instanceof Error ? err.message : 'Failed to fetch invoices';
180
+ res.status(500).json({ error: message });
181
+ }
182
+ });
183
+
184
+ // GET /workspaces/:id/billing/usage - monthly call count vs limit
185
+ router.get('/workspaces/:id/billing/usage', (req: Request, res: Response) => {
186
+ if (!requireSession(req, res)) return;
187
+ const user = (req as any).sessionUser;
188
+ const workspaceId = param(req, 'id');
189
+
190
+ const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
191
+ if (!membership) {
192
+ res.status(403).json({ error: 'Not a member of this workspace' });
193
+ return;
194
+ }
195
+
196
+ const workspace = workspaceStore.getById(workspaceId);
197
+ if (!workspace) {
198
+ res.status(404).json({ error: 'Workspace not found' });
199
+ return;
200
+ }
201
+
202
+ const plan = (workspace.plan || 'free') as PlanTier;
203
+
204
+ // Count events for this workspace in the current month
205
+ const allEvents = gateway.getAuditLogger().getAllEvents();
206
+ const now = new Date();
207
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
208
+ const wsSlug = workspace.slug;
209
+
210
+ const monthlyCount = allEvents.filter(e => {
211
+ const matches = e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug);
212
+ if (!matches) return false;
213
+ const eventDate = new Date(e.timestamp);
214
+ return eventDate >= monthStart;
215
+ }).length;
216
+
217
+ const check = PlanEnforcer.checkCallLimit(plan, monthlyCount);
218
+
219
+ res.json({
220
+ plan,
221
+ calls_this_month: monthlyCount,
222
+ calls_limit: check.limit,
223
+ allowed: check.allowed,
224
+ usage_percent: check.limit === Infinity ? 0 : Math.round((monthlyCount / check.limit) * 100),
225
+ });
226
+ });
227
+
228
+ return router;
229
+ }
@@ -0,0 +1,58 @@
1
+ import Stripe from 'stripe';
2
+ import { StripeConfig } from '../types/stripe-config';
3
+
4
+ export class StripeClient {
5
+ private stripe: Stripe;
6
+ private config: StripeConfig;
7
+
8
+ constructor(config: StripeConfig) {
9
+ this.stripe = new Stripe(config.secret_key);
10
+ this.config = config;
11
+ }
12
+
13
+ async createCustomer(email: string, workspaceId: string, name?: string): Promise<Stripe.Customer> {
14
+ return this.stripe.customers.create({
15
+ email,
16
+ name: name || undefined,
17
+ metadata: { workspace_id: workspaceId },
18
+ });
19
+ }
20
+
21
+ async createCheckoutSession(customerId: string, priceId: string, workspaceId: string): Promise<Stripe.Checkout.Session> {
22
+ return this.stripe.checkout.sessions.create({
23
+ customer: customerId,
24
+ mode: 'subscription',
25
+ line_items: [{ price: priceId, quantity: 1 }],
26
+ success_url: this.config.checkout_success_url || `${process.env.APP_BASE_URL || 'http://localhost:5173'}/billing?success=true&session_id={CHECKOUT_SESSION_ID}`,
27
+ cancel_url: this.config.checkout_cancel_url || `${process.env.APP_BASE_URL || 'http://localhost:5173'}/billing?canceled=true`,
28
+ metadata: { workspace_id: workspaceId },
29
+ });
30
+ }
31
+
32
+ async createPortalSession(customerId: string): Promise<Stripe.BillingPortal.Session> {
33
+ return this.stripe.billingPortal.sessions.create({
34
+ customer: customerId,
35
+ return_url: this.config.portal_return_url || undefined,
36
+ });
37
+ }
38
+
39
+ async getSubscription(subscriptionId: string): Promise<Stripe.Subscription> {
40
+ return this.stripe.subscriptions.retrieve(subscriptionId);
41
+ }
42
+
43
+ async getInvoices(customerId: string, limit: number = 10): Promise<Stripe.Invoice[]> {
44
+ const result = await this.stripe.invoices.list({
45
+ customer: customerId,
46
+ limit,
47
+ });
48
+ return result.data;
49
+ }
50
+
51
+ constructEvent(payload: Buffer, signature: string): Stripe.Event {
52
+ return this.stripe.webhooks.constructEvent(
53
+ payload,
54
+ signature,
55
+ this.config.webhook_secret,
56
+ );
57
+ }
58
+ }
@@ -0,0 +1,182 @@
1
+ import Stripe from 'stripe';
2
+ import { randomUUID } from 'crypto';
3
+ import { SubscriptionStore, WorkspaceStore } from '../storage/interfaces';
4
+ import { PlanTier } from '../types/subscription';
5
+ import { StripeConfig } from '../types/stripe-config';
6
+
7
+ export class WebhookHandler {
8
+ constructor(
9
+ private subscriptionStore: SubscriptionStore,
10
+ private workspaceStore: WorkspaceStore,
11
+ private config: StripeConfig,
12
+ ) {}
13
+
14
+ async handleEvent(event: Stripe.Event): Promise<void> {
15
+ switch (event.type) {
16
+ case 'checkout.session.completed':
17
+ await this.handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session);
18
+ break;
19
+ case 'customer.subscription.updated':
20
+ await this.handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
21
+ break;
22
+ case 'customer.subscription.deleted':
23
+ await this.handleSubscriptionDeleted(event.data.object as Stripe.Subscription);
24
+ break;
25
+ case 'invoice.payment_failed':
26
+ await this.handlePaymentFailed(event.data.object as Stripe.Invoice);
27
+ break;
28
+ case 'invoice.payment_succeeded':
29
+ await this.handlePaymentSucceeded(event.data.object as Stripe.Invoice);
30
+ break;
31
+ }
32
+ }
33
+
34
+ private resolvePlan(priceId: string): PlanTier {
35
+ if (priceId === this.config.price_ids.pro_monthly) return 'pro';
36
+ if (priceId === this.config.price_ids.business_monthly) return 'business';
37
+ return 'free';
38
+ }
39
+
40
+ private async handleCheckoutCompleted(session: Stripe.Checkout.Session): Promise<void> {
41
+ const workspaceId = session.metadata?.workspace_id;
42
+ if (!workspaceId || !session.subscription || !session.customer) return;
43
+
44
+ const subscriptionId = typeof session.subscription === 'string' ? session.subscription : session.subscription.id;
45
+ const customerId = typeof session.customer === 'string' ? session.customer : session.customer.id;
46
+
47
+ // Check for existing subscription by workspace (idempotency)
48
+ const existing = this.subscriptionStore.getByWorkspace(workspaceId);
49
+ if (existing && existing.stripe_subscription_id === subscriptionId) return;
50
+
51
+ // Resolve plan from the checkout session line items (stored in subscription)
52
+ // We need to determine plan from the price ID - use a default for now,
53
+ // the subscription.updated event will set the correct plan
54
+ const now = new Date().toISOString();
55
+
56
+ if (existing) {
57
+ // Update existing subscription
58
+ this.subscriptionStore.update(existing.id, {
59
+ stripe_subscription_id: subscriptionId,
60
+ stripe_customer_id: customerId,
61
+ status: 'active',
62
+ updated_at: now,
63
+ });
64
+ } else {
65
+ // Create new subscription record
66
+ this.subscriptionStore.create({
67
+ id: randomUUID(),
68
+ workspace_id: workspaceId,
69
+ stripe_customer_id: customerId,
70
+ stripe_subscription_id: subscriptionId,
71
+ plan: 'pro', // Default; subscription.updated will correct this
72
+ status: 'active',
73
+ created_at: now,
74
+ updated_at: now,
75
+ });
76
+ }
77
+ }
78
+
79
+ private async handleSubscriptionUpdated(subscription: Stripe.Subscription): Promise<void> {
80
+ const existing = this.subscriptionStore.getByStripeSubscriptionId(subscription.id);
81
+ if (!existing) return;
82
+
83
+ const now = new Date().toISOString();
84
+ // Guard: skip if our record is strictly newer than this event's creation timestamp
85
+ const eventCreatedAt = new Date(subscription.created * 1000).toISOString();
86
+ if (existing.updated_at > eventCreatedAt) return;
87
+
88
+ // Resolve plan from the first line item's price
89
+ const priceId = subscription.items?.data?.[0]?.price?.id;
90
+ const plan = priceId ? this.resolvePlan(priceId) : existing.plan;
91
+
92
+ const status = this.mapStripeStatus(subscription.status);
93
+
94
+ // Access period timestamps (Stripe SDK types vary across versions)
95
+ const subAny = subscription as any;
96
+ const periodStart = subAny.current_period_start;
97
+ const periodEnd = subAny.current_period_end;
98
+
99
+ this.subscriptionStore.update(existing.id, {
100
+ plan,
101
+ status,
102
+ current_period_start: periodStart ? new Date(periodStart * 1000).toISOString() : undefined,
103
+ current_period_end: periodEnd ? new Date(periodEnd * 1000).toISOString() : undefined,
104
+ cancel_at_period_end: subscription.cancel_at_period_end,
105
+ updated_at: now,
106
+ });
107
+
108
+ // Update workspace plan
109
+ this.workspaceStore.update(existing.workspace_id, {
110
+ plan,
111
+ updated_at: now,
112
+ });
113
+ }
114
+
115
+ private async handleSubscriptionDeleted(subscription: Stripe.Subscription): Promise<void> {
116
+ const existing = this.subscriptionStore.getByStripeSubscriptionId(subscription.id);
117
+ if (!existing) return;
118
+
119
+ const now = new Date().toISOString();
120
+
121
+ this.subscriptionStore.update(existing.id, {
122
+ status: 'canceled',
123
+ plan: 'free',
124
+ updated_at: now,
125
+ });
126
+
127
+ // Downgrade workspace to free
128
+ this.workspaceStore.update(existing.workspace_id, {
129
+ plan: 'free',
130
+ updated_at: now,
131
+ });
132
+ }
133
+
134
+ private getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | undefined {
135
+ // Stripe SDK types vary across versions for invoice.subscription
136
+ const sub = (invoice as any).subscription;
137
+ if (!sub) return undefined;
138
+ return typeof sub === 'string' ? sub : sub.id;
139
+ }
140
+
141
+ private async handlePaymentFailed(invoice: Stripe.Invoice): Promise<void> {
142
+ const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
143
+ if (!subscriptionId) return;
144
+
145
+ const existing = this.subscriptionStore.getByStripeSubscriptionId(subscriptionId);
146
+ if (!existing) return;
147
+
148
+ this.subscriptionStore.update(existing.id, {
149
+ status: 'past_due',
150
+ updated_at: new Date().toISOString(),
151
+ });
152
+ }
153
+
154
+ private async handlePaymentSucceeded(invoice: Stripe.Invoice): Promise<void> {
155
+ const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
156
+ if (!subscriptionId) return;
157
+
158
+ const existing = this.subscriptionStore.getByStripeSubscriptionId(subscriptionId);
159
+ if (!existing) return;
160
+
161
+ if (existing.status === 'past_due' || existing.status === 'incomplete') {
162
+ this.subscriptionStore.update(existing.id, {
163
+ status: 'active',
164
+ updated_at: new Date().toISOString(),
165
+ });
166
+ }
167
+ }
168
+
169
+ private mapStripeStatus(status: Stripe.Subscription.Status): 'active' | 'past_due' | 'canceled' | 'trialing' | 'incomplete' {
170
+ switch (status) {
171
+ case 'active': return 'active';
172
+ case 'past_due': return 'past_due';
173
+ case 'canceled': return 'canceled';
174
+ case 'trialing': return 'trialing';
175
+ case 'incomplete': return 'incomplete';
176
+ case 'incomplete_expired': return 'canceled';
177
+ case 'unpaid': return 'past_due';
178
+ case 'paused': return 'canceled';
179
+ default: return 'active';
180
+ }
181
+ }
182
+ }
@@ -0,0 +1,28 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import express from 'express';
3
+ import { StripeClient } from './stripe-client';
4
+ import { WebhookHandler } from './webhook-handler';
5
+
6
+ export function createWebhookRouter(stripeClient: StripeClient, handler: WebhookHandler): Router {
7
+ const router = Router();
8
+
9
+ // IMPORTANT: Use express.raw() for Stripe signature verification
10
+ router.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (req: Request, res: Response) => {
11
+ const sig = req.headers['stripe-signature'] as string;
12
+ if (!sig) {
13
+ res.status(400).json({ error: 'Missing stripe-signature header' });
14
+ return;
15
+ }
16
+
17
+ try {
18
+ const event = stripeClient.constructEvent(req.body, sig);
19
+ await handler.handleEvent(event);
20
+ res.json({ received: true });
21
+ } catch (err) {
22
+ const message = err instanceof Error ? err.message : 'Webhook signature verification failed';
23
+ res.status(400).json({ error: message });
24
+ }
25
+ });
26
+
27
+ return router;
28
+ }