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,14 @@
1
+ /**
2
+ * Side-channel for passing MCP OAuth tokens to the HttpExecutor without
3
+ * polluting ToolCall.args (which would trigger DLP and leak into audit logs).
4
+ *
5
+ * Keyed by `tool_call_id` — the MCP handler sets an entry before gateway.execute(),
6
+ * and the HttpExecutor consumes (and deletes) it during request execution.
7
+ */
8
+
9
+ export interface InternalAuthEntry {
10
+ token: string;
11
+ gateway_base_url: string;
12
+ }
13
+
14
+ export const internalAuthTokens = new Map<string, InternalAuthEntry>();
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Server-rendered HTML pages for MCP OAuth 2.0 consent flow.
3
+ * Follows the same pattern as src/admin/templates.ts.
4
+ */
5
+
6
+ function escapeHtml(str: string): string {
7
+ return String(str)
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#039;');
13
+ }
14
+
15
+ export interface ConsentPageParams {
16
+ clientName: string;
17
+ scopes: string[];
18
+ workspaces: { id: string; name: string; slug: string }[];
19
+ /** Hidden fields to pass through the OAuth params */
20
+ clientId: string;
21
+ redirectUri: string;
22
+ state?: string;
23
+ codeChallenge: string;
24
+ /** CSRF token */
25
+ csrfToken: string;
26
+ }
27
+
28
+ export function renderConsentPage(params: ConsentPageParams): string {
29
+ const workspaceOptions = params.workspaces
30
+ .map(
31
+ (w) =>
32
+ `<option value="${escapeHtml(w.id)}">${escapeHtml(w.name)} (${escapeHtml(w.slug)})</option>`,
33
+ )
34
+ .join('\n');
35
+
36
+ const scopeList = params.scopes
37
+ .map((s) => `<li>${escapeHtml(s)}</li>`)
38
+ .join('\n');
39
+
40
+ return `<!DOCTYPE html>
41
+ <html>
42
+ <head>
43
+ <meta charset="utf-8">
44
+ <meta name="viewport" content="width=device-width, initial-scale=1">
45
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; form-action 'self' http://localhost:* http://127.0.0.1:*">
46
+ <title>Authorize - Palaryn</title>
47
+ <style>
48
+ * { margin: 0; padding: 0; box-sizing: border-box; }
49
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
50
+ .card { background: #fff; border-radius: 12px; padding: 32px; max-width: 440px; width: 100%; box-shadow: 0 4px 24px rgba(0,0,0,0.08); }
51
+ .logo { font-size: 20px; font-weight: 700; color: #1a1d23; margin-bottom: 4px; }
52
+ .subtitle { font-size: 14px; color: #6b7280; margin-bottom: 24px; }
53
+ .client-name { font-weight: 600; color: #1a1d23; }
54
+ .section-label { font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 8px; }
55
+ .scope-list { list-style: none; margin-bottom: 20px; }
56
+ .scope-list li { padding: 6px 0; font-size: 14px; color: #4b5563; }
57
+ .scope-list li::before { content: "\\2713"; color: #10b981; margin-right: 8px; font-weight: 700; }
58
+ select { width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; margin-bottom: 24px; background: #fff; }
59
+ .btn-row { display: flex; gap: 12px; }
60
+ .btn { flex: 1; padding: 10px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
61
+ .btn-primary { background: #3b82f6; color: #fff; }
62
+ .btn-primary:hover { background: #2563eb; }
63
+ .btn-secondary { background: #f3f4f6; color: #374151; }
64
+ .btn-secondary:hover { background: #e5e7eb; }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <div class="card">
69
+ <div class="logo">Palaryn</div>
70
+ <div class="subtitle">
71
+ <span class="client-name">${escapeHtml(params.clientName)}</span> wants to access your account
72
+ </div>
73
+
74
+ <div class="section-label">Permissions requested</div>
75
+ <ul class="scope-list">
76
+ ${scopeList || '<li>Execute tool calls through the gateway</li>'}
77
+ </ul>
78
+
79
+ <form method="POST" action="/authorize/decision">
80
+ <input type="hidden" name="client_id" value="${escapeHtml(params.clientId)}">
81
+ <input type="hidden" name="redirect_uri" value="${escapeHtml(params.redirectUri)}">
82
+ <input type="hidden" name="state" value="${escapeHtml(params.state || '')}">
83
+ <input type="hidden" name="code_challenge" value="${escapeHtml(params.codeChallenge)}">
84
+ <input type="hidden" name="scopes" value="${escapeHtml(params.scopes.join(' '))}">
85
+ <input type="hidden" name="csrf_token" value="${escapeHtml(params.csrfToken)}">
86
+
87
+ ${
88
+ params.workspaces.length > 1
89
+ ? `<div class="section-label">Workspace</div>
90
+ <select name="workspace_id">${workspaceOptions}</select>`
91
+ : `<input type="hidden" name="workspace_id" value="${escapeHtml(params.workspaces[0]?.id || '')}">`
92
+ }
93
+
94
+ <div class="btn-row">
95
+ <button type="submit" name="decision" value="deny" class="btn btn-secondary">Deny</button>
96
+ <button type="submit" name="decision" value="approve" class="btn btn-primary" id="approve-btn">Approve</button>
97
+ </div>
98
+ </form>
99
+ </div>
100
+ <script>
101
+ document.querySelector('form').addEventListener('submit', function(e) {
102
+ var form = this;
103
+ setTimeout(function() {
104
+ var btns = form.querySelectorAll('button');
105
+ for (var i = 0; i < btns.length; i++) { btns[i].disabled = true; }
106
+ document.getElementById('approve-btn').textContent = 'Authorizing...';
107
+ }, 0);
108
+ });
109
+ </script>
110
+ </body>
111
+ </html>`;
112
+ }
113
+
114
+ export function renderErrorPage(error: string, description?: string): string {
115
+ return `<!DOCTYPE html>
116
+ <html>
117
+ <head>
118
+ <meta charset="utf-8">
119
+ <meta name="viewport" content="width=device-width, initial-scale=1">
120
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'none'; style-src 'unsafe-inline'">
121
+ <title>Error - Palaryn</title>
122
+ <style>
123
+ * { margin: 0; padding: 0; box-sizing: border-box; }
124
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
125
+ .card { background: #fff; border-radius: 12px; padding: 32px; max-width: 440px; width: 100%; box-shadow: 0 4px 24px rgba(0,0,0,0.08); text-align: center; }
126
+ .logo { font-size: 20px; font-weight: 700; color: #1a1d23; margin-bottom: 16px; }
127
+ .error { font-size: 16px; font-weight: 600; color: #dc2626; margin-bottom: 8px; }
128
+ .desc { font-size: 14px; color: #6b7280; }
129
+ </style>
130
+ </head>
131
+ <body>
132
+ <div class="card">
133
+ <div class="logo">Palaryn</div>
134
+ <div class="error">${escapeHtml(error)}</div>
135
+ ${description ? `<div class="desc">${escapeHtml(description)}</div>` : ''}
136
+ </div>
137
+ </body>
138
+ </html>`;
139
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * PostgreSQL-backed OAuth stores for MCP OAuth 2.0 flow.
3
+ *
4
+ * Extends the in-memory stores with write-through persistence to PostgreSQL.
5
+ * Follows the same pattern as other Postgres stores in src/storage/postgres.ts:
6
+ * - In-memory cache serves fast reads
7
+ * - Async writes to Postgres for durability
8
+ * - Hydration from DB at startup
9
+ *
10
+ * Auth codes (10-min TTL) remain in-memory only — not worth persisting.
11
+ */
12
+ import { Pool } from 'pg';
13
+ import { randomUUID, randomBytes } from 'crypto';
14
+ import { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js';
15
+ import { OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js';
16
+ import { StoredToken } from './oauth-stores';
17
+
18
+ // Reliable async query with retry (same pattern as storage/postgres.ts)
19
+ async function reliableAsyncQuery(pool: Pool, sql: string, params?: unknown[]): Promise<void> {
20
+ for (let attempt = 0; attempt < 3; attempt++) {
21
+ try {
22
+ await pool.query(sql, params);
23
+ return;
24
+ } catch (err: any) {
25
+ if (attempt === 2) {
26
+ console.error('[OAuth Postgres] query failed after 3 attempts:', err.message);
27
+ } else {
28
+ await new Promise(r => setTimeout(r, 100 * (attempt + 1)));
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // PostgresOAuthClientsStore
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export class PostgresOAuthClientsStore implements OAuthRegisteredClientsStore {
39
+ private clients = new Map<string, OAuthClientInformationFull>();
40
+ private _pendingWrites: Promise<void>[] = [];
41
+
42
+ constructor(private pool: Pool) {}
43
+
44
+ static async createTables(pool: Pool): Promise<void> {
45
+ await pool.query(`
46
+ CREATE TABLE IF NOT EXISTS mcp_oauth_clients (
47
+ client_id TEXT PRIMARY KEY,
48
+ data JSONB NOT NULL,
49
+ created_at TEXT NOT NULL DEFAULT NOW()
50
+ );
51
+ `);
52
+ }
53
+
54
+ async hydrate(): Promise<void> {
55
+ try {
56
+ const { rows } = await this.pool.query('SELECT client_id, data FROM mcp_oauth_clients');
57
+ for (const r of rows) {
58
+ this.clients.set(r.client_id, r.data);
59
+ }
60
+ } catch (err: any) {
61
+ console.error('[PostgresOAuthClientsStore] hydrate failed:', err.message);
62
+ }
63
+ }
64
+
65
+ getClient(clientId: string): OAuthClientInformationFull | undefined {
66
+ return this.clients.get(clientId);
67
+ }
68
+
69
+ registerClient(
70
+ client: Omit<OAuthClientInformationFull, 'client_id' | 'client_id_issued_at'>,
71
+ ): OAuthClientInformationFull {
72
+ const clientId = `pn_client_${randomUUID().replace(/-/g, '').slice(0, 16)}`;
73
+ const clientSecret = randomBytes(32).toString('hex');
74
+ const full: OAuthClientInformationFull = {
75
+ ...client,
76
+ client_id: clientId,
77
+ client_secret: clientSecret,
78
+ client_id_issued_at: Math.floor(Date.now() / 1000),
79
+ };
80
+ this.clients.set(clientId, full);
81
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
82
+ `INSERT INTO mcp_oauth_clients (client_id, data) VALUES ($1, $2)
83
+ ON CONFLICT (client_id) DO UPDATE SET data = $2`,
84
+ [clientId, JSON.stringify(full)],
85
+ ));
86
+ return full;
87
+ }
88
+
89
+ /** Serialize for persistence (compatibility with file-persistence) */
90
+ entries(): [string, OAuthClientInformationFull][] {
91
+ return Array.from(this.clients.entries());
92
+ }
93
+
94
+ /** Load from persisted data (compatibility with file-persistence) */
95
+ load(entries: [string, OAuthClientInformationFull][]): void {
96
+ for (const [id, client] of entries) {
97
+ this.clients.set(id, client);
98
+ }
99
+ }
100
+
101
+ clear(): void {
102
+ this.clients.clear();
103
+ this._pendingWrites.push(reliableAsyncQuery(this.pool, 'TRUNCATE mcp_oauth_clients'));
104
+ }
105
+
106
+ async flush(): Promise<void> {
107
+ const writes = this._pendingWrites;
108
+ this._pendingWrites = [];
109
+ await Promise.all(writes);
110
+ }
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // PostgresOAuthTokenStore
115
+ // ---------------------------------------------------------------------------
116
+
117
+ export class PostgresOAuthTokenStore {
118
+ private accessTokens = new Map<string, StoredToken>();
119
+ private refreshTokens = new Map<string, StoredToken>();
120
+ private cleanupTimer: ReturnType<typeof setInterval>;
121
+ private _pendingWrites: Promise<void>[] = [];
122
+
123
+ constructor(
124
+ private pool: Pool,
125
+ private accessTtlSec: number = 3600,
126
+ private refreshTtlSec: number = 30 * 24 * 3600,
127
+ ) {
128
+ this.cleanupTimer = setInterval(() => this.cleanup(), 5 * 60_000);
129
+ this.cleanupTimer.unref();
130
+ }
131
+
132
+ static async createTables(pool: Pool): Promise<void> {
133
+ await pool.query(`
134
+ CREATE TABLE IF NOT EXISTS mcp_oauth_tokens (
135
+ token TEXT PRIMARY KEY,
136
+ token_type TEXT NOT NULL,
137
+ client_id TEXT NOT NULL,
138
+ user_id TEXT NOT NULL,
139
+ workspace_id TEXT NOT NULL,
140
+ scopes JSONB NOT NULL DEFAULT '[]',
141
+ expires_at BIGINT NOT NULL,
142
+ created_at BIGINT NOT NULL
143
+ );
144
+ `);
145
+ await pool.query(`
146
+ CREATE INDEX IF NOT EXISTS idx_mcp_oauth_tokens_type ON mcp_oauth_tokens (token_type);
147
+ `);
148
+ await pool.query(`
149
+ CREATE INDEX IF NOT EXISTS idx_mcp_oauth_tokens_user ON mcp_oauth_tokens (user_id);
150
+ `);
151
+ }
152
+
153
+ async hydrate(): Promise<void> {
154
+ try {
155
+ const nowSec = Date.now() / 1000;
156
+ const { rows } = await this.pool.query(
157
+ 'SELECT * FROM mcp_oauth_tokens WHERE expires_at > $1',
158
+ [Math.floor(nowSec)],
159
+ );
160
+ for (const r of rows) {
161
+ const token: StoredToken = {
162
+ token: r.token,
163
+ clientId: r.client_id,
164
+ userId: r.user_id,
165
+ workspaceId: r.workspace_id,
166
+ scopes: r.scopes || [],
167
+ expiresAt: Number(r.expires_at),
168
+ createdAt: Number(r.created_at),
169
+ };
170
+ if (r.token_type === 'access') {
171
+ this.accessTokens.set(r.token, token);
172
+ } else {
173
+ this.refreshTokens.set(r.token, token);
174
+ }
175
+ }
176
+ // Clean up expired rows in background
177
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
178
+ 'DELETE FROM mcp_oauth_tokens WHERE expires_at <= $1',
179
+ [Math.floor(nowSec)],
180
+ ));
181
+ } catch (err: any) {
182
+ console.error('[PostgresOAuthTokenStore] hydrate failed:', err.message);
183
+ }
184
+ }
185
+
186
+ saveAccessToken(entry: StoredToken): void {
187
+ this.accessTokens.set(entry.token, entry);
188
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
189
+ `INSERT INTO mcp_oauth_tokens (token, token_type, client_id, user_id, workspace_id, scopes, expires_at, created_at)
190
+ VALUES ($1, 'access', $2, $3, $4, $5, $6, $7)
191
+ ON CONFLICT (token) DO UPDATE SET expires_at = $6`,
192
+ [entry.token, entry.clientId, entry.userId, entry.workspaceId,
193
+ JSON.stringify(entry.scopes), entry.expiresAt, entry.createdAt],
194
+ ));
195
+ }
196
+
197
+ getAccessToken(token: string): StoredToken | undefined {
198
+ const entry = this.accessTokens.get(token);
199
+ if (!entry) return undefined;
200
+ if (Date.now() / 1000 > entry.expiresAt) {
201
+ this.accessTokens.delete(token);
202
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
203
+ 'DELETE FROM mcp_oauth_tokens WHERE token = $1', [token]));
204
+ return undefined;
205
+ }
206
+ return entry;
207
+ }
208
+
209
+ revokeAccessToken(token: string): void {
210
+ this.accessTokens.delete(token);
211
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
212
+ 'DELETE FROM mcp_oauth_tokens WHERE token = $1', [token]));
213
+ }
214
+
215
+ saveRefreshToken(entry: StoredToken): void {
216
+ this.refreshTokens.set(entry.token, entry);
217
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
218
+ `INSERT INTO mcp_oauth_tokens (token, token_type, client_id, user_id, workspace_id, scopes, expires_at, created_at)
219
+ VALUES ($1, 'refresh', $2, $3, $4, $5, $6, $7)
220
+ ON CONFLICT (token) DO UPDATE SET expires_at = $6`,
221
+ [entry.token, entry.clientId, entry.userId, entry.workspaceId,
222
+ JSON.stringify(entry.scopes), entry.expiresAt, entry.createdAt],
223
+ ));
224
+ }
225
+
226
+ getRefreshToken(token: string): StoredToken | undefined {
227
+ const entry = this.refreshTokens.get(token);
228
+ if (!entry) return undefined;
229
+ if (Date.now() / 1000 > entry.expiresAt) {
230
+ this.refreshTokens.delete(token);
231
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
232
+ 'DELETE FROM mcp_oauth_tokens WHERE token = $1', [token]));
233
+ return undefined;
234
+ }
235
+ return entry;
236
+ }
237
+
238
+ revokeRefreshToken(token: string): void {
239
+ this.refreshTokens.delete(token);
240
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
241
+ 'DELETE FROM mcp_oauth_tokens WHERE token = $1', [token]));
242
+ }
243
+
244
+ get accessTtlSeconds(): number {
245
+ return this.accessTtlSec;
246
+ }
247
+
248
+ get refreshTtlSeconds(): number {
249
+ return this.refreshTtlSec;
250
+ }
251
+
252
+ private cleanup(): void {
253
+ const nowSec = Date.now() / 1000;
254
+ for (const [t, entry] of this.accessTokens) {
255
+ if (nowSec > entry.expiresAt) this.accessTokens.delete(t);
256
+ }
257
+ for (const [t, entry] of this.refreshTokens) {
258
+ if (nowSec > entry.expiresAt) this.refreshTokens.delete(t);
259
+ }
260
+ // Also clean DB
261
+ this._pendingWrites.push(reliableAsyncQuery(this.pool,
262
+ 'DELETE FROM mcp_oauth_tokens WHERE expires_at <= $1',
263
+ [Math.floor(nowSec)],
264
+ ));
265
+ }
266
+
267
+ destroy(): void {
268
+ clearInterval(this.cleanupTimer);
269
+ this.accessTokens.clear();
270
+ this.refreshTokens.clear();
271
+ }
272
+
273
+ async flush(): Promise<void> {
274
+ const writes = this._pendingWrites;
275
+ this._pendingWrites = [];
276
+ await Promise.all(writes);
277
+ }
278
+ }